Wire-compatible databases
Several distributed databases speak the PostgreSQL wire protocol, which means the Postgres backend drives them with no new code:
| Database | Journal | Snapshot store | Durable state | Certified by |
|---|---|---|---|---|
| CockroachDB | ✓ | ✓ | ✓ | test:integration:cockroachdb |
| YugabyteDB (YSQL) | ✓ | ✓ | ✓ | test:integration:yugabytedb |
There is no CockroachJournal and no YugabyteJournal — you use
PostgresJournal and point it at the other server:
import { PostgresJournal, PostgresJournalOptions } from 'actor-ts';
// CockroachDBconst cockroachJournalOptions = PostgresJournalOptions.create() .withUrl('postgres://root@localhost:26257/defaultdb?sslmode=verify-full');const journal = new PostgresJournal(cockroachJournalOptions);YugabyteDB serves YSQL on 5433, not 5432:
const yugabyteJournalOptions = PostgresJournalOptions.create() .withUrl('postgres://yugabyte@localhost:5433/yugabyte');Everything else — registerPostgresPlugins, the plugin IDs, the options
families — is exactly as documented for Postgres.
Why this works, and why it is tested rather than assumed
Section titled “Why this works, and why it is tested rather than assumed”Wire compatibility gets you the protocol; it does not automatically get you identical semantics. Two things in particular could have broken and did not:
-
Error classification. The journal’s optimistic-concurrency backstop translates a lost write race into
JournalConcurrencyError. It matches on SQLSTATE, never on message text — which matters because YugabyteDB is known to reword some Postgres error messages (yugabyte/yugabyte-db#9294). A text-matching implementation would have failed silently there, in the worst possible way: a real conflict surfacing as a generic error.Matching the code was necessary but, it turned out, not sufficient. Both engines run at
SERIALIZABLEand abort the losing transaction before it reaches the primary key, so they report40001(serialization failure) where PostgreSQL reports23505(unique violation). The stores classify both, but they do not trust a contention abort on its own: an abort proves only that the engine picked a victim, not that anyone else wrote. So they re-read the head and report a conflict only if it actually moved — otherwise an ordinary lock-wait timeout against an unrelated transaction would be relabelled as someone else’s append and send the caller into a retry loop against a head that is never going to change. -
The SQL itself.
ON CONFLICT … DO UPDATE,GREATEST,CREATE INDEX IF NOT EXISTSandLIMITinside a subquery are all used by the Postgres stores and all accepted by both servers.
Both suites run the same contract as the Postgres suite — journal append,
ranged reads, the compaction high-water mark, tags, snapshot keepN pruning,
durable-state CAS, and racing appends — so a future upstream divergence turns a
CI job red instead of surfacing in production.
Caveats
Section titled “Caveats”Running the suites
Section titled “Running the suites”bun run test:integration:cockroachdbbun run test:integration:yugabytedbBoth need Docker. YugabyteDB initializes its system catalogs on first boot and takes noticeably longer to become ready than any other image in the matrix; the runner waits up to three minutes for it.
Where to next
Section titled “Where to next”- PostgreSQL — the backend that serves all of these, and the full options reference.
- Cassandra journal — if you want a distributed store with a native driver rather than the Postgres protocol.
- Persistence overview — the backend matrix.
