In the browser
The whole backend, in a tab
Every tinbase service is a pure (Request) ⇒ Response fetch handler. There is no HTTP layer to stand up — hand the handler to supabase-js as its fetch and the database, auth, storage, and realtime all run in-process, inside the page. No server. No network round-trip.
Run it in-process
The same supabase-js you use everywhere, pointed at a backend that lives in the same JavaScript heap:
import { createClient } from '@supabase/supabase-js'
import { createBackend, createPgmemEngine } from 'tinbase'
// a whole backend — in memory, inside the page
const backend = await createBackend({
engine: await createPgmemEngine(), // pure JS, nothing to download
migrations: [{
name: 'init',
sql: 'create table todos (id serial primary key, text text, done boolean default false)',
}],
})
// hand it to supabase-js as a custom fetch — no server, no network
const supabase = createClient('http://localhost', backend.anonKey, {
global: { fetch: (input, init) => backend.fetch(new Request(input, init)) },
})
await supabase.from('todos').insert({ text: 'ship it' })
const { data } = await supabase.from('todos').select()Pick an engine for the browser
Two of the three engines run in a browser. pg-mem is pure JavaScript and the lightest thing to embed; PGlite is real Postgres compiled to WASM when you want full fidelity.
| pg-mem | PGlite (wasm) | |
|---|---|---|
| Footprint | ~6.7 MB install · pure JS · no WASM | ~575–650 MB heap · WASM |
| Fidelity | CRUD, auth, functions, realtime, webhooks + PL/pgSQL, triggers, RLS policies | full Postgres — enforced RLS, extensions |
| Persistence | in-memory | IndexedDB / OPFS |
| Best for | phones, previews, the lightest embed | full Postgres parity in the browser |
pg-mem now runs PL/pgSQL, triggers and RLS-policy DDL (via the @tinbase/pg-memfork), but as a superuser so RLS isn't enforced per-request, and cron/pgmq are absent — it's meant for local dev and previews. For full, enforced Postgres semantics in the browser, use PGlite — and persist across reloads with an IndexedDB data dir:
import { createBackend } from 'tinbase'
// omit `engine` → PGlite (real Postgres in WASM).
// persist across reloads with an IndexedDB-backed data dir:
const backend = await createBackend({ dataDir: 'idb://my-app' })How it fits together
The same fetch handler serves an HTTP + WebSocket server in Node, or runs directly in the page in the browser — only the transport changes.
Why it was built this way
tinbase came out of lifo — a project that maps Linux APIs into the browser — and RapidNative, where Expo apps run full-stack in the browser and on phones. Both need a real backend with no server behind it, which is exactly why every service here is a fetch handler and the database can be pure JavaScript.