Documentation

Why tinbase

tinbase came out of lifo and RapidNative with a hard goal: run an entire dev stack — database, auth, storage, realtime — in the browser and on phones, with no VMs and no cloud behind it. The first step was cutting the memory overhead of running that backend locally; the next was making the same backend run in-process inside a browser tab.

That is why every service is a pure (Request) => Response fetch handler and the database can be pure JavaScript. Along the way it became a Docker-free, drop-in replacement for local Supabase development that covers most use cases — so it is open source for everyone.

Architecture

The official supabase-js SDK talks to a single (Request) => Response fetch handler. That handler routes to the service implementations — PostgREST-compatible REST, GoTrue-compatible auth, Storage, the Realtime Phoenix protocol, Edge Functions, and the Studio admin API — and every one of them sits on a single swappable DbEngine adapter. Swap the engine (PGlite / native / pg-mem) without changing a line above it. In Node the handler is wrapped in an HTTP + WebSocket server; in the browser you call it in-process — see running in the browser.

@supabase/supabase-jsthe official SDK, unmodifiedRESTAuthStorageRealtimeFunctionssame wire protocol — HTTP in Node · in-process fetch in the browsertinbaseone (Request) ⇒ Response fetch handlerPostgREST/rest/v1GoTrue/auth/v1Storage/storage/v1RealtimeWebSocketEdge Fns/functions/v1Studio/_/DbEngine adapter · query · exec · transaction · listentinbase (wasm)PGlite — Postgres in WASMtinbase (native)embedded Postgres 17tinbase (pg-mem)pure JS, in-memoryreal Postgres semantics — RLS, triggers, FKs, jsonb (pg-mem is a subset)In NodeHTTP + WebSocket server — one process, no DockerIn the browserin-process fetch handler — the whole backend in a tab

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
tinbase gen types  # emit a TypeScript Database type
tinbase db reset   # wipe + re-run migrations and seed
tinbase db diff    # DDL for out-of-migration schema changes

  -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>      native (default), wasm, or pgmem
      --database-url <url>  use an external Postgres you already run
                            (postgres://…; or DATABASE_URL env)

Engines

One DbEngine interface, four backends. Pick with --engine (or --database-url) — nothing above the engine changes.

nativeDefault · macOS/Linux

Embedded Postgres 17 — real Postgres, PocketBase-class footprint

Memory
~59 MB at boot
Setup
first run downloads ~12 MB binaries (cached in ~/.cache/tinbase)
Access
private unix socket, trust auth — never TCP
Platform
macOS / Linux on x64 / arm64
wasmDefault · Windows · Browser

PGlite — Postgres compiled to WebAssembly

Setup
none — runs anywhere Node runs, including a browser tab
Memory
~575–650 MB WASM heap (does not shrink under load)
Parity
identical bootstrap, migrations, RLS, and realtime CDC to native
pgmemPreview · local-dev only

Ultralight pure-JS, in-memory — no WASM — via @tinbase/pg-mem, our pg-mem fork

Runs
a full Supabase bootstrap + real migrations unchanged (PL/pgSQL, triggers, RLS DDL, correlated subqueries, MERGE, partitioning); REST, auth, edge functions, realtime, webhooks
Caveats
LISTEN/NOTIFY are no-ops (CDC synthesized in JS); RLS created but not enforced (superuser); no cron / pgmq
Use
local-dev / preview — never production
--database-urlNew in 0.10

Bring your own external Postgres — REST, Auth, and Storage run against a database you already run

Connect
tinbase start --database-url postgres://…, the DATABASE_URL env, or createBackend({ databaseUrl })
Auth
TCP with SCRAM-SHA-256 (or md5)
Shared
idempotent bootstrap; migrations/seed stay tracked — never assumes an empty or exclusive DB
Soon
TLS / sslmode (managed providers), realtime CDC without superuser, pooling

The wasm and native engines run identical bootstrap, migrations, RLS, and realtime CDC — the full suite passes on both: TINBASE_TEST_ENGINE=native npm test.

Single binary

npm run build:binary   # requires bun; emits dist-bin/tinbase (~58 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 ~49 MB of RAM at boot, ~66 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. See the full Studio tour with screenshots:

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. Supabase-style Deno.serve(handler) functions (with Deno.env) run unchanged, as do export default handlers. The CLI loads them from supabase/functions/<name>/index.{ts,js,mjs}, or pass them to createBackend({ functions }). Functions using only Web APIs work as-is; npm:/jsr:/URL imports still need a bundling step.

// supabase/functions/hello/index.mjs
Deno.serve(async (req) => {
  const { name = 'world' } = await req.json().catch(() => ({}))
  return new Response(
    JSON.stringify({ message: `Hello ${name}!` }),
    { headers: { 'content-type': 'application/json' } }
  )
})

Webhooks, cron & queues

The automation layer works with no extension on either engine — tinbase implements it natively rather than needing pg_net/pg_cron/pgmq installed.

Database webhooksfire an HTTP request when rows change, with Supabase's exact payload (type/table/schema/record/old_record). Configure via createBackend({ webhooks }), backend.webhooks.register(), or supabase/webhooks.json.

Cron — drop-in with pg_cron's API: select cron.schedule('nightly', '0 0 * * *', 'delete from logs') (also the 'N seconds' form), cron.unschedule(...), and the cron.job / cron.job_run_details tables. Schedules match in UTC, like hosted pg_cron. An in-process scheduler runs due jobs and logs each run.

HTTP from SQL — a pg_net emulation: net.http_post / net.http_get / net.http_delete(...) enqueue a request that an in-process worker sends, recording the reply in net._http_response. So the common Supabase pattern — a cron job that calls an Edge Function, cron.schedule(..., $$ select net.http_post(...) $$) — runs unchanged.

All of the above run only while tinbase is up (no catch-up of missed runs), execute with service-role privileges (RLS bypassed), and are available on the wasm and native engines, not pg-mem.

Queues — a pgmq subset: call from SQL or the client.

await supabase.schema('pgmq').rpc('send', { queue_name: 'jobs', msg: { task: 'email' } })
const { data } = await supabase.schema('pgmq').rpc('read', { queue_name: 'jobs', vt: 30, qty: 5 })

Typed clients

Generate a Supabase-shaped Database type from the live schema, the same as supabase gen types typescript:

tinbase gen types typescript > database.types.ts
import type { Database } from './database.types'
const supabase = createClient<Database>(url, anonKey)  // fully typed queries

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) => Responsefetch 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. There's a dedicated guide to running in the browser (including the lighter pure-JS pg-mem engine):

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

Feature completeness

Where tinbase stands against the Supabase surface, mapped from the roadmap. ✓ Yes means it's implemented and covered by the test suite against the real supabase-js; ◑ Partial and – Planned are honest about the rest.

Database · REST (PostgREST)~95%
Select + filters (eq/neq/gt/lt/like/ilike/in/is, or/and trees)✓ Yes
Embedded resources (to-one, to-many, m2m, nested, !inner, aliases, hints)✓ Yes
Insert, bulk insert, upsert (merge/ignore)✓ Yes
Update, delete✓ Yes
count (exact/planned/estimated), single / maybeSingle✓ Yes
Full-text search, JSON-path filters, casts✓ Yes
order / limit / offset (top-level and per-embed)✓ Yes
RPC (scalar, setof, void, filters on results)✓ Yes
Spread embeds (…rel(col))✓ Yesto-one, to-many, m2m
Aggregates in select (count/sum/avg/…)✓ Yestop-level; not yet within embeds
.explain(), .csv()✓ Yes
Auth (GoTrue)~90%
Email / password sign-up + sign-in✓ Yes
Anonymous sign-in✓ Yes
Anonymous → permanent upgrade✓ Yeskeeps the same uid
Session refresh + rotation, getUser / updateUser / signOut✓ Yes
OAuth (Google, GitHub, generic OIDC) + PKCE✓ Yes
MFA / TOTP (enroll → challenge → verify, aal2)✓ Yes
Magic link / OTP / password recovery✓ Yesviewable in the local /inbox
Identity linking (auth.identities)✓ Yes
Admin user CRUD (service_role)✓ Yes
Phone / SMS auth– Plannedplanned
SSO / SAML– Plannedplanned
Realtime~90%
postgres_changes (INSERT / UPDATE / DELETE)✓ Yes
RLS-filtered postgres_changes✓ YesDELETE re-check is limited
Broadcast + presence✓ Yes
Private channels (RLS authorization via realtime.messages)✓ Yes
Broadcast-from-database (realtime.send)✓ Yes
Per-row DELETE RLS (WALRUS)◑ Partialauthenticated/service only
Storage~90%
Bucket CRUD, upload (raw + multipart), download✓ Yes
Public objects, signed URLs, signed upload URLs✓ Yes
List with folders, move / copy, remove, size/MIME limits✓ Yes
RLS on storage.objects✓ Yes
Resumable (TUS) uploads✓ Yes
Image transformations (resize/quality)◑ Partialserved as a no-op (original returned); real resize needs a codec
Edge Functions~85%
supabase.functions.invoke()✓ Yes
Deno.serve / Deno.env / export default handlers✓ Yes
TypeScript + relative / multi-file imports (bundled)✓ Yes
npm: / jsr: / URL imports✓ Yesvia esm.sh, fetched on first run
Function secrets (supabase/functions/.env)✓ Yes
Automationextension-free
Database webhooks (CDC → HTTP)✓ YesSupabase webhook payload
Cron jobs (cron.schedule)✓ Yespg_cron API — matches in UTC
HTTP from SQL (net.http_post/get)✓ Yespg_net emulation
Queues (pgmq: send/read/pop/archive/drop/purge/list)✓ Yesreplaces pgmq
Secrets (Supabase Vault)✓ Yescreate_secret / decrypted_secrets
Migrations, CLI & Studio~80%
supabase/migrations + seed conventions✓ Yesportable to hosted Supabase
Runs real projects unchanged (CREATE EXTENSION tolerated, CONCURRENTLY, per-file search_path)✓ Yese.g. Cap-go/capgo: 335 migrations + seed
db reset, db diff, db pull, inspect✓ Yes
gen types (TypeScript Database type)✓ Yes
Studio: table editor, SQL, auth, RLS editor, storage, logs✓ Yesat /_/
Studio: table/column designer UI– Plannedplanned
Engines & extensions
Native embedded Postgres 17 — default (macOS/Linux)✓ Yes
PGlite (WASM Postgres) — browser-ready, default on Windows✓ Yes
pg-mem (pure-JS, in-memory) — @tinbase/pg-mem fork✓ Yesruns PL/pgSQL, triggers, RLS DDL; RLS not enforced (superuser); no cron / pgmq
External Postgres (--database-url) — SCRAM/md5 auth◑ PartialREST/Auth/Storage; TLS + realtime CDC in progress
Single-file binary✓ Yes
Common extensions (uuid-ossp, pgcrypto, citext, pg_trgm, …)✓ Yeswhere the engine bundles them
pgvector (vector search)– Plannedneeds bundled extension binaries (Phase 4)

API coverage

ModuleCoverageNotable gaps
Database (postgrest-js)~95%aggregates within embeds
Auth (auth-js)~80%MFA, SSO/SAML, phone auth
Storage (storage-js)~90%image transforms (served as no-op)
Realtime (realtime-js)~85%per-row DELETE RLS, private channels
Edge Functions~70%npm:/jsr: import resolution, secrets
Type generation~85%composite-type args, multi-schema

Roughly 80% of the supabase-js SDK surface overall — and ~90% of what a typical CRUD + auth + storage + realtime app actually calls.

Beyond the client SDK, the local platform features real projects rely on also work: type generation, RLS (enforced on REST, Storage, and realtime), database webhooks, cron, queues (pgmq), the Studio dashboard, and Supabase-CLI migration conventions with db reset / db diff.

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.

Install footprint vs memory under load (MB) · lower is better
Install footprintMemory under load
PocketBase
SQLite · different API
30
24
tinbase (native)
real Postgres 17
36
100
tinbase (binary)
real Postgres 17
92
66
Supabase local
Postgres · 12 containers
2,291
1,626

tinbase engines in colour; PocketBase and Supabase muted for context. PocketBase is the smallest footprint, but it is SQLite behind a different API — not a drop-in for supabase-js, unlike every tinbase engine. Linear scale; Supabase local (2,291 / 1,626 MB) is a 12-container Docker stack whose bars run off the axis (torn end) so the single-process engines stay comparable. Physical footprint of the whole process tree (vmmap / docker stats), Apple Silicon · macOS 15 · bench/footprint.ts

The two axes tell different stories: pg-mem uses the most RAM under load of the tinbase engines, yet is by far the lightest to ship — a ~6.7 MB pure-JS install with no WASM and no native binary, ideal for the browser and embedded previews.

tinbase (binary)tinbase (native)tinbase (pg-mem)tinbase (wasm)PocketBaseSupabase local
Databasereal Postgres 17 + RLSreal Postgres 17 + RLSin-memory, pure-JSreal Postgres (PGlite) + RLSSQLitePostgres 17
Memory at boot49 MB59 MB71 MB~610 MB15 MB1,441 MB
Memory under load66 MB100 MB185 MB~640 MB24 MB1,626 MB
Data on disk (1k rows)39 MB39 MB0 (in-memory)40 MB7 MB70 MB
Install size92 MB (58 MB bin + PG)36 MB + Node6.7 MB + Node27 MB + Node30 MB2,291 MB + Docker
Processes2211112 containers
1,000 inserts0.4 s0.5 s0.8 s0.8 s0.3 s1.1 s
1,000 filtered reads0.3 s0.4 s0.8 s0.9 s0.3 s1.0 s

The wasm figure is essentially PGlite's WASM heap, which measures anywhere in ~575–650 MB depending on GC timing — treat it as a band, not a point. pgmem is a pure-JS in-memory engine that runs PL/pgSQL, triggers and RLS-policy DDL (migrations apply unchanged), though as a superuser so RLS isn't enforced per-request, and cron/pgmq are absent — but a ~6.7 MB install with no WASM, the lightest option for the browser.

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