Skip to content

Alpha · on npm, Swift Package Manager, and Maven Central

Offline data and media, inside your Supabase

AlphaSupabase sync Alpha for

Synchronizes rows and attachment references. Buckets select; Postgres grants and RLS authorize.

$kizunasync
Star on GitHub

Kizuna at a glance

CapabilityKizuna
LicenseEngine, drivers, CLI & panel: Apache-2.0; server SQL pack: PolyForm-Shield
Distributionnpm kizunasync with its five @kizunasync/<platform> binary packages, Swift package kizunasync/kizunasync-swift, Maven com.kizunasync:kizunasync, published by the release workflows on a tag matching the workspace version (X.Y.Z with an optional prerelease identifier)
Kizuna-operated data planeNone; clients call SQL and Storage in your Supabase project
Data + media syncRows plus attachment references; standard upload through 6 MiB, then TUS
Conflict handlingColumn LWW by server arrival by default; optional clamped-HLC mode
Query APIDocumented supabase-js-like subset over local SQLite; unsupported operations throw
DeletesRetained tombstones plus CHECKPOINT_EXPIRED rehydration beyond the horizon
StatusAlpha; published on npm, Swift Package Manager, and Maven Central

How it works

Alpha

Three pieces: source-built CLI, sync config, and local client.

01 · Provision

Init

Preview the source-built CLI before it writes.

A connected kizunasync init can inspect policies and propose simple owner buckets. Dry-run changes nothing; an applied run writes the pack and project config. kizunasync deprovision removes understood ledgered objects, not every base-pack object.

  • no Kizuna account: the target remains your Supabase project
  • declared equality buckets select rows; RLS remains the authorization layer
  • npx kizunasync, pnpm dlx kizunasync, yarn dlx kizunasync, or bunx kizunasync in your Supabase app project
Kizuna Sync » terminalAlphaillustration
$ npx kizunasync init --dry-run▍

02 · Configure

The everyday API

A scoped supabase-js-like local API.

A documented supabase-js-like subset reads local SQLite while offline. Unsupported constructs throw LOCAL_UNSUPPORTED according to the compatibility matrix; there is no silent network fallback.

  • createKizunaSync(driver, remote, config): the network client is an explicit argument, never hidden
  • kizunasync.from(table).select() / .insert() / .update() / .delete(), plus kizunasync.on(handler) for engine events
  • a TanStack DB collection adapter is on the roadmap
import { useMutation, useQuery } from 'kizunasync/react'

export function Todos() {
  const { data: todos, isLoading } = useQuery((kizunasync) =>
    kizunasync.from('todos').select().eq('done', false).order('created_at'),
  )
  const { mutate } = useMutation()

  const addTodo = (title: string) =>
    mutate((kizunasync) =>
      kizunasync.from('todos').insert({
        id: crypto.randomUUID(), title, done: false,
      }),
    )

  const toggleTodo = (id: string, done: boolean) =>
    mutate((kizunasync) => kizunasync.from('todos').update({ done }).eq('id', id))

  if (isLoading) {
    return <p>Loading…</p>
  }

  return <TodoList todos={todos} onAdd={addTodo} onToggle={toggleTodo} />
}

03 · Wire client

Quickstart

One file wires it into your framework.

Each tab trims a working guide composition: a platform driver, your config and Supabase client, plus attachment ports where that example configures media.

// src/kizunasync.ts
import { byOwner, defineConfig } from 'kizunasync'
import { createSupabaseKizunaSync } from 'kizunasync/supabase'
import { createWebWorkerDriver } from 'kizunasync/web'
import { supabase } from './supabase-client'

const config = defineConfig({
  tables: { todos: { sync: 'read-write', bucket: byOwner('user_id') } },
})

export const kizunasync = createSupabaseKizunaSync({
  supabase,
  driver: createWebWorkerDriver('todos.db'),
  config,
})

// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { KizunaSyncProvider } from 'kizunasync/react'
import { TodoList } from './components/todo-list'
import { kizunasync } from './kizunasync'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <KizunaSyncProvider client={kizunasync}>
      <TodoList />
    </KizunaSyncProvider>
  </StrictMode>,
)

The engine

Implemented surfaces, with their limits named in place.

The current Alpha covers local writes, pull and push, schema gates, retained deletes, attachments, framework bindings, provisioning, and inspection. It does not turn unit coverage into crash, scale, device, or uptime promises.

  1. Transactional offline writes

    The local row and its outbox entry commit in one SQLite transaction. Durability across abrupt termination still depends on the driver, journal mode, filesystem, and platform; the repository has no complete kill-test matrix.

    Mutation IDs make a committed remote effect replay-safe. Only explicitly permanent failures count toward the five-attempt dead-letter budget; retryable failures remain queued.

    Read the technical details for Transactional offline writes

0

extra Kizuna data-plane services

5

authenticated public RPCs

5

in-repository example apps

50

protocol corpus cases

Why zero data plane

Kizuna places no Kizuna-operated service between a client and its Supabase project. The trade-off is explicit: you keep the SQL, credentials, quotas, upgrades, retention, and operational evidence in your own environment. This architecture avoids adding a Kizuna-operated hop; it does not promise zero downtime.

"In local-first software, the availability of another computer should never prevent you from working."Kleppmann, Wiggins, van Hardenberg & McGranaghan, "Local-first software" (2019)

Kizuna's protocol is scoped on purpose: it draws on Bayou's tentative writes and server arbitration (1995), session guarantees (1994), and causal+ consistency to checkpoints (COPS, 2011). The default is column-LWW by accepted server arrival; an optional mode uses clamped HLC ordering. Both remain subject to RLS, grants, constraints, preconditions, tombstone retention, and successful checkpoints.

Sources: Local-first software · Bayou (SOSP 1995) · COPS (SOSP 2011)

Radical honesty

When NOT to use Kizuna

These are the current wrong-tool boundaries, with the relevant alternative class named.

Trusting a sync engine requires being able to read it. Kizuna's answer is readable SQL provisioned into your own project, a public wire protocol, and a conformance corpus, so the inspector and the transcripts let you verify every claim on this page yourself.

Concurrent collaborative text or canvas co-editing

Use a CRDT engine. Yjs, Automerge, Loro. Different problem class.

Strict cross-user invariants: ledgers, bookings, inventory holds

Keep those flows online-only and transactional. Offline writes are the wrong model regardless of engine.

Arbitrary RLS-predicate partial sync at enterprise scale

Evaluate a dedicated server-side sync service. Kizuna supports declared equality buckets, with RLS as authorization.

Multi-terminal mesh sync while offline (full POS, kiosk fleets)

Use a system designed and tested for peer or edge mesh replication. Kizuna synchronizes through one authoritative Supabase project.

FAQ

›

What happens when a device stays offline for a very long time?

Its queued writes wait for as long as it takes, and the device catches up when it reconnects, rebuilding its local copy if it fell behind the server's retention. When the network returns, the app client pushes the outbox first, in the order you made the writes, then pulls from its saved cursor. A device whose cursor predates the delete history the server still keeps (tombstone_ttl_days, 30 days by default) receives CHECKPOINT_EXPIRED, pulls its data again from the start, replaces its local copy, and replays the writes still pending on top. A queued update to a row someone deleted meanwhile is rejected instead of bringing the row back. The rejection reads DELETE_WINS while the delete history is kept and the user had already pulled a live row from that row's bucket, and RLS_DENIED otherwise.

On tables with client registration turned on, a registration silent for client_ttl_days, 90 by default, is pruned, and the device registers again on its next pull or push. An app build below a table's min_schema_version receives RESET_REQUIRED and stops syncing until an update ships and the app calls reset(), which discards the writes still queued. Offline writes explains the queue, and Fencing and horizons explains the cursor.

›

Does Kizuna work with the supabase-js client, or instead of it?

It works with it. createSupabaseKizunaSync takes your app's supabase-js client and uses its session for every pull and push, its Realtime connection for the wake-up hint, and its Storage for attachments. Your screens read and write synced tables through kizunasync.from(...), which answers from the local database, while your app keeps supabase-js for sign-in, Edge Functions, server RPCs, and the tables it does not sync (rpc and schema on the app client throw LOCAL_UNSUPPORTED rather than reach the network). Swift and Kotlin apps keep supabase-swift and supabase-kt for sign-in and hand the session token to the app client.

A write that goes to a synced table through supabase-js or SQL still reaches the devices that pull that row, because the triggers Kizuna installs record it. In the default arrival mode, though, a device that still has a queued write to the same column replaces that value when it pushes. A table you write only through supabase-js can sync pull-only instead, which makes its local copy read-only and leaves every write on your supabase-js path. How Kizuna works shows where each piece sits.

›

How does Kizuna sync large files?

Through an attachment queue that uploads each file after its row reaches the server, resumably and in 6 MiB chunks once the file is larger than 6 MiB. The row holds a short reference string, and fromFile copies the file into a local sandbox, computes its SHA-256 digest, and queues the upload, which runs only once the outbox has pushed the row. A file of at most 6 MiB takes one standard Storage upload, and a larger one takes a resumable TUS upload whose session URL is saved before the first chunk, so a later attempt continues from the offset the server reports. Another device downloads the file when a view first asks for it and checks the bytes against the SHA-256 digest, and a transfer that keeps failing stops after five attempts by default until your app calls retry. Kizuna caps neither the size nor the type of a file, so set the Storage bucket's file_size_limit. The adapter also reads the whole file into memory to move it in either direction, which means resumable uploads do not make a very large file cheap in memory. Media attachments wires the queue end to end.

›

How is Kizuna different from PowerSync, Electric, Firestore, and the others?

Kizuna runs its sync logic inside your own Supabase project, while PowerSync, Electric, and Zero run a sync service beside your database and Firestore keeps your data in its own backend. Kizuna's SQL functions apply your Row Level Security to every pull and push and answer each write with a typed verdict, files sync with their rows through Supabase Storage, and the JavaScript, Swift, and Kotlin clients all run the same Rust engine. Comparison with alternatives covers each product and when to choose it, and the capability matrix puts them side by side.

›

How are conflicts handled?

Per column, in your Postgres database. In the default arrival mode, two writes that name different columns of a row both keep their values. When two accepted writes touch the same column, the one the server receives later holds it, and device clocks play no part. A table set to hlc mode compares each column's origin Hybrid Logical Clock instead, after the server clamps a device clock running ahead by more than hlc_max_skew_ms, five seconds by default. Row Level Security, constraints, preconditions, and delete history can still reject a single write, which comes back as a typed verdict, while the push policy (a maximum batch size or required atomic batches) refuses the whole request with an error. Kizuna's guarantees (causal+ consistency to checkpoints and the four session guarantees) hold for successful checkpoints, the buckets a device pulls, and devices that sync within retention. Conflict resolution walks through both modes.

›

What does Kizuna cost to run?

Kizuna adds no bill of its own, because no service that Kizuna operates or charges for sits between your devices and your Supabase project. The engine, drivers, bindings, CLI, inspector, and protocol sources are Apache-2.0, and the server SQL pack is PolyForm Shield 1.0.0. You still pay for and operate the Supabase, hosting, storage, egress, build, and support resources your application uses, and pulls, pushes, and attachment transfers are requests to your own project. Governance records the licensing commitments.

Read the full FAQ →