Skip to content

Portable Seeds

Synchro supports generating a client-compatible SQLite seed directly from PostgreSQL.

The generator lives at:

  • api/go/cmd/synchro-seed

The portable seed generator is trusted tooling. It connects directly to PostgreSQL and builds a SQLite database that native clients can open immediately.

Protocol version 3 generation rules:

  • the entire export uses one SERIALIZABLE READ ONLY DEFERRABLE PostgreSQL transaction
  • the export reads WAL-materialized projections at one durable stream boundary, never independent live pages
  • the SQLite schema comes from one verified immutable schema manifest
  • only explicitly server-declared portable shared scopes are exported
  • every seeded row includes its opaque server version and verified SHA-256 row digest
  • every portable scope includes verified cardinality, scope digest, provenance, and an opaque portable seed receipt
  • publication occurs atomically only after row, scope, metadata, and SQLite integrity verification
  • the finalized database requires no WAL or shared-memory sidecar file

If no portable scopes are configured, the generator falls back to a schema-only seed.

The portable seed generator does not:

  • accept a JWT
  • export auth-scoped user data
  • create a separate client sync mode

Clients still log in normally, connect normally, and reconcile with the server on first use.

Run this example in Bash on the tutorial server, from its repository checkout. It requires Go 1.25 and the database created by Server setup. Create a separate export login. Neither the adapter nor the application owner needs seed-export authority.

Terminal window
sudo -u postgres createuser --host /var/run/postgresql --port 55432 \
--login --inherit --pwprompt \
--no-superuser --no-createdb --no-createrole --no-replication \
synchro_quickstart_seed
sudo -u postgres psql --host /var/run/postgresql --port 55432 \
--dbname synchro_quickstart -X -v ON_ERROR_STOP=1 \
--command 'GRANT synchro_seed TO synchro_quickstart_seed;'
Terminal window
make build-seed
(
export DATABASE_URL='postgres://synchro_quickstart_seed@127.0.0.1:55432/synchro_quickstart'
read -rsp 'Tutorial seed database password: ' PGPASSWORD
echo
export PGPASSWORD
bin/synchro-seed --output ./build/seed.db
)

The generator reads DATABASE_URL when --database-url is absent. The subshell discards its password after generation. For deployment, use a dedicated login with synchro_seed membership. Obtain its credential from your secret manager. Do not put database credentials in a seed artifact, application bundle, source file, or command history. The export accepts no user JWT.

Set seedDatabasePath before the first open. The SDK validates and atomically copies the seed only when the destination database does not exist. Later opens keep the existing local database and do not overwrite pending intent.

The following snippets show only the seedDatabasePath addition. Apply it to the complete client initialization in Client consumption. The variables in each snippet come from that prior initialization.

For Swift, pass an absolute bundled-file path:

let config = SynchroConfig(
dbPath: databaseURL.path,
serverURL: serverURL,
authProvider: { accessToken },
clientID: clientID,
appVersion: "0.3.0",
seedDatabasePath: seedURL.path
)

For Kotlin, copy the bundled asset to an application-private file before creating SynchroClient, then pass that file’s absolute path:

val config = SynchroConfig(
dbPath = context.getDatabasePath("notes.sqlite").absolutePath,
serverURL = serverURL,
authProvider = { accessToken },
clientID = clientID,
appVersion = "0.3.0",
seedDatabasePath = seedFile.absolutePath,
)

For React Native, pass an asset name or an absolute seed path:

const client = new SynchroClient({
dbPath: "notes.sqlite",
serverURL,
authProvider: async () => accessToken,
clientID,
appVersion: "0.3.0",
seedDatabasePath: "seed.db",
});

On Android, a relative React Native seed name resolves from the app assets. On iOS, it resolves from the application bundle before a Documents-directory fallback. Do not call initialize() until the seed asset is present.

When clients open a portable seed:

  • they keep the seeded schema
  • they keep the seeded portable rows
  • they use each verified seeded row version as the compare-and-swap base for offline update or delete capture
  • they treat each embedded continuation value as a portable seed receipt, not a runtime cursor
  • authenticated connect verifies the receipt, current assignment, schema, generations, boundary, cardinality, and checksum
  • only successful validation converts the receipt into a user-bound and client-bound incremental cursor
  • an unassigned seeded scope loses seed provenance and only unchanged zero-provenance cache rows; pending authored intent and its application-visible local row state remain for normal authorization and explicit outcome
  • an assigned stale or unverifiable scope loses only continuation state, reconciles eligible pending intent under push-first ordering, and then rebuilds
  • auth-scoped user data arrives through normal sync after login

This is a warm-start path, not a second protocol.

Portable seeds are useful when you want:

  • faster first app start
  • installer-time or bundle-time content available immediately
  • less initial schema work on device
  • shared portable data available before the first round trip completes

Portable seeds contain the same shared runtime scopes clients keep syncing later.

Do not invent seed-only scope copies for bundling. If a dataset belongs in a shared runtime scope, that is the scope the seed should export.