5.5 KiB
@suaveplan/api-2pc — two-phase commit coordinator for atomic cross-plugin writes
Package: @suaveplan/api-2pc
Status: PROPOSED
Type: new-package
Stage: 6 (Distributed transactions + Better-Auth) — see umbrella openspec/changes/api-plugin-foundation/
Why
Some workflows genuinely require atomic cross-DB writes — no partial visibility is acceptable (classic finance/inventory). Per design doc §9.2 the foundation ships a 2PC coordinator on top of engines supporting PREPARE TRANSACTION. Postgres is the only mainstream engine that supports it; SQLite and SurrealDB cannot participate as 2PC writers and the coordinator must refuse them at install time. Without this package, plugins needing strict atomicity have no first-class option and would have to roll bespoke coordinator code that nobody can audit.
This package is also the second half of Stage-6 acceptance in the umbrella: "api-2pc commits + aborts an integration scenario against real Postgres."
What changes
Net-new package depending on @suaveplan/api-types, @suaveplan/api-host (consumes the metadata DB), @suaveplan/api-manifest, @suaveplan/db (for the db.preparable capability check + PREPARE TRANSACTION exec), @suaveplan/error, @suaveplan/logger, @suaveplan/opentelemetry, and zod.
- ADDED:
createTwoPcCoordinator(deps)— factory binding the coordinator to a host's metadata DB, capability registry, OTel meter, and clock. - ADDED:
run2pc(opts, ctx)— primary entry point.opts: { participants: Array<{ plugin, action, input }>, timeoutMs?: number }. DefaulttimeoutMsis 5000ms, hard ceiling 30000ms. Exceeding the ceiling is refused at call time. - ADDED:
TwoPcParticipantRegistry— registers participating plugins; refuses registration if the plugin's bound DB engine does not supportdb.preparable(Surreal / SQLite plugins MAY still register as read-only voters and contribute to the vote but MUST NOT participate in write phases). - ADDED:
TwoPcCoordinatorState—"voting" | "committing" | "aborting" | "committed" | "aborted" | "in_doubt". - ADDED:
TwoPcVote—"commit" | "abort". - ADDED: Boot-time orphan resolution — on coordinator start, query every participant's
pg_prepared_xactsfor prepared transactions tagged with the host's instance ID; commit those whose ledger row says "all-voted-commit", roll back the rest. - ADDED:
outstandingPreparedCap— default cap of 256 outstanding prepared transactions per coordinator (design doc §14 left this as an implementation choice; this is the spec'd default and is operator-tunable via host config). Exceeding the cap rejects newrun2pccalls withTWOPC_OUTSTANDING_LIMIT_EXCEEDED. - ADDED:
run2pcParticipantConformance(participantFactory)— re-export of the conformance suite fromapi-types/twopc. - ADDED:
TWOPC_*error codes:TWOPC_TIMEOUT,TWOPC_VOTE_ABORT,TWOPC_PREPARE_FAILED,TWOPC_COMMIT_FAILED,TWOPC_ROLLBACK_FAILED,TWOPC_IN_DOUBT,TWOPC_PARTICIPANT_NOT_PREPARABLE,TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE,TWOPC_OUTSTANDING_LIMIT_EXCEEDED,TWOPC_TIMEOUT_CEILING_EXCEEDED. - ADDED: OTel instruments:
2pc.transactionspan (per transaction lifetime),2pc.prepare/2pc.commit/2pc.rollbackspans per participant phase,2pc.transactions_totalcounter{ outcome },2pc.prepare_duration_mshistogram,2pc.commit_duration_mshistogram,2pc.outstanding_preparedobservable gauge.
Out of scope
- The
suaveplan_2pc_transactionstable schema — ships withapi-host's migrations. This package only reads/writes rows. - Engines other than Postgres — design doc §9.2 explicitly scopes the writer surface to Postgres. SQLite and SurrealDB participate only as read-only voters; this package SHALL refuse them as writers at install time and the spec pins this.
- Cross-region / multi-host coordinator coordination — umbrella's out-of-scope list excludes multi-region.
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
run2pc API + phase-1 prepare / phase-2 commit |
§9.2 (paragraph 2 + code block) |
Default timeoutMs 5000 / hard ceiling 30000 |
§9.2 (operational warning paragraph) |
Coordinator state in suaveplan_2pc_transactions survives restart |
§9.2 (paragraph 3) |
Orphan resolution via pg_prepared_xacts on boot |
§9.2 (paragraph 3) |
| Postgres-only writer; Surreal/SQLite as read-only voters only | §9.2 (paragraph 1) |
db.preparable capability requirement |
§9.2 (paragraph 4) |
2pc.prepare_duration_ms + 2pc.commit_duration_ms instruments |
§9.2 (operational warning paragraph) |
Verification approach
src/coordinator/coordinator.test.ts— happy-path commit, vote-abort path, timeout-then-rollback path, in-doubt path.src/coordinator/coordinator.spec-conformance.test.ts— runsrun2pcParticipantConformanceagainst a reference Postgres participant.src/registry/participant-registry.test.ts— refusal of non-preparable engines as writers; acceptance as read-only voters.src/recovery/recovery.test.ts(integration, 30000ms) — kills coordinator mid-prepare, restarts, asserts orphan resolution commits or rolls back based on ledger state.src/limits/outstanding-cap.test.ts— exceeds the 256-prepared cap, assertsTWOPC_OUTSTANDING_LIMIT_EXCEEDED.tests/integration/postgres-2pc.test.ts(60000ms) — real@suaveplan/testing-integration-postgres: full commit + abort + restart-recovery scenarios end-to-end.
Migration impact
None — additive only.