What Solebase is
A cloneable ES-module framework for hobby apps that stay offline by default. No app server, no hosted database — your static host only serves files.
- Local-first Each device keeps its own OPFS database. Opening the same URL elsewhere does not auto-share data.
- Zero-backend Ship HTML/JS/CSS. Optional sync talks to your own S3/Drive or peer-to-peer — never a Solebase cloud.
-
Layer 1 only
App code uses
Solebase/collection. Persistence runs in a Web Worker — never IndexedDB or main-thread SQLite.
Initialize
Import the built ESM from packages/solebase/dist/, then call
Solebase.init once at boot. Point workerUrl at
core/db-worker.js.
app.js
import { Solebase } from './packages/solebase/dist/index.js';
const db = await Solebase.init({
appId: 'myApp',
encryptionKey: 'device-secret',
workerUrl: new URL('./core/db-worker.js', import.meta.url),
});
const notes = db.collection('notes');
Needs a secure context (http://localhost or HTTPS) and a browser with OPFS.
Full reference: docs/solebase-layer1.md.
Use collection
Each named collection is a CRR table in the worker. Documents are JSON; ids are UUIDs when omitted.
CRUD + live query
const row = await notes.insert({ title: 'Hello' });
await notes.update(row.id, { title: 'Hi' });
const one = await notes.findById(row.id);
const hits = await notes.find({ title: 'Hi' });
await notes.delete(row.id);
const stop = notes.subscribe((docs) => {
render(docs); // immediate + on worker NOTIFY
});
insert(doc)— create; returns the stored documentupdate(id, patch)— merge fields into the documentdelete(id)— remove the rowfind(query?)/findById(id)— readsubscribe(cb)— live list; returns an unsubscribe function
Two-device sync
Hosting is not sync. Opening the same URL on a second phone does not share data. Each device keeps its own OPFS database until you configure optional E2EE sync. Plaintext never leaves the device.
Prerequisite on every device: the same appId and the same
encryptionKey. Different keys → decrypt fails. Different
appId → separate blob namespaces.
Shared identity (both devices)
const db = await Solebase.init({
appId: 'notes', // same on every device
encryptionKey: 'shared-secret', // same passphrase / key material
workerUrl: new URL('./core/db-worker.js', import.meta.url),
sync: { /* S3 and/or P2P below */ },
});
S3 path (R2 / MinIO / AWS)
Configure a real bucket once. Device A writes → encrypted changeset blobs land in S3.
Device B pulls those blobs, decrypts with the shared key, applies into its OPFS DB,
and collection.subscribe refreshes.
provider: 's3'
sync: {
provider: 's3',
options: {
endpoint: 'https://xxx.r2.cloudflarestorage.com',
region: 'auto',
bucket: 'my-bucket',
accessKeyId: '…',
secretAccessKey: '…',
prefix: 'solebase-changesets',
autoSync: true, // debounced push/pull after local writes
// pollIntervalMs: 15000,
},
},
await db.syncNow(); // manual push/pull when autoSync is off
- What uploads: AES-GCM ciphertext packages only (packed changesets), never plaintext rows.
- Blob keys:
{prefix}/{appId}/{siteId}/{cursor}-{uuid}.changeset - Second device: same
appId+encryptionKey+ S3 options →syncNow()(or wait forautoSync) downloads & merges. - Dev without a bucket:
memory: true(in-process only — not cross-device).
P2P path (WebRTC, no server)
Zero-backend signaling: peers exchange SDP offer/answer out-of-band (clipboard, QR, Share). There is no Solebase signaling cloud.
createP2POffer → acceptP2POffer → acceptP2PAnswer
// Both devices: sync.options.p2p = true (with or without S3)
const syncA = dbA.getSync();
const syncB = dbB.getSync();
const { payload: offerText } = await syncA.createP2POffer();
// …device A → B via QR / clipboard…
const { payload: answerText } = await syncB.acceptP2POffer(offerText);
// …device B → A via QR / clipboard…
await syncA.acceptP2PAnswer(answerText);
// Success: DataChannel open; encrypted changesets flow; subscribe refreshes
- Enable with
options.p2p: true(STUN defaultstun:stun.l.google.com:19302). - API lives on the transport from
db.getSync()— not on the host URL. - Success looks like: handshake completes, channel
open, peer receives ciphertext, decrypt + apply, UI updates viasubscribe.
Drive
provider: 'drive' is a stub today: same blob interface as S3, but
live Drive API + OAuth are not wired. Default backend is in-memory so apps and smokes
can exercise the config path. Do not expect cross-device Drive sync yet.
Full pipeline reference: docs/sync-transport.md, docs/sync-changesets.md.
Try & docs
Real app usage: copy an example, call Solebase.init, ship static files.
Live demo (no clone):
oguzatas.github.io/Solebase
— enable once under repo Settings → Pages → GitHub Actions if that URL 404s.
Engineers / smoke tests
Smokes are dev harnesses that prove Layer 1 / worker / sync paths —
not sample products. Prefer them while hacking the engine; app authors should stay on
Solebase.init + collection. Ship checklist:
docs/SHIP.md.
- examples/smoke-solebase Layer 1 init → insert → subscribe → update → find
- examples/smoke-sync-transport E2EE changeset push/pull + optional P2P
- examples/smoke-db Layer 3 OPFS / cr-sqlite worker
- examples/smoke-yjs Yjs persistence on cr-sqlite
- examples/smoke-sync-changesets Changeset extract + crypto
- docs/layer3-storage.md