Idempotency
Exactly-once idempotency key ledger for retried operations, as a Bijection component
git clone https://github.com/bijectionhq/idempotency.git
cd idempotency
git checkout e2c5bb07c1b11ecd39c7156015b1934e8c789944Bijection repository access is required. Request access.
Description
Exactly-once idempotency key ledger for retried operations, as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/idempotency. Based on vllnt/convex-idempotency. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Exactly-once idempotency key ledger for retried operations, as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@vllnt/convex-idempotency helps your backend recognize repeated attempts at the same operation. A key identifies the work, an active claim coordinates attempts, and a stored result lets retries return the previous outcome.
begin returns a fresh claim, an inflight state, or a completed result. complete stores a typed outcome and reports lost or expired claims. Configure separate TTLs for active claims and completed results; scopes namespace keys by tenant or operation.
Your application authorizes the operation and must coordinate business writes with the claim lifecycle. This ledger does not make arbitrary external side effects exactly-once: use provider idempotency and recovery logic where needed. Once retention expires, a key may be used again.
Maintained by bntvllnt at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- Webhook handlers: recognize repeated provider delivery IDs before applying a change.
- Mobile apps: replay a saved response when a client retries after losing its connection.
- Forms and checkout flows: protect host-side mutations from duplicate submissions.
- Agent tools and integrations: reuse an operation key when a caller retries an uncertain request.
How it works
From the upstream package documentation.
begin returns a fresh claim, an inflight state, or a completed result. complete stores a typed outcome and reports lost or expired claims. Configure separate TTLs for active claims and completed results; scopes namespace keys by tenant or operation.
Your application authorizes the operation and must coordinate business writes with the claim lifecycle. This ledger does not make arbitrary external side effects exactly-once: use provider idempotency and recovery logic where needed. Once retention expires, a key may be used again.
Upstream package FAQ
What Convex function types can use @vllnt/convex-idempotency?
@vllnt/convex-idempotency is designed for use inside Convex mutations. The begin and complete methods are mutation-level calls that participate in the surrounding transaction, which is what makes the inflight claim atomic. The get method is a query. There is no React or client-side entry point; it is a pure backend infrastructure component.
How does the inflight lease prevent a crashed worker from blocking a key forever?
When idem.begin is called, the component records an inflight claim with an expiry derived from the server clock using a configurable inflight TTL that defaults to 60 seconds. If the worker crashes before calling idem.complete, the claim expires and a subsequent call to idem.begin for the same key mints a fresh claim rather than returning inflight. This self-healing behavior requires no manual intervention.
What happens if complete is called but the inflight row is missing or already expired?
idem.complete returns a discriminated union: either { recorded: true } on success, or { recorded: false, reason: 'missing' | 'expired' | 'already_done' } when the row could not be updated. This lets the host know the work executed but the idempotency record was not written, so it can log a warning or opt into upsertOnMissing to write the outcome anyway.
Can a caller supply their own timestamp to force a key to look expired or live?
No. @vllnt/convex-idempotency reads expiry from the Convex server clock internally. Callers cannot pass a now argument, so a client with a skewed or adversarial clock cannot manipulate whether a claim appears inflight, expired, or done. This is described in the component documentation as server-sourced expiry.
How are idempotency keys scoped and isolated from other tables in the Convex deployment?
@vllnt/convex-idempotency uses Convex component sandboxed tables, meaning the ledger tables are only accessible through the exported component functions and are not visible to the host app's raw database queries. Keys are global by default but can be namespaced with an explicit scope string to separate tenants or operation types without collisions.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Idempotency with Bijection.
Read: https://bijection.com/components/idempotency/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-idempotency, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-idempotency
Exactly-once idempotency key ledger for retried operations, as a Convex component.
const idem = new Idempotency(components.idempotency);
const claim = await idem.begin(ctx, requestId); // mints a claim, or short-circuits a replay
if (claim.state === "done") return claim.result; // replayed — skip the work
// ... run the work ...
await idem.complete(ctx, requestId, result); // record the outcome for next time
Record an idempotency key with a grace TTL; on a replay short-circuit and return the prior outcome
instead of re-running the work. Domain-neutral: payment intents, webhook deliveries, queue consumers,
double-submit guards — any operation that must run at most once per key.
Features
- Exactly-once per
(scope, key)—beginmints an inflight claim that rides the mutation transaction; a concurrent retry seesinflightwith aretryAfterMsbackoff hint. - Replay — once
completerecords an outcome, a laterbeginreturns{ state: "done", result }for a short-circuit. - Split TTLs — a short inflight lease (default 60s) so a crashed worker's claim self-heals, and a longer done grace (default 24h) after which a key may be re-minted.
- Lost-claim detection —
completereturns{ recorded: true } | { recorded: false, reason }so a host knows when its work finished but the row was gone. Opt intoupsertOnMissing. - Server-sourced time — expiry is read from the server clock; a caller can't supply
now, so an adversarial clock can't force a key to look live or expired. - TTL validation — non-positive or infinite TTLs throw
INVALID_TTLbefore any write. - Typed result —
Idempotency<TResult>types the stored outcome; aresultValidatornarrows it at the boundary. - Scopes — global by default, or namespace per tenant / operation type.
- Bounded purge + cron — a daily cron sweeps expired keys in batches and self-reschedules until clean.
Installation
pnpm add @vllnt/convex-idempotency
Peer dependency: convex@^1.41.0.
Usage
// convex/convex.config.ts
import { defineApp } from "convex/server";
import idempotency from "@vllnt/convex-idempotency/convex.config";
const app = defineApp();
app.use(idempotency);
export default app;
// convex/charge.ts — host owns auth; pass an opaque idempotency key in.
import { components } from "./_generated/api";
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { Idempotency } from "@vllnt/convex-idempotency";
const idem = new Idempotency<{ chargeId: string }>(components.idempotency, {
resultValidator: v.object({ chargeId: v.string() }).parse, // narrow at the boundary
});
export const charge = mutation({
args: { requestId: v.string(), amount: v.number() },
handler: async (ctx, { requestId, amount }) => {
const claim = await idem.begin(ctx, requestId);
if (claim.state === "done") return claim.result; // typed replay
if (claim.state === "inflight")
throw new Error(`retry in ${claim.retryAfterMs}ms`); // backoff hint
const result = { chargeId: await doCharge(amount) }; // state === "fresh"
const done = await idem.complete(ctx, requestId, result);
if (!done.recorded) console.warn("claim lost:", done.reason); // work ran, row gone
return result;
},
});
API Reference
| Method | Kind | Result |
|---|---|---|
begin(ctx, key, opts?) | mutation | { state: "fresh" } | { state: "inflight"; expiresAt; retryAfterMs } | { state: "done"; result? } |
complete(ctx, key, result?, opts?) | mutation | { recorded: true } | { recorded: false; reason: "missing" | "expired" | "already_done" } |
get(ctx, key, scope?) | query | { status, result?, expiresAt } | null |
purge(ctx, opts?) | mutation | number (keys removed in the first bounded pass) |
Full reference: docs/API.md.
React
Backend-only — no ./react entry. Pure infra dedup with no user-facing reactive surface.
Security
- Auth-agnostic — the host resolves identity and decides who may run an operation.
- Tables sandboxed — reached only through the exported functions.
- Server-sourced expiry — a skewed client clock can't hijack a replay or bypass dedup;
key/scope/resultstay opaque.
See docs/API.md.
Testing
pnpm test # single run
pnpm test:coverage # enforced 100% on covered files
Tests run against the real component runtime via convex-test (@edge-runtime/vm), not mocks.
Contributing
See CONTRIBUTING.md.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.