This guide describes the minimum contract for CAP storage adapters. Storage
packages should keep their package root framework-free and expose framework
wrappers only from explicit subpaths such as /nest.
Use @mikara89/cap-storage-knex, @mikara89/cap-storage-typeorm, and
@mikara89/cap-storage-prisma as v2.3 reference adapters for framework-free
SQL storage packages. Prisma demonstrates how a generated-client ORM can use a
small structural raw-SQL interface without requiring CAP schema models. Do not
extract a shared SQL core until multiple real adapters prove repeated
implementation details.
| Adapter | Adapter style | Transaction context |
|---|---|---|
| MikroORM | ORM-specific adapter | MikroORM EntityManager |
| Knex | SQL query-builder adapter | Knex.Transaction |
| TypeORM | ORM adapter | TypeORM EntityManager |
| Prisma | Raw-SQL Prisma Client adapter; no CAP models in the Prisma schema | Prisma.TransactionClient |
Drizzle, Sequelize, and Mongoose are future candidates. Raw SQL or SQL-core extraction remains deferred until duplication across these real adapters proves the need.
@mikara89/cap-* package naming rule for publishable packages.@mikara89/cap-storage-example/nest.npm run pack:dry-run before publishing.Implement PublishStoragePort.savePublish(event, ctx?) as the primary outbox
write API. Adapters that support transactions should read the ORM-specific
transaction from ctx.tx.
savePublishWithTx(event, tx) is deprecated compatibility only. Existing
adapters may keep it as a wrapper over savePublish(event, { tx }), but new
code should call savePublish(event, ctx?).
Publish storage must also implement:
claimUnpublished({ limit, lockedBy, lockUntil, now })markPublished(id, publishedAt?, { expectedLockedBy }?)markPublishFailed(id, error, { maxRetries, nextRetryAt, now,
expectedLockedBy? })renewPublishClaim({ id, expectedLockedBy, lockUntil, now })releaseExpiredClaims(now)initialize(options)findPublishById, listPublishUse definePublishStorageContract from @mikara89/cap-testing in the adapter
test suite. Pass conservative capability options until the adapter has tests
that prove stronger behavior.
Treat lockedBy as an opaque, per-claim-round ownership token. When
expectedLockedBy is supplied, publication and failure updates must be a
single atomic statement constrained by id, status = processing, and the
owner token, and must return false when no row matches. Never follow a fenced
miss with an unconditional update. Failure updates must increment retry state
in that same statement.
Lease renewal must atomically require the same owner and an unexpired current
lease (lockedUntil > now) before setting the new expiry. Expired-claim release
must likewise be a conditional update so it cannot overwrite a concurrent
renewal. Keep the older unconditional completion behavior when ownership is
omitted; this preserves source compatibility for direct callers and legacy
adapters. The optional renewal method allows older third-party adapters to keep
working, but they cannot advertise lease renewal.
Implement ReceivedStoragePort for inbox persistence:
trySaveReceived(event) returning { inserted, id, event }markProcessed(id, processedAt?)markReceivedFailed(id, error, { maxRetries, nextRetryAt, now })getRetryDue(limit, now?)initialize(options)findReceivedById, listReceivedInbox dedupe must use consumer group plus dedupeKey. The broker
messageId is traceability metadata unless a transport also uses it to build
the dedupe key.
Use defineReceivedStorageContract from @mikara89/cap-testing in the adapter
test suite. Keep unsupported concurrency guarantees explicit through the
contract options.
Implement CapabilityAwareStoragePort when the adapter can report behavior
without guessing. Report conservative capabilities:
transactionsskipLockedClaimingadvisoryLocksatomicInsertIgnorenestedTransactionsisolationLevelsclaimOwnershipFencingclaimLeaseRenewalCapability values are informational in the current release line. They should match documented behavior and the contract options used in tests.
Before a storage adapter is treated as first-party, verify:
definePublishStorageContractdefineReceivedStorageContract