Docs
retrysafe is a CLI on npm. Run it with npx, or add it to your project; it needs no account, and and it never sends your code anywhere: the playground is the only part that runs on a server.
Safe patterns for handlers that run more than once →
Choosing a key, claiming in a transaction, leases for external calls, status codes, ack timing.
Install
Node 20 or newer. npx runs the latest version without installing; add it as a dev dependency to pin a version for CI. retrysafe on npm
$ npx retrysafe// package.json
"scripts": { "retrysafe": "retrysafe --ci" }Run it
npx retrysafe # analyze the current project npx retrysafe --handlers # list discovered webhook and queue handlers npx retrysafe --effect-map # effect map for every handler npx retrysafe explain <id> # failing sequence, why, and the fix npx retrysafe rules # list rules
Live analysis while you edit
Run this in your project, open the link it prints, and the Live page updates every time you save, in any editor. Analysis runs on your machine; the page connects to it on 127.0.0.1 with a one-time token, and clicking a finding opens it in VS Code, Cursor, or Windsurf.
npx retrysafe live --open npx retrysafe live --port 5000 # use another local port
Fix findings
retrysafe fix prints a diff; --write applies it. It adds idempotency keys and job ids derived from the event or job id, answers permanent failures with 400, and wraps handlers with a guard from @retrysafe/guard.
npx retrysafe fix npx retrysafe fix --guard "@/lib/guard#guard" --write
Confirm a finding dynamically
verify loads the handler with payment, database, email, and queue libraries stubbed, then delivers the same event twice: in a row, concurrently, and with a crash before each effect.
npx retrysafe verify "POST /api/webhooks/stripe"
Configure
// retrysafe.config.ts (npx retrysafe init)
import { defineConfig } from "retrysafe";
export default defineConfig({
handlers: { add: ["src/jobs/send-invoice.ts#handler"], ignore: ["POST /api/health"] },
guards: [{ callee: "withIdempotency", keyArg: 0, atomic: true }],
effects: {
extend: [{ match: { module: "@/lib/billing", callee: "chargeCustomer" }, kind: "payment", idempotent: false }],
},
providers: { paystack: { retryWindowHours: 72 } },
rules: { "order-dependence-assumed": "off" },
});Adopting it in an existing codebase: record today's findings with --update-baseline so only new ones fail CI, and suppress single lines with // retrysafe-ignore <rule> -- <reason>.
CI
# .github/workflows/retrysafe.yml
on: [pull_request]
jobs:
retrysafe:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- uses: sammy440/retrysafe/packages/action@main
with: { fail-on: error, changed-only: "true" }Findings are annotated on the changed lines. Also available: --format sarif for code scanning, --format markdown, --format json, and a pre-commit hook (--staged).
Runtime guards
The runtime library is not on npm yet; the API below is what it will ship with.
npm install @retrysafe/guard
import { eventKey, prismaGuard } from "@retrysafe/guard";
const guard = prismaGuard(prisma);
await guard.run(eventKey.stripe(event), async (tx) => {
await tx.order.create({ data: { ... } }); // commits with the claim
});postgresGuard and prismaGuard claim the event in the same transaction as your writes; redisGuard uses a lease for external calls. retrysafe recognizes all of them.