Docs Getting started
Project status
Kizuna is Alpha: the repository contains a working client engine, drivers, a SQL pack, a native CLI, native bindings, tests, and the five examples in Playground. Apps install one npm package, kizunasync, which holds the CLI, the SQL pack, every JavaScript entry point as a subpath, and the React Native module. npm also installs the one @kizunasync/<platform> package that matches the machine, which carries the CLI binary and the N-API engine. The release workflows publish the five platform packages and then kizunasync on a v* tag matching the workspace version. The workspaces behind the subpaths and @kizunasync/utilities, @kizunasync/ui, @kizunasync/protocol, and @kizunasync/supabase-pack stay private, and Rust crates carry publish = false.
The native CLI's user-facing VERSION constant and the SQL pack's _provisions.pack_version both read 0.2.6-alpha.3 from Cargo, matching every other workspace; kizunasync --version prints that value.
This page separates two states: "Implemented in the repository" describes a source-backed surface, and "Verified" applies only when the named automated or manual evidence exists. Code that exists and a check that proves the code behaves are different things, and no line below lets one stand in for another.
Surface matrix#
| Surface | Repository state | Distribution and evidence |
|---|---|---|
| Shared app client | Implemented: local query subset evaluated inside the Rust kernel on every runtime, optimistic writes, transactional SQLite-backed outbox, pull/push scheduler, rejection journal, reset, events, inspector snapshot, attachments | npm kizunasync; abrupt-termination evidence remains driver/platform-specific |
| Rust engine | Implemented across the store, query, protocol, engine, transfer, remote HTTP, N-API, UniFFI, and wasm crates, and it is the only engine any client runs | Crates are workspace-internal; workflows build native artifacts for the bindings above |
| Browser driver | Implemented as the Rust engine compiled to WebAssembly in a dedicated worker, over the OPFS synchronous-access-handle pool or the relaxed IndexedDB fallback, with one leading tab per database and the others proxied to it | npm kizunasync, subpath kizunasync/web |
| Expo driver | Implemented with expo-sqlite on device, the kizunasync/web driver on Expo web, plus file storage and connectivity | npm kizunasync, subpath kizunasync/expo. The Expo web target is wired to the kizunasync/web driver, and a Metro web export bundles the kizunasync/web worker as its own bundle. |
| React Native op-sqlite driver | Implemented behind the common driver API with a verification helper | Opt-in; no documented physical-device release evidence |
| React and Vue bindings | Implemented providers, hooks/composables, query invalidation, sync state, rejections, and attachments | npm kizunasync, subpaths kizunasync/react and kizunasync/vue |
| Supabase adapters | Implemented RPC remote, Realtime doorbell, standard Storage transfer, and resumable TUS transfer | npm kizunasync, subpath kizunasync/supabase |
| SQL pack | Implemented as one installable migration plus generated per-project table configuration, including the opt-in server-side conflict journal | Source-available under PolyForm Shield; installs through kizunasync init |
| CLI | Native Rust commands for init, sync, status, doctor, lint, upgrade, deprovision, jobs, mock seed, mock churn, and version, plus the global -v / --version flags; a guided wizard for init and sync, and Management API parity for both | Invoke as npx kizunasync, pnpm dlx kizunasync, yarn dlx kizunasync, or bunx kizunasync |
| Direct Swift and Kotlin | Implemented: typed KizunaSyncClient over UniFFI matching IKizunaSync's write builders, catalog error codes, and verdict-ring inspector (journal, events, transforms, opt-in attachmentRoot attachments, host KizunaSyncScheduler with health()/onHealth) plus iOS and Android example projects | Swift package kizunasync/kizunasync-swift and Maven com.kizunasync:kizunasync; Android AAR/APK uses Gradle wrapper 8.7 with AGP 8.6.1 |
| React Native Rust path | rn-uniffi loader and generated ubrn Turbo Module | npm kizunasync, which ships the React Native module and autolinks it; needs a native rebuild |
| Protocol and corpus | Schemas, decision registry, TypeScript oracle, golden transcripts, Rust runner, and six TLA+ property models | Public source; no standalone third-party TCK package |
| Examples | Expo, React, Vue, native iOS, and native Android | Source projects with individually documented runnable lanes; Android AAR/APK configure under wrapper 8.7 / AGP 8.6.1 when the SDK is present; JVM host tests unset the SDK variables |
| Sync inspector | Read-only Next.js inspector exists in apps/sync-inspector | Local development tool bound to 127.0.0.1; not deployed |
| Documentation website | Next.js renderer loads the public Markdown registry | Deploys to Vercel through deploy-website.yml when a v* tag on main runs the release pipeline |
Engine selection#
The app-facing JavaScript app client, createKizunaSync, chooses its engine on its first use, and only an engine that can open the same SQLite database as the driver, as How Kizuna works explains:
- A browser app runs Rust through WebAssembly. The
kizunasync/webdriver carries the engine itself: the worker owns the store, and the driver exposes no SQL methods of its own, so the transport is the only route to those rows. - Node and Bun run Rust through N-API when the addon loads. The driver names the store as
databasePath, ornullfor a private in-memory database. - React Native runs Rust through UniFFI when the Turbo Module is linked. The
kizunasync/expodrivers load it through theirloadNativeEngine, so thekizunasyncentry point never imports the module. The samedatabasePathrule applies. - Where none of the three resolves, the app client's first use fails with
ENGINE_UNAVAILABLE, which names the artifact to install plus every path the addon loader tried, and every later engine call fails with the same error. Building the client never throws it. There is no engine to fall back to, and no environment variable that changes the outcome.
The JavaScript app client exposes the attachment queue, which Media and attachments documents. Direct Swift/Kotlin hosts opt in with attachmentRoot on KizunaSyncClient.create (fromFile, resolveDownload, vacuum, watch). React Native UniFFI must omit attachment_root so the JavaScript host's attachment queue owns bytes.
Attachment status#
The JavaScript Supabase transfer chooses by byte size:
- A file of at most 6 MiB takes the standard Storage upload.
- A file above 6 MiB takes a resumable TUS upload in 6 MiB chunks.
- A new TUS session URL is persisted before the first chunk and reused to recover the server offset.
Each attachment carries an attempt budget, attachmentAttempts in defineConfig, default 5. The budget's sixth failure, and a Storage 404 on download, move the queued entry to a permanent terminal state rather than retrying it forever; retry, cancel, and remove are the app-facing controls over that lifecycle.
The repository includes TUS unit coverage and Rust interruption/resume coverage. A normal unit test is not evidence of a successful physical-device upload to a hosted Supabase project. Match the test to the claim states which suite backs which claim.
Protocol evidence#
The corpus manifest, which the Protocol reference describes, contains 50 cases:
- 49 cases execute. The JavaScript reference executor runs them against the oracle server, and three client lanes run them against the engine: the Rust conformance runner, the N-API transport lane in the
coreworkspace, and the browser lane in thewebworkspace. - 1 case is skipped;
wakeup/002-wakeup-payloadhas no transcript bytes because the channel payload remains an open protocol decision. - Six TLA+ models cover cursor monotonicity, no lost committed change, exactly-once effect, session guarantees, atomic checkpoint, and no resurrection.
The live SQL replay used by the database job pins 46 replayable transcript families:
- 45 families replay as
PASS-SEMANTIC. - 0 families replay as
DIVERGED. - 1 family is
UNSUPPORTED, because a synchronous single-connection replay cannot deterministically reproduce the held-transaction fencing race. - 0 families end in
ERROR.
The live-SQL count and the 50-case corpus count measure different things and must not be combined. One counts recorded protocol cases and the other counts transcript families replayed against a live Postgres, so a sum of the two would describe nothing.
SQL pack status#
packages/supabase-pack/pack.manifest.json separates:
0001_kizuna_init.sqlis the installable engine pack.0002_example.sqlis the local demo fixture.
The pack creates Kizuna-owned schema objects, itemized in What is installed. Registering an application table also installs Kizuna triggers on that table; it does not add Kizuna columns or replace the table's RLS policies. Buckets select rows but do not authorize them.
Turning on a table's conflict_journal column enables server-side recording of overwritten column values in kizunasync._conflict_journal, which Conflict history explains. It defaults to false. Pull may attach matching rows as optional conflicts, omitted when empty. The engine persists _kizunasync_overwrites and emits COLUMN_OVERWRITTEN. Authenticated callers have no SELECT; service_role can read the audit rows.
kizunasync._settings also carries retention knobs and job schedules. The three schedules are the tombstone reaper, the changelog compactor, and the client pruner, each a UTC crontab that kizunasync._schedule_jobs() writes into pg_cron. The knobs are client TTL, HLC skew, and project tombstone retention. The pack tries to enable pg_cron itself and installs cleanly either way. kizunasync jobs runs the three functions by hand over a service connection, whether or not pg_cron is present.
Current limitations#
- Distribution covers npm, the Swift package URL, Maven Central, and GitHub Releases; the Rust crates do not go to crates.io. Release contract names what each channel still needs.
- The local query API is a subset. Unsupported PostgREST operations throw
LOCAL_UNSUPPORTED; there is no transparent network fallback. Supported query operators lists what the subset answers. - Offline acceptance is optimistic. Row Level Security, constraints, preconditions, and push policies can reject a queued mutation after reconnect, and Offline writes shows how to surface that verdict.
- Swift/Kotlin
KizunaSyncClientmatchesIKizunaSync, and the Swift package and Maven artifacts publish from av*tag. Android configuration uses wrapper 8.7 with AGP 8.6.1. The file picker is the app's (fromFiletakes a path). There are no native React-style hooks; Swift and Kotlin shows the pattern that replaces them. - Physical-device release evidence is incomplete. CI defines host, iOS simulator, Expo simulator, and Android assembly lanes, and it records no physical-device matrix. AAR and APK jobs produce CI artifacts.
- The wakeup byte format is not final, because the wakeup payload decision remains blocked. Polling and explicit pull preserve correctness meanwhile.
- Kizuna does not package third-party driver tooling. The corpus and headless harness exist, and no standalone TCK command ships.
- The Android connectivity path monitor and foreground source run under Robolectric on the
:androidlane, which exercisesKizunaSyncConnectivityPathMonitorandKizunaSyncProcessForegroundSourceagainst Android's real APIs on the JVM. Only device behavior, real radios and the real process lifecycle, is unverified. - The CLI refuses
--require-atomic. Ordinary writes are non-atomic, so enabling the flag would dead-letter them.kizunasync doctorfails when_settings.require_atomicis already on.kizunasync sync --no-require-atomicturns it off. LocalStorewraps onerusqlite::Connection, which isSendand notSync: one store serves one thread at a time. Every binding gives it a dedicated thread of its own instead of sharing it: N-API runs each command as a task on aLocalSetover one thread, and UniFFI runs a per-handle actor thread that receives closures and returns their replies. Engine selection names which client uses which lane.- The protocol corpus leaves five cases uncovered. They are pagination under concurrency, transforms on an absent or RLS-hidden row, and
transformson insert or delete. The other two are integers at or above 2^31 and theKZPpush-policy error codes. The pack testpull-fencing-continuation.test.tscovers the continuation-page fencing case the corpus omits. Each gap is a hole in test coverage, not a second implementation of the engine. - A synced table needs a primary key whose columns are
uuid,text,character varying,smallint,integer, orbigint, and a table with an attachment column needs a singleuuidkey namedid. On a read-write table keyed by an identity orserialcolumn, an insert made offline has to carry the key value, because the device cannot generate one. Row keys recommends uuid keys for the tables devices create rows in.
Sequenced future work lives on the Roadmap, not on this page.
Next steps#
- Roadmap: sequenced future work, with each item's status.
- Swift and Kotlin: the native client setup this page scopes.
- CI and CD: the workflows behind the evidence claims above.
- Status taxonomy: what implemented, verified, and published each mean.
Found a problem? Edit this page on GitHub