Every app includes:
| path | use |
|---|---|
/shared.db |
replicated SQLite |
/shared/ |
replicated files |
Open them as local paths. Writes commit locally, converge across instances and regions, and land durably in object storage. There is no database service or primary endpoint to provision.
Use these before adding a volume. A persistent volume is for software that requires a private local filesystem.
sqlite3 /shared.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes(id TEXT PRIMARY KEY, body TEXT);
INSERT INTO notes VALUES('one', 'hello')
ON CONFLICT(id) DO UPDATE SET body=excluded.body;
SELECT body FROM notes WHERE id='one';
SQL
printf 'generated once\n' > /shared/artifact.txt
cat /shared/artifact.txt
multi-writer rules
- Replicated tables need a primary key. Bare
INTEGER PRIMARY KEYandAUTOINCREMENTschemas are rewritten to assign sparse, collision-safe 63-bit values. Recover them withINSERT ... RETURNING id, notlast_insert_rowid(). - An instance reads its own commit immediately. Other replicas converge asynchronously, normally in under a second.
- Conflicts resolve deterministically: when two instances update the same row,
the later write wins whole-row, ordered by a hybrid logical clock. A
nullable
UNIQUEcolumn gives the contested value to the later writer and nulls it on the losing row. NOT NULL UNIQUEvalues and DDL coordinate cluster-wide before the local commit, so they can fail with a retryable error while a region is unreachable. All other writes keep committing through disconnection and converge on reconnect.- Committed writes reach peer instances within milliseconds and object storage typically within a second; losing a host loses at most that window of its latest local writes.
- Framework migrations work when each transaction contains one DDL statement plus bookkeeping. Split transactions containing several DDL statements.
- Tables whose names begin with
_stay local by default. Setpersistence.replicate-underscore-tables=truewhen creating the database to include them in replication; the database retains that policy afterward. - Temporary tables and
:memory:databases remain local.
The replication engine is syzy; its documentation specifies the full conflict model.
SQLite compatibility
/shared.db works transparently with programs that dynamically link the
system libsqlite3: Python's stdlib, Ruby's system SQLite gem, the CLI, Rails,
Django, and Go built with -tags=libsqlite3.
Bindings that statically embed SQLite cannot be intercepted. Examples include
better-sqlite3, Node's built-in node:sqlite, modernc.org/sqlite, and
mattn/go-sqlite3 without the system-library build tag. Rebuild against
libsqlite3.so, or load the engine explicitly after opening:
SELECT load_extension('/usr/local/lib/syzy-engine.so', 'sqlite3_syzy_init');
shared files
/shared/ is a content-oriented POSIX mount for uploads, generated assets, and
small blobs. Advisory locks, mmap, and O_DIRECT are not supported; put
coordination in /shared.db.
Two instances writing one file merge by byte range under the same last-writer-wins clock; creating the same new path twice keeps one winner.
mount shared files elsewhere
A shared Compose volume mounts the app's /shared/ tree at another path:
services:
web:
volumes: [uploads:/srv/uploads]
worker:
volumes: [uploads:/work/uploads]
volumes:
uploads:
x-kedge: {shared: true}
Both mount points refer to the same replicated file tree. One service may currently mount one shared volume path.
named databases
An app's default database follows that app. A named database can outlive and serve several apps:
kedge db create mydata
kedge db attach mydata myapp
kedge db attach mydata myworker
New instances attach immediately; existing instances pick it up when they
recycle. kedge db detach myapp returns that app to its private default. The
app page provides a SQL console for either kind. Run
kedge db shell myapp 'SELECT …' from anywhere, or omit myapp when
KEDGE_APP is set.
The database console gives each submitted statement its own transaction
boundary, and writes replicate atomically. Explicit BEGIN, COMMIT, and
savepoint wrappers are not supported. Put writes that must commit together in
one statement, such as a multi-row INSERT or a CTE.
previews and forks
On object-backed deployments, a preview's first deploy branches the production app's current database and files without copying all pages. Re-pushes keep the preview's changes; deleting and recreating it branches production again. A source with no stored state starts empty. Local servers without an object bucket keep the empty-preview behavior.
The branch covers /shared.db and /shared/, including an attached named
database. Persistent volumes are separate. External database state is not
branched; inherited URLs can still point both apps at the same service.
Explicit forks use the same shared-data branch:
kedge fork myapp myapp-experiment
A static or HTML app published with the forkable=true property can be forked
by anyone, signed in or not, into an app they own; the copy is private and
carries the source's data as of that moment.