# tinbase runs a Supabase-shaped backend on real Postgres without Docker

> tinbase boots a local backend in one process and speaks the same wire protocols as hosted Supabase, so the official supabase-js client works unchanged. It is explicitly alpha, and the choice between its three Postgres engines decides whether row level security is actually enforced.

**tinbase/tinbase** — Supabase-compatible backend in a single binary — real Postgres with RLS, no Docker. Works with supabase-js unchanged.

- Repository: https://github.com/tinbase/tinbase
- Website: https://tinbase.dev
- Stars: 346 · Forks: 33
- Language: TypeScript
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/tinbase-tinbase

## One command boots the API, and the keys never change

tinbase start is the whole setup, and it needs nothing already installed beyond Node:

```bash
npx tinbase start
```

It prints an API URL of http://127.0.0.1:54321 plus an anon key and a service_role key. There is no Docker daemon, no container image and no external service; the single runtime dependency is @electric-sql/pglite. The point of the exercise is wire compatibility, so the official @supabase/supabase-js client talks to it without a shim across REST, Auth, Storage and Realtime.

The same printed values come back on every start and on every machine, because they are the well-known Supabase local-dev demo keys. That is a deliberate design choice with a security consequence attached, covered in the next section.

The caveat sits at the top of the README and should not be skimmed: alpha, not production-ready yet, aimed at local development, prototypes and embedded or browser use.

## The keys are meant to be committed to .env.local

Because those keys are identical for every teammate, the workflow matches supabase start rather than fighting it. You write them once into a shared file and check it in:

```bash
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
```

Determinism is configurable rather than fixed. Pass --jwt-secret and the keys stay deterministic, stable per secret, so two developers with the same secret see the same values. Set NODE_ENV=production and every start signs fresh, unique keys instead, which closes the hole that a committed service_role key would otherwise open.

The client code downstream needs no changes either:

```ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient('http://127.0.0.1:54321', ANON_KEY)

await supabase.auth.signUp({ email: 'me@example.com', password: 'secret123' })
await supabase.from('todos').insert({ title: 'hello' })
const { data } = await supabase.from('todos').select('*, author:users(name)').eq('done', false)
```

That last select with the embedded author:users(name) relation is the shape of query that decides whether a drop-in replacement is real.

## Migrations come from supabase/migrations and stay portable

tinbase reads the same files the Supabase CLI reads, supabase/migrations/*.sql and supabase/seed.sql, and records what it has run in supabase_migrations.schema_migrations. Nothing about the convention changes, so a directory of migrations is not something you rewrite to develop locally.

What changes is tolerance. CREATE EXTENSION statements for extensions tinbase cannot install, such as pg_cron, pg_net, http and hypopg, are skipped instead of aborting the file. CREATE INDEX CONCURRENTLY is applied with the keyword removed, since that form is illegal inside a transaction. Every migration runs with a fresh search_path the way the Supabase CLI does, and Vault, pgmq, cron, pg_net and moddatetime are emulated rather than missing.

The stress test named in the README is Cap-go's repository, where 335 migrations plus the seed file apply and query cleanly. The subcommands around that flow are migrate to apply pending files and exit, status to list what has run, db reset to wipe database and storage and re-run everything, and db diff to print DDL for changes you have not captured yet, with -f <name> to save it.

```bash
tinbase migrate
tinbase status
tinbase db reset
tinbase db diff
```

## Three engines, and only two of them enforce RLS

The engine choice is the real decision here, because each one trades memory for fidelity.

The native engine is the default on macOS and Linux: embedded Postgres 17, about 59 MB of RAM at boot, described as a PocketBase-class footprint. First run downloads roughly 12 MB of platform binaries from theseus-rs/postgresql-binaries into ~/.cache/tinbase, then runs initdb. It listens only on a private unix socket in a 0700 directory with trust auth and never on TCP.

The wasm engine, PGlite, is the default on Windows and runs anywhere Node does, including the browser. Its WASM heap sits around 575 to 650 MB and does not shrink under load, which is the opposite trade from native.

The pgmem engine, selected with --engine pgmem, is pure JS with no WASM and the lightest option for browser previews. Its fork of pg-mem adds PL/pgSQL, triggers, RLS policies, correlated subqueries, MERGE and declarative partitioning, and a full Supabase-style bootstrap applies unchanged. The limits are stated plainly: LISTEN and NOTIFY are no-ops so realtime events are synthesized in JS, cron and pgmq are absent, and because the engine runs with superuser rights, RLS policies are created but not enforced per request. Events arrive unfiltered rather than per subscriber. That engine is for local dev and previews only.

## Every service is a fetch handler, so it runs in the browser

The architectural bet is visible in one line: each service is a pure (Request) => Response fetch handler. In Node that becomes an HTTP server. In a browser you hand the same handler to supabase-js as a custom fetch and the whole backend executes in-process, with PGlite storing through IndexedDB or OPFS.

That is why the pgmem engine exists at all. A WASM Postgres heap of several hundred megabytes is a poor fit for a preview page or a RapidNative session, and a pure JS engine with no WASM is the answer, at the price of the RLS caveat above.

The native and wasm engines are held to the same standard as each other: identical bootstrap, migrations, RLS and realtime CDC, with the test suite passing on both.

```bash
TINBASE_TEST_ENGINE=native npm test
```

The repository also carries a parity harness, run through npm run parity, and directories for benchmarks, an admin UI and a todo example, so the compatibility claim has somewhere to be checked besides the README.

## One executable, but Postgres binaries still download at runtime

For deployment targets without Node, the project compiles itself:

```bash
npm run build:binary
./tinbase start
```

The build requires bun and emits dist-bin/tinbase at about 58 MB. The result needs no Node and no npm on the target machine and serves REST, Auth, Storage and Realtime WebSockets from roughly 49 MB of RAM at boot, about 66 MB under load, running under Bun's runtime via a Bun-native server.

One catch travels with the binary: it defaults to the native engine, so those 12 MB of Postgres binaries are downloaded on the target machine on first run. The section describing the single-binary deployment is cut off partway through in the copy of the README available here, so read the rest of it before you script this for a machine without outbound network access.

The npm package itself declares Node 22 or newer as its engine floor, and ships two entry points: the default build and a ./node export, with the tinbase CLI binary alongside them.

## Alpha, and moving fast enough that versions matter

Three releases landed inside five days: v0.17.0 and v0.18.0 on 2026-09-25, then v0.19.0 on 2026-09-30, with the last push recorded on 2026-09-25. At that rate the version number tells you little about which behaviour you will get, which is why the project ships CHANGELOG.md and ROADMAP.md at the root.

Other root files are worth knowing about before you build on this. COMPLIANCE.md is there for the same reason a local dev tool that emulates hosted Supabase needs one. The supabase/ directory in the tree holds a project-shaped example rather than configuration you must adopt, and examples/todo-app/ is the smallest end-to-end thing to read when you want to know what a working setup looks like.

MIT covers the code, which is the one uncomplicated fact in a tool whose whole purpose is standing in for someone else's licensed service.

## Conclusion

Adopt tinbase for local development, prototypes, preview environments and browser demos where Docker on the machine is the thing you want gone, and pick the native or wasm engine so row level security stays real. Do not adopt it as a production Supabase replacement, because the README marks it alpha and not production-ready, and the pgmem engine never enforces RLS. Verify first that your migrations run under the engine you intend to ship, using tinbase migrate and tinbase db diff, and read the notices in ROADMAP.md before you depend on the version number.

## FAQ

### Is tinbase ready to replace Supabase in production?

No. The README marks it alpha and not production-ready, aimed at local development, prototypes and embedded or browser use. Two details point the same way: under NODE_ENV=production every start signs fresh unique keys, and the pgmem engine is documented as local dev and previews only, never production.

### Does tinbase enforce row level security?

On the native and wasm engines it does, since RLS policies, auth.uid(), triggers and foreign keys are Postgres features. On the pgmem engine the policies are created but not enforced per request, because that engine runs with superuser rights and realtime events are delivered unfiltered instead of per subscriber.

### Will my existing Supabase migrations run under tinbase?

They are read from supabase/migrations/*.sql and supabase/seed.sql and tracked in supabase_migrations.schema_migrations. CREATE EXTENSION for unsupported extensions is skipped rather than aborting, CREATE INDEX CONCURRENTLY is applied without the keyword, and Cap-go's 335 migrations plus seed are cited as applying and querying cleanly.

### Can tinbase run on a machine with no Node installed?

Yes, through the single-binary build. npm run build:binary requires bun and emits dist-bin/tinbase at about 58 MB, started with ./tinbase start. The executable defaults to the native engine, so about 12 MB of Postgres binaries are still downloaded on first run.

### Which tinbase engine should I use for local development?

The default is embedded native Postgres 17 on macOS and Linux at about 59 MB of RAM, and PGlite on Windows with a WASM heap around 575 to 650 MB that does not shrink under load. For browser previews, --engine pgmem removes the WASM entirely but gives up per-request RLS enforcement.

## Sources

- [License: MIT](https://github.com/tinbase/tinbase/blob/main/LICENSE)
- [Project website](https://tinbase.dev)
- [README](https://github.com/tinbase/tinbase/blob/main/README.md)
- [Releases](https://github.com/tinbase/tinbase/releases)
- [tinbase/tinbase on GitHub](https://github.com/tinbase/tinbase)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tinbase-tinbase
