When a user starts a trial, schedule the downgrade for when it ends.
// POST /api/schedule
const res = await fetch("https://<worker>/api/schedule", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Cookie": sessionCookie,
},
body: JSON.stringify({
target_url: "https://yourapp.com/api/downgrade",
execute_at: trialEndDate.toISOString(),
payload: { user_id: "u_123", plan: "free" },
}),
});
Soft-delete immediately, schedule hard-delete in 30 minutes. Cancel if the user clicks undo.
// Schedule hard delete 30 min from now
const deleteAt = new Date(Date.now() + 30 * 60 * 1000);
const { id: jobId } = await fetch("https://<worker>/api/schedule", {
method: "POST",
headers: { "Content-Type": "application/json", "Cookie": sessionCookie },
body: JSON.stringify({
target_url: "https://yourapp.com/api/hard-delete",
execute_at: deleteAt.toISOString(),
payload: { record_id: "rec_456" },
}),
}).then(r => r.json());
// Store jobId so you can show "undo" in your UI
// (RunLater MVP does not support cancellation yet -
// your hard-delete endpoint should check if the
// record was restored before deleting)
Every RunLater request includes X-RunLater-Timestamp and X-RunLater-Signature headers.
const crypto = require("crypto");
function verifyRunLater(req, secret) {
const timestamp = req.headers["x-runlater-timestamp"];
const signature = req.headers["x-runlater-signature"];
const body = req.rawBody; // exact request body string
// Reject old timestamps (5 min window)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
The signature is computed as:
HMAC-SHA256(your_webhook_secret, timestamp + "." + raw_request_body)
Your webhook secret is available on the Settings page. Always use constant-time comparison. Reject timestamps older than 5 minutes.