# `@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 }`. Default `timeoutMs` is 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 support `db.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_xacts` for 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 new `run2pc` calls with `TWOPC_OUTSTANDING_LIMIT_EXCEEDED`. - ADDED: `run2pcParticipantConformance(participantFactory)` — re-export of the conformance suite from `api-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.transaction` span (per transaction lifetime), `2pc.prepare` / `2pc.commit` / `2pc.rollback` spans per participant phase, `2pc.transactions_total` counter `{ outcome }`, `2pc.prepare_duration_ms` histogram, `2pc.commit_duration_ms` histogram, `2pc.outstanding_prepared` observable gauge. ## Out of scope - The `suaveplan_2pc_transactions` table schema — ships with `api-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` — runs `run2pcParticipantConformance` against 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, asserts `TWOPC_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.