Idempotency key
An idempotency key is a caller-supplied token that lets a retried request apply its effect at most once. Caisson's credits ledger and Paddle webhook handler both claim the key via INSERT ON CONFLICT DO NOTHING RETURNING: a fresh key runs the grant or debit, a replay returns the same balance with zero new writes, so a resent webhook never double-grants.
In code
async function claim(tx: TenantExecutor, eventKey: string): Promise<boolean> {
const { rows } = await tx.query<{ event_key: string }>(
`INSERT INTO billing_processed_event (event_key, account_id)
VALUES ($1, current_setting($2, true))
ON CONFLICT (event_key) DO NOTHING
RETURNING event_key`,
[eventKey, TENANT_GUC],
);
return rows.length === 1;
}
/** OUTER layer: run `fn` for `sourceEventId` exactly once across re-deliveries. */
export async function processEvent(
tx: TenantExecutor,
sourceEventId: string,
fn: () => Promise<void>,
): Promise<ProcessResult> {
assertValidSourceEventId(sourceEventId, "processEvent");
const fresh = await claim(tx, sourceEventId);
if (!fresh) return { alreadyProcessed: true };
await fn();
return { alreadyProcessed: false };
}How it holds
One INSERT, atomic claim-or-skip
Both layers use the same primitive: INSERT ... ON CONFLICT DO NOTHING RETURNING. A fresh key inserts and returns a row; a duplicate key returns zero rows instead of raising, so the caller checks row count rather than catching a unique-violation exception.
Dual-layer coverage: DB writes and side-effects
The credits ledger's UNIQUE (source_event_id, event_type) index makes the money write itself idempotent. billing-orchestration's processEvent/withIdempotentSideEffect add an outer claim over the whole handler and a per-effect claim, so a detached post-commit push (Discord role grant, a confirmation email) also fires at most once across re-deliveries.
Commits atomically with the work it guards
Every claim runs inside the same withTenant transaction as the grant or debit it protects: if the guarded work throws, the claim rolls back too, so the next delivery retries cleanly instead of finding a stale claim with no matching effect.
Caller picks the key shape, mutually exclusive
credits.ts's idemColumns() requires exactly one of sourceEventId (a provider event/invoice/payment id) or idempotencyKey (a caller-chosen per-account key) (never both, never neither) so every ledger row always has one clear identity to dedupe on.