A Python backend can replace a TypeScript backend behind an unmodified Zero client, for one pinned
@rocicorp/zero version at a time, and later releases fast-follow each upstream release. The first pin is
1.10.0.
In scope, mirroring these rocicorp/mono packages (about 10-15k TS lines):
zero-server:handleQueryRequest/handleTransformRequest,handleMutateRequest/PushProcessor, the transaction provider and its two hooks (updateClientMutationID,writeMutationResult).zero-protocol: the wire types, including the query AST (src/ast.ts).z2s: ZQL compiled to Postgres SQL, which is what server-side ZQL (tx.run(query)) runs on.zero-schema/zero-types: the schema, client-to-server name mapping.- The query builder from
zql(builder,query). Its IVM engine and planner are client-side and out. defineQueries/defineMutatorsequivalents.
Out of scope: zero-client, replicache, zql's IVM, zero-cache, zqlite, UI bindings. A Python backend serves the same TypeScript client a TS backend would.
The schema stays authored in TypeScript (schema.ts), because the client needs it. Python loads it as JSON
emitted from the TS schema. A Python schema DSL would be a second source of truth.
The published npm package at the pinned version is the definition of correct. The public rocicorp/mono repo
ships no .test.ts for zql, z2s, zero-server, zero-protocol or zero-cache, so there are no upstream tests to
port. Three tiers of differential tests, each comparing Python against the real package:
- Wire. A small bun runner calls the real handlers. Hypothesis generates query args and mutation
sequences, including gaps, duplicates, refusals and the
_zero_cleanupResultsmutation. Diff the responses and the rows written to the upstream schema (zero_0.clients,zero_0.mutations). - SQL (z2s). The same ZQL through TS and Python against the same Postgres. Compare result rows, never SQL text.
- End to end. Docker compose: Postgres, the
rocicorp/zero:<pin>image, a TS reference app server and the Python one, one scripted client. Diff the rows the client ends up with.
Python's own tests hold 100% branch coverage as a hygiene floor, on top of the differential tiers. The
compatibility proof (tests/compat.py, described in compat.md) goes further: the differential
tests alone must reach every line and branch of the cited upstream modules inside the npm package, and of
zero_server.
Each phase is a PR. Its completion criterion is what makes it done.
- Scaffold.
ZERO_VERSION(1.10.0), CI running ruff, ty, pytest with coverage. Done when CI is green on an empty test suite. - Seed. The protocol models, the AST with every operator (
<,>,LIKE,NOT LIKE,ILIKE,NOT ILIKEincluded), and the push and query schemas. Done when every module inzero-protocol/src/ast.tsand the push/query schemas has a Python counterpart. - Mutate endpoint, one transaction.
handle_mutate_requestruns the mutator inside the same transaction as the lastMutationID bump, as upstream does. First transaction provider: SQLAlchemy async. Done when the wire tier passes for every branch ofprocess-mutations.ts: ok, app error, already processed, out of order, unsupported push version, cleanup bulk and single. - Query endpoint.
handle_query_request, adefine_queriesregistry, schema loading with name mapping. Done when the wire tier passes for generated queries over a test schema covering every AST node type. - End-to-end tier against 1.10.0. Done when the e2e diff is empty for the test app. This is the spike's finish line: the point to make the repo public and contact Rocicorp.
- z2s and
tx.run. Server-side ZQL against Postgres. Done when the SQL tier passes over generated queries covering every operator,related,exists/not exists, ordering,limitandstartbounds. - Builder ergonomics and the remaining DB adapters (asyncpg, psycopg 3).
- Release automation. Watch npm for new
@rocicorp/zeroversions, diff the in-scope packages between tags, port, run all three tiers, release with the new pin.
rocicorp/mono, packages/zero-server/src/process-mutations.ts,
packages/zero-server/src/queries/process-queries.ts, packages/zero-protocol/src/ast.ts, packages/z2s.
Read it at the tag matching ZERO_VERSION.
- Name
zero-serveron PyPI and in code (importzero_server, CLIzero-server). PyPI refuseszero-pythonas too similar tozeropython, andzero-pyas too similar to the abandonedzeropy. The repo stayszero-python, because the PyPI trusted publisher is bound to that name. - Repo private until phase 5 passes; then public, then contact Rocicorp about a community listing and early notice of protocol changes.
- PyPI publishing by trusted publishing from
.github/workflows/release.yml, environmentpypi. Av*tag on a commit already onmainpublishes the version inpyproject.toml.