Documentation

Experimental. tinbase is young and moving fast — great for prototypes, local development, demos, and embedded/browser use. It is not meant for production usage yet.

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 native

Engines

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 deployment

One 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:

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

ModuleCoverageNotable 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.

Memory under load (MB) · 1,000 inserts + 1,000 filtered reads · lower is better
PocketBaseSQLite
25
tinbase (binary)real Postgres + RLS
64
tinbase (wasm)real Postgres + RLS
347
Supabase localPostgres
1,626

Physical footprint of the whole process tree (vmmap / docker stats), Apple Silicon · macOS 15 · bench/footprint.ts

tinbase (binary)tinbase (wasm)PocketBaseSupabase local
Databasereal Postgres 17 + RLSreal Postgres (PGlite) + RLSSQLitePostgres 17
Memory at boot44 MB573 MB16 MB1,441 MB
Memory under load64 MB347 MB25 MB1,626 MB
Data on disk (1k rows)38 MB39 MB7 MB70 MB
Install size92 MB, no runtime26 MB + Node30 MB2,291 MB + Docker
Processes21112 containers
1,000 inserts0.4 s0.8 s0.3 s1.1 s
1,000 filtered reads0.4 s0.8 s0.3 s1.0 s

Methodology, raw numbers, and a reproducible script live in the repo: bench/footprint.ts and bench/results.json.