Store contracts
GuildGate does not bind the application to an ORM. The application supplies the ten GuildGateStores contracts directly or through official adapters.
Contract version
adapterContractVersion is 1.1. Action and realtime contracts remain 1.0. A custom store should record the adapter contract it implements and run runStoreContract() in CI.
import { runStoreContract } from "@kavtuai/guildgate/contracts";
const report = await runStoreContract(() => createCustomStores());
if (!report.passed) process.exitCode = 1;
Operations that require atomicity
| Store | Required behavior |
|---|---|
OAuthStateStore.consume | Read and remove in one operation |
RateLimitStore.hit | Counter and window update are atomic |
SessionStore.create | Insert and per-user cap are applied together |
IdempotencyStore.begin | Key, hash, state, and reservation are decided together |
IdempotencyStore.complete/fail/renew | Reservation-owner compare-and-set |
LockStore.release/renew | Only the token owner may change the lease |
CacheStore.set/deleteByTags | Entry and tag indexes remain consistent |
OutboxStore.claim | The same record is not leased to two workers simultaneously |
Idempotency owner token
reservationId owns an inflight record. A SQL update predicate should check reservation and state as well as the key:
UPDATE guildgate_idempotency
SET state = 'completed', response = $1, expires_at_ms = $2
WHERE key = $3
AND reservation_id = $4
AND state = 'inflight';
Zero affected rows means ownership was lost. Do not delete unconditionally by key.
Session cap
A shared store should insert the new session and evict the oldest excess sessions inside one serialized operation. A single-process memory store can do this directly; SQL can use a transaction or advisory lock, while Redis can use Lua or an equivalent atomic command.
Audit cursor
AuditStore.listPage() accepts an opaque cursor and returns { items, nextCursor }. Stable ordering is:
createdAt DESC, id DESC
Do not expose the cursor internals or treat it as a client-editable offset. A malformed cursor should produce an input error.
Outbox claim
claim(limit, workerId, leaseMs) leases records in a bounded batch. When a worker crashes, the lease must expire so another worker can claim the record. Sanitize and bound publisher errors before storing them in markFailed.
Error contract
Store errors must not disclose secrets or raw driver objects. Separate retryable connectivity failures from invariant violations. Preserve the cause in server logs so the action kernel can map it to a stable GuildGate error code.
Test areas
- competing
consumecalls - different body hashes for the same idempotency key
- reservation loss
- lease renew and release ownership
- concurrent logins under the session cap
- cache retagging and stale-tag cleanup
- complete traversal with audit cursors
- outbox claim lease expiry