Skip to main content

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​

StoreRequired behavior
OAuthStateStore.consumeRead and remove in one operation
RateLimitStore.hitCounter and window update are atomic
SessionStore.createInsert and per-user cap are applied together
IdempotencyStore.beginKey, hash, state, and reservation are decided together
IdempotencyStore.complete/fail/renewReservation-owner compare-and-set
LockStore.release/renewOnly the token owner may change the lease
CacheStore.set/deleteByTagsEntry and tag indexes remain consistent
OutboxStore.claimThe 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 consume calls
  • 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