Adding a Store
A store implementation (approval, continuation, credential, audit, suspended invocation, checkpoint, lease, chat memory) implements an authoritative store SPI in its owning module and, where a shared TCK exists, proves conformance by enrolling in it. Enrolment is architecture-enforced: a store without its TCK runner fails the build. The TCK is the contract.
Pick The Owning Module First
| Store family | SPI lives in |
|---|---|
| Approval, approval continuation, resume credential | tramai-core (dev.tramai/core/approval) |
| Audit | tramai-security |
| Suspended invocation | tramai-engine |
| Workflow checkpoint, workflow lease | tramai-orchestration |
Implementations land in the persistence module that will own the backend (tramai-persistence-file, tramai-persistence-jdbc), or in the module that owns the new backend. In-memory defaults stay in the module that owns the SPI. Shared TCK fixtures live in tramai-testing.
The Extension Points
| Store | SPI file | Required methods |
|---|---|---|
ApprovalStore | tramai-core/.../approval/ApprovalStore.kt | create · get · transition(approvalId, expectedVersion, transition) · consumeApprovedOrReplay |
ApprovalContinuationStore | tramai-core/.../approval/ApprovalContinuationStore.kt | create(continuation, arguments) · get · claimForExecution · complete · expire · cancel · findStaleClaimed · forceCancelClaimed · sweepExpired |
ApprovalResumeCredentialStore | tramai-core/.../approval/gateway/ApprovalResumeCredentialStore.kt | create (duplicate → IllegalStateException) · get · delete; must encrypt the sealed resume token at rest |
AuditStore | tramai-security/.../audit/AuditStore.kt | appendNext · readStream · readStreamPage · latestEvent |
SuspendedInvocationStore | tramai-engine/.../SuspendedInvocationStore.kt | create(metadata, replayEnvelope) · get · revealReplayEnvelope · remove |
WorkflowCheckpointStore | tramai-orchestration/.../WorkflowPersistence.kt | load · save(checkpoint, expectedRevision) · delete · requireRecovery · clearRecovery, plus WorkflowStateCodec<S> |
WorkflowLeaseStore | tramai-orchestration/.../WorkflowLease.kt | currentLease · claim · renew · release, plus optional WorkflowLeaseCheckpointFence |
Two hard invariants cut across all of them:
claimForExecutionis the only path that exposes raw continuation arguments. They are released exactly once.revealReplayEnvelopeis the only path that exposes the replay envelope, and only after a claim.get()must not return it.
Mandatory Contract Tests
Shared TCKs live in tramai-testing/src/testFixtures/kotlin/dev/tramai/testing/persistence/:
| Store family | Shared TCK | Harness / enrolment |
|---|---|---|
ApprovalStore | approval/ApprovalStoreTck.kt | approval/ApprovalStoreTckHarness.kt (createStore(clock: MutableClock), closeStore) |
ApprovalContinuationStore | approval/continuation/ApprovalContinuationStoreTck.kt | approval/continuation/ApprovalContinuationStoreTckHarness.kt |
AuditStore | audit/AuditStoreTck.kt | direct createStore() |
SuspendedInvocationStore | engine/SuspendedInvocationStoreTck.kt | direct |
WorkflowCheckpointStore | checkpoint/WorkflowCheckpointStoreTck.kt | direct createStore() |
WorkflowLeaseStore | lease/WorkflowLeaseStoreTck.kt | direct createStore(clock: MutableMillisClock) |
WorkflowLeaseCheckpointFence | lease/WorkflowLeaseCheckpointFenceTck.kt | direct |
ChatMemoryStore | memory/ChatMemoryStoreTck.kt | memory/ChatMemoryStoreTckHarness.kt |
SovereignOpsAuditOutboxStore | outbox/SovereignOpsAuditOutboxStoreTck.kt | SovereignOpsAuditOutboxStoreTckEnrollmentArchitectureTest with in-memory/file/JDBC runners |
The approval TCKs are the deepest of the set. ApprovalStoreTck pins the transition matrix, consumption and exact-replay semantics, a model-based property check, a wrong-version matrix, and a race schedule. ApprovalContinuationStoreTck pins claim, exactly-once argument release, expiry, cancel, complete, recovery, sweep, races, and a continuation version ceiling of version ≤ 2 per continuation.
Stores without a shared TCK
Do not search for a TCK that does not exist. These are covered by module-level contract tests only:
| Store | Obligation |
|---|---|
ApprovalResumeCredentialStore | SPI conformance, encryption of the sealed resume token at rest, and module tests (for example JdbcApprovalResumeCredentialStoreTest.kt under tramai-spring-boot-starter-sovereign-persistence-jdbc/src/test/) |
SovereignOpsWorkerLeaseStore | SPI conformance plus module tests |
SovereignOpsApprovalMutationStore, SovereignOpsApprovalRequestMutationStore, ApprovedContinuationResumeQueueStatusStore, ApprovedContinuationResumeWorkerStatusStore | Starter-local stores with no engine SPI; module tests only |
Verify per store before assuming either situation.
Enrolment Is Architecture-Enforced
tramai-testing/src/test/kotlin/dev/tramai/testing/StoreEnrollmentScanner.kt backs one *EnrollmentArchitectureTest per store family:
ApprovalStoreTckEnrollmentArchitectureTestApprovalContinuationStoreTckEnrollmentArchitectureTestAuditStoreTckEnrollmentArchitectureTestSuspendedInvocationStoreTckEnrollmentArchitectureTestWorkflowCheckpointStoreTckEnrollmentArchitectureTestWorkflowLeaseStoreTckEnrollmentArchitectureTestWorkflowLeaseCheckpointFenceTckEnrollmentArchitectureTestChatMemoryStoreTckEnrollmentArchitectureTestSovereignOpsAuditOutboxStoreTckEnrollmentArchitectureTest
Each pins a runner allowlist and requires every concrete implementation to have a valid runner.
Files That Change
- The new store class and its
<Store>TckTest.ktrunner, in the owning module'ssrc/test/kotlin. - JDBC codecs, when the backend is JDBC:
JdbcReplayEnvelopeCodec.kt,JdbcAuditPayloadCodec.kt, the continuation-arguments codec insideJdbcApprovalContinuationStore.kt, andJdbcOpsAuditOutboxPayloadCodec.kt. - Spring wiring, when the store is auto-configurable: a
@ConditionalOnMissingBeanbean inSovereignJdbcPersistenceAutoConfiguration.ktorSovereignFilePersistenceAutoConfiguration.kt, plus the auto-configuration.importsregistration. config/quality/module-catalog.yml, if the store ships in a new module.
@ConditionalOnMissingBean is mandatory. A user-provided store bean must always win over a new default.
Replay-Envelope Security
SensitiveReplayEnvelope is opaque — toString returns [REDACTED] and only revealForResume() exposes the contents. A store must:
- verify
metadata.replayEnvelopeDigestatcreatetime, and - re-validate the replay invariants before persisting.
ReplayEnvelopeValidator and ReplayEnvelopeDigestHelper are shared: stores re-validate, they never re-define. Encryption at rest is the implementer's seam — key management, algorithm, and nonce are owned by the store.
Verification
./gradlew :tramai-testing:test # all *EnrollmentArchitectureTest gates
./gradlew :<module>:test --tests '*TckTest' # your store's TCK runner
./gradlew verifyPr
./gradlew verifyChangePolicy -PchangeClass=runtime-behaviour
JDBC TCK runners require Docker (Testcontainers), because they assert against real PostgreSQL durability behaviour.
What Not To Change In This Pull Request
- The TCKs. They are independent oracles; never weaken them to make a store pass.
- The SPI contracts.
ApprovalStore,ApprovalContinuationStore, and friends change only for apublic-api-classified change, never when adding an implementation. config/quality/0.6.0-baseline.json. Never edited in the same pull request.
Common Mistakes
| Symptom | Cause |
|---|---|
verifyPr fails in :tramai-testing:test | The runner extends nothing, or extends a copied TCK instead of the real one |
| Replay trust broken | Digest not verified at create, or the raw envelope exposed via get() |
| Durable state mutated by a rejected action | Optimistic concurrency ignored — rejected actions must not mutate durable state |
| User beans silently replaced | A new Spring store bean without @ConditionalOnMissingBean |
| Cancellation tests fail | CancellationException caught, wrapped, or persisted instead of rethrown |
| TCK assertions fail on version | A new state increments the continuation version past the version ≤ 2 ceiling |
