Documentation
Why tinbase
tinbase was built for lifo (lifo.sh) — a project that maps Linux APIs into the browser — to let Expo apps run fully in the browser with full-stack capability: database, auth, storage, and realtime with no server behind them. It is part of RapidNative.
That origin drives the architecture: every service is a pure (Request) => Response fetch handler and the default engine is Postgres compiled to WASM, so the entire backend can run in-process inside a browser tab — or as a tiny server on your machine. The same design turned out to make an excellent standalone local Supabase replacement, so it is open source for everyone.
Getting started
tinbase is a Supabase-compatible backend in a single process. In a project with a supabase/ directory (or none — it still boots):
npx tinbase start
# API URL: http://127.0.0.1:54321
# anon key: eyJ...
# service_role key: eyJ...Migrations in supabase/migrations/*.sql and supabase/seed.sql are applied on boot, using the same file conventions and tracking table as the Supabase CLI — so they remain portable to hosted Supabase. Then point the official SDK at it:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('http://127.0.0.1:54321', ANON_KEY)CLI reference
tinbase start # boot the server (applies pending migrations first)
tinbase migrate # apply pending migrations and exit
tinbase status # list applied migrations
tinbase keys # print anon / service_role keys
-p, --port <n> port (default 54321; or TINBASE_PORT / PORT env)
--dir <path> project dir containing supabase/ (default cwd)
--data-dir <path> data dir (default <dir>/.tinbase/db)
--jwt-secret <s> JWT secret (or TINBASE_JWT_SECRET)
--memory in-memory database (wasm engine)
--engine <e> wasm (default) or nativeEngines
wasm (default) runs PGlite — Postgres compiled to WebAssembly. Zero setup, runs anywhere Node runs including the browser; costs ~350 MB of RAM.
native runs embedded native Postgres 17. The first run downloads platform binaries (~12 MB, cached in ~/.cache/tinbase), then initdb with memory-lean settings. ~53 MB of RAM at boot. It listens only on a private unix socket (0700 directory, trust auth) — never TCP. macOS and Linux on x64/arm64.
Both engines run identical bootstrap, migrations, RLS, and realtime CDC. The full test suite passes on both: TINBASE_TEST_ENGINE=native npm test.
Single binary
npm run build:binary # requires bun; emits dist-bin/tinbase (~57 MB)
./tinbase start # that's the whole deploymentOne compiled executable — no Node, npm, or Docker on the target machine. It defaults to the native engine and serves REST, Auth, Storage, and Realtime WebSockets at 44 MB of RAM at boot, 64 MB under load.
Studio
A built-in dashboard ships at /_/, shaped like Supabase Studio (React + Radix + Tailwind). Log in with the service_role key printed at startup:
- Table Editor — browse tables with pagination and row counts; insert, edit, and delete rows
- SQL Editor — run SQL with result grids and Postgres error details
- Authentication — list, create, delete users, reset passwords
- Storage — create/delete buckets, upload/delete objects, toggle public access
- Database — stats overview and applied migrations
It compiles to a single self-contained HTML file, so it works inside the single binary too.
Edge Functions
supabase.functions.invoke() runs your handlers in-process. A function is any fetch handler; the CLI loads them from supabase/functions/<name>/index.{ts,js,mjs} (default export), or you pass them to createBackend({ functions }). Each call receives the verified auth context and the project's env keys.
// supabase/functions/hello/index.mjs
export default async function handler(req, ctx) {
const { name = 'world' } = await req.json().catch(() => ({}))
return new Response(
JSON.stringify({ message: `Hello ${name}!`, role: ctx.auth.role }),
{ headers: { 'content-type': 'application/json' } }
)
}Row Level Security
Every REST and Storage request runs inside a transaction with SET LOCAL role and request.jwt.claims applied, so policies behave exactly like hosted Supabase:
create policy "own rows" on todos
for all to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());Embedding & browser
The core is a pure (Request) => Response fetch handler. Serve it over HTTP in Node, or hand it to supabase-js as a custom fetch and run the whole backend in-process — in the browser, PGlite persists to IndexedDB/OPFS:
import { createBackend } from 'tinbase'
const backend = await createBackend({
// dataDir: 'idb://my-app' <- browser persistence
migrations: [{ name: '20240101000000_init', sql: '...' }],
})
const supabase = createClient('http://localhost', backend.anonKey, {
global: { fetch: (i, init) => backend.fetch(new Request(i, init)) },
})API coverage
| Module | Coverage | Notable gaps |
|---|---|---|
| Database (postgrest-js) | ~85% | aggregates in select, .explain(), .csv() |
| Auth (auth-js) | ~65% | OAuth providers, MFA, SSO, PKCE |
| Storage (storage-js) | ~80% | resumable uploads, image transforms |
| Realtime (realtime-js) | ~70% | RLS-filtered fan-out, private channels |
| Edge Functions | ~60% | Deno runtime compat, import maps |
Roughly 75% of the SDK surface overall — but ~90% of what a typical CRUD + auth + storage + realtime app actually calls.
Benchmarks
Same workload for every backend: boot with one migrated table, then 1,000 single-row inserts followed by 1,000 filtered list queries. Memory is the physical footprint of the whole process tree (vmmap) for native processes and the sum of docker stats for containers. Apple Silicon, macOS 15.
Physical footprint of the whole process tree (vmmap / docker stats), Apple Silicon · macOS 15 · bench/footprint.ts
| tinbase (binary) | tinbase (wasm) | PocketBase | Supabase local | |
|---|---|---|---|---|
| Database | real Postgres 17 + RLS | real Postgres (PGlite) + RLS | SQLite | Postgres 17 |
| Memory at boot | 44 MB | 573 MB | 16 MB | 1,441 MB |
| Memory under load | 64 MB | 347 MB | 25 MB | 1,626 MB |
| Data on disk (1k rows) | 38 MB | 39 MB | 7 MB | 70 MB |
| Install size | 92 MB, no runtime | 26 MB + Node | 30 MB | 2,291 MB + Docker |
| Processes | 2 | 1 | 1 | 12 containers |
| 1,000 inserts | 0.4 s | 0.8 s | 0.3 s | 1.1 s |
| 1,000 filtered reads | 0.4 s | 0.8 s | 0.3 s | 1.0 s |
Methodology, raw numbers, and a reproducible script live in the repo: bench/footprint.ts and bench/results.json.