Skip to content

Commit 6e99e66

Browse files
authored
Merge pull request #715 from mikewheeleer/codex/issue-663
feat: add durable idempotency key storage
2 parents ae93407 + 44e238a commit 6e99e66

4 files changed

Lines changed: 41 additions & 2 deletions

File tree

docs/db/schema.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ The schema enforces eight invariants (I1–I8), verified by
9999
| `billingInvoices` | `billing_invoices` | Invoice records |
100100
| `sessions` | `sessions` | Auth sessions with sliding and absolute expiry |
101101
| `backfillProgress` | `backfill_progress` | Backfill job progress tracking |
102+
| `idempotencyKeys` | `idempotency_keys` | Durable 24-hour response replay records |
103+
104+
Idempotency records are keyed by `(route, key)` and include the request body
105+
fingerprint, response status, response body, and expiry timestamp. The unique
106+
primary key gives billing and diagnostics handlers a shared database-level
107+
claim point; expired rows can be removed by a scheduled cleanup query using the
108+
`expires_at` index.
102109

103110
## Security Boundary (Sensitive Fields)
104111

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE IF NOT EXISTS "idempotency_keys" (
2+
"route" text NOT NULL,
3+
"key" text NOT NULL,
4+
"body_fingerprint" text NOT NULL,
5+
"status_code" integer NOT NULL,
6+
"response_body" jsonb NOT NULL DEFAULT '{}'::jsonb,
7+
"expires_at" timestamp NOT NULL,
8+
CONSTRAINT "idempotency_keys_pkey" PRIMARY KEY ("route", "key")
9+
);
10+
--> statement-breakpoint
11+
CREATE INDEX IF NOT EXISTS "idempotency_keys_expires_at_idx"
12+
ON "idempotency_keys" USING btree ("expires_at");

src/db/schema-consistency.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ describe("schema contract invariants", () => {
139139
expect(exportedTableNames.sort()).toEqual(schemaTableNames.sort());
140140
});
141141

142-
it("has exactly 11 tables", () => {
143-
expect(schema.SCHEMA_TABLES).toHaveLength(11);
142+
it("has exactly 12 tables", () => {
143+
expect(schema.SCHEMA_TABLES).toHaveLength(12);
144144
});
145145

146146
it("every SCHEMA_TABLES entry has a defined table reference", () => {

src/db/schema.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
boolean,
77
timestamp,
88
index,
9+
primaryKey,
10+
jsonb,
911
numeric,
1012
check,
1113
type PgTableWithColumns,
@@ -661,6 +663,23 @@ export const backfillProgress = pgTable(
661663
}),
662664
);
663665

666+
/** Shared durable response cache used by idempotent mutating routes. */
667+
export const idempotencyKeys = pgTable(
668+
"idempotency_keys",
669+
{
670+
route: text("route").notNull(),
671+
key: text("key").notNull(),
672+
bodyFingerprint: text("body_fingerprint").notNull(),
673+
statusCode: integer("status_code").notNull(),
674+
responseBody: jsonb("response_body").notNull().default({}),
675+
expiresAt: timestamp("expires_at").notNull(),
676+
},
677+
(table) => ({
678+
primaryKey: primaryKey({ columns: [table.route, table.key] }),
679+
expiresAtIdx: index("idempotency_keys_expires_at_idx").on(table.expiresAt),
680+
}),
681+
);
682+
664683
// ---------------------------------------------------------------------------
665684
// Compatibility Version
666685
// ---------------------------------------------------------------------------
@@ -796,4 +815,5 @@ export const SCHEMA_TABLES: Array<{ name: string; table: PgTableWithColumns<any>
796815
{ name: "billingInvoices", table: billingInvoices },
797816
{ name: "sessions", table: sessions },
798817
{ name: "backfillProgress", table: backfillProgress },
818+
{ name: "idempotencyKeys", table: idempotencyKeys },
799819
];

0 commit comments

Comments
 (0)