Solebase

Local-first, zero-backend starter for personal apps. Data lives in the browser (OPFS + cr-sqlite). Apps talk only to the Layer 1 API.

Start with Solebase.init Two-device sync Live on GitHub Pages

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.

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
});

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

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

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.