Portable Seeds
Synchro supports generating a client-compatible SQLite seed directly from PostgreSQL.
The generator lives at:
api/go/cmd/synchro-seed
What the generator does
Section titled “What the generator does”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 DEFERRABLEPostgreSQL 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.
What it does not do
Section titled “What it does not do”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.
Generate a seed
Section titled “Generate a seed”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.
sudo -u postgres createuser --host /var/run/postgresql --port 55432 \ --login --inherit --pwprompt \ --no-superuser --no-createdb --no-createrole --no-replication \ synchro_quickstart_seedsudo -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;'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.
Configure a client seed
Section titled “Configure a client seed”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.
Client behavior after login
Section titled “Client behavior after login”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
connectverifies 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.
When to use portable seeds
Section titled “When to use portable seeds”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
Relationship to scope modeling
Section titled “Relationship to scope modeling”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.