[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-92587":3},{"id":4,"name":5,"fullName":6,"owner":5,"repo":5,"description":7,"homepage":8,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":15,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":16,"rankGlobal":9,"rankLanguage":9,"license":17,"archived":18,"fork":18,"defaultBranch":19,"hasWiki":20,"hasPages":18,"topics":21,"createdAt":9,"pushedAt":9,"updatedAt":31,"readmeContent":32,"aiSummary":33,"trendingCount":14,"starSnapshotCount":14,"syncStatus":34,"lastSyncTime":35,"discoverSource":36},92587,"tinbase","tinbase\u002Ftinbase","Supabase-compatible backend in a single binary — real Postgres with RLS, no Docker. Works with supabase-js unchanged.","https:\u002F\u002Ftinbase.vercel.app",null,"TypeScript",204,14,91,0,113,53.53,"MIT License",false,"main",true,[22,23,24,25,26,27,28,29,30],"baas","backend","pglite","pocketbase-alternative","postgres","realtime","sqlite-alternative","supabase","supabase-alternative","2026-07-22 04:02:06","\u003Cp align=\"center\">\u003Cimg src=\"assets\u002Flogo.svg\" width=\"96\" alt=\"tinbase logo\">\u003C\u002Fp>\n\n# tinbase\n\nLocal Supabase dev without Docker — one process, real Postgres, and it even runs in the browser. It speaks the same wire protocols as hosted Supabase, so the **official `@supabase\u002Fsupabase-js` SDK works unchanged** - REST, Auth, Storage, and Realtime.\n\nA pure-JS backend built on [PGlite](https:\u002F\u002Fpglite.dev) (Postgres compiled to WASM), with an embedded-native-Postgres and an ultralight pure-JS ([pg-mem](https:\u002F\u002Fgithub.com\u002Foguimbal\u002Fpg-mem)) engine too. One command, real Row Level Security, and 1:1 with Supabase's APIs and migration conventions.\n\n> [!WARNING]\n> **Alpha — not production-ready yet.** Great for local development, prototypes, and embedded\u002Fbrowser use.\n\n```\nnpx tinbase start\n```\n\n- **No Docker, no external services.** One runtime dependency: `@electric-sql\u002Fpglite`.\n- **Real Postgres semantics.** RLS policies, `auth.uid()`, triggers, FKs - it is Postgres.\n- **Three engines, one API.** Default is embedded native Postgres 17 (macOS\u002FLinux): ~59 MB of RAM at boot, PocketBase-class footprint, zero semantic differences; the first run downloads ~12 MB of binaries. `--engine wasm` runs PGlite (Postgres compiled to WASM) instead - portable, browser-ready, and the default on Windows. `--engine pgmem` is an ultralight pure-JS in-memory subset for local dev \u002F previews - see [Engines](#engines).\n- **Supabase CLI migration conventions.** Reads `supabase\u002Fmigrations\u002F*.sql` and `supabase\u002Fseed.sql`; tracks them in `supabase_migrations.schema_migrations`. Your migration files stay portable to hosted Supabase.\n- **Runs real projects unchanged.** A project's `CREATE EXTENSION` statements for extensions tinbase can't install (pg_cron, pg_net, http, hypopg, …) are skipped rather than aborting; `CREATE INDEX CONCURRENTLY` is applied without the (transaction-illegal) `CONCURRENTLY`; each migration runs with a fresh `search_path` like the Supabase CLI; and Vault, pgmq, cron, pg_net, and `moddatetime` are emulated. As a test, [Cap-go\u002Fcapgo](https:\u002F\u002Fgithub.com\u002FCap-go\u002Fcapgo)'s **335 migrations + seed apply and query cleanly**.\n- **Browser-ready core.** Every service is a pure `(Request) => Response` fetch handler. In Node it's served over HTTP; in the browser you can hand it to supabase-js as a custom `fetch` and run the whole backend in-process (PGlite already runs in the browser via IndexedDB\u002FOPFS).\n\n## Quick start\n\n```bash\n# in a project with a supabase\u002F directory (or none - it still boots)\nnpx tinbase start\n\n#   API URL: http:\u002F\u002F127.0.0.1:54321\n#   anon key: eyJ...\n#   service_role key: eyJ...\n```\n\nPoint the ordinary supabase-js client at it:\n\n```ts\nimport { createClient } from '@supabase\u002Fsupabase-js'\n\nconst supabase = createClient('http:\u002F\u002F127.0.0.1:54321', ANON_KEY)\n\nawait supabase.auth.signUp({ email: 'me@example.com', password: 'secret123' })\nawait supabase.from('todos').insert({ title: 'hello' })\nconst { data } = await supabase.from('todos').select('*, author:users(name)').eq('done', false)\n\nsupabase\n  .channel('feed')\n  .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'todos' }, console.log)\n  .subscribe()\n```\n\n### CLI\n\n```\ntinbase start      # boot the server (applies pending migrations first)\ntinbase migrate    # apply pending migrations and exit\ntinbase status     # list applied migrations\ntinbase keys       # print anon \u002F service_role keys\ntinbase gen types  # print a TypeScript Database type for the schema\ntinbase db reset   # wipe the database + storage, re-run migrations and seed\ntinbase db diff    # DDL for schema changes not yet in migrations (-f \u003Cname> to save one)\n\n  -p, --port \u003Cn>        port (default 54321; or TINBASE_PORT \u002F PORT env)\n      --dir \u003Cpath>      project dir containing supabase\u002F (default cwd)\n      --data-dir \u003Cpath> PGlite data dir (default \u003Cdir>\u002F.tinbase\u002Fdb)\n      --jwt-secret \u003Cs>  JWT secret (or TINBASE_JWT_SECRET)\n      --memory          in-memory database, no persistence (wasm engine)\n      --engine \u003Ce>      native (default), wasm, or pgmem\n```\n\n### Engines\n\n- **native** (default on macOS\u002FLinux): embedded native Postgres 17. First run downloads platform binaries (~12 MB, from [theseus-rs\u002Fpostgresql-binaries](https:\u002F\u002Fgithub.com\u002Ftheseus-rs\u002Fpostgresql-binaries), cached in `~\u002F.cache\u002Ftinbase`), then `initdb` with memory-lean settings. ~59 MB RAM at boot. Listens only on a private unix socket (0700 dir, trust auth) - never TCP. macOS\u002FLinux on x64\u002Farm64.\n- **wasm** (default on Windows): PGlite. Zero setup, runs anywhere Node runs - and in the browser. Its WASM heap sits around ~575-650 MB and does not shrink under load.\n\n- **pgmem** (`--engine pgmem`): an ultralight, pure-JS, in-memory subset via [pg-mem](https:\u002F\u002Fgithub.com\u002Foguimbal\u002Fpg-mem) — **~3.6 MB install, no WASM**, so it's the lightest option for the browser (RapidNative local-dev \u002F previews). Runs the REST CRUD surface, email\u002Fpassword auth, **edge functions, realtime (broadcast\u002Fpresence + `postgres_changes`), and database webhooks**. pg-mem has no triggers or LISTEN\u002FNOTIFY, so realtime\u002Fwebhook change events are synthesized in JS by the REST layer (every write goes through it in-process). What's *not* here: **RLS** (so realtime\u002Fwebhook events are delivered unfiltered, not per-subscriber), **cron**, and **pgmq** - RLS DDL in migrations is skipped, not fatal. Local-dev \u002F preview only — never production.\n\nThe wasm and native engines run the identical bootstrap, migrations, RLS, and realtime CDC - the test suite passes on both (`TINBASE_TEST_ENGINE=native npm test`).\n\n### Single-binary build\n\n```bash\nnpm run build:binary   # requires bun; emits dist-bin\u002Ftinbase (~58 MB)\n\n.\u002Ftinbase start        # that's the whole deployment\n```\n\nOne compiled executable, no Node or npm on the target machine. It defaults to the native engine (Postgres binaries auto-download on first run, 12 MB) and serves everything - REST, Auth, Storage, Realtime WebSockets - at ~49 MB of RAM at boot (~66 MB under load). Runs under Bun's runtime via a Bun-native server (`Bun.serve` + built-in WebSockets); the same CLI on Node uses the node:http server.\n\n## Embedding (Node or browser)\n\n```ts\nimport { createBackend } from 'tinbase'\n\nconst backend = await createBackend({\n  \u002F\u002F dataDir: 'idb:\u002F\u002Fmy-app'  \u003C- browser persistence\n  migrations: [{ name: '20240101000000_init', sql: 'create table notes (...)' }],\n})\n\n\u002F\u002F supabase-js talks to it in-process - no HTTP server, no network\nconst supabase = createClient('http:\u002F\u002Flocalhost', backend.anonKey, {\n  global: { fetch: (input, init) => backend.fetch(new Request(input, init)) },\n})\n```\n\nNode-only helpers live in `tinbase\u002Fnode`:\n\n```ts\nimport { serve, FsStorageDriver, loadSupabaseProject } from 'tinbase\u002Fnode'\n\nconst project = await loadSupabaseProject(process.cwd())\nconst backend = await createBackend({ ...project, storageDriver: new FsStorageDriver('.\u002Ffiles') })\nconst server = await serve(backend, { port: 54321 })\n```\n\n## What's implemented\n\n| Service | Endpoint | Coverage |\n| --- | --- | --- |\n| REST (PostgREST) | `\u002Frest\u002Fv1` | select with embedded resources (to-one, to-many, many-to-many via junction, nested, `!inner`, aliases, hints, casts, JSON paths), all common filter operators incl. `or`\u002F`and` trees, full-text search, order\u002Flimit\u002Foffset (top-level and per-embed), `single`\u002F`maybeSingle`, `count`, insert\u002Fbulk insert, upsert (merge\u002Fignore), update, delete, `Prefer` handling, RPC (scalar, `setof`, void, filters on results), PostgREST-shaped errors |\n| Auth (GoTrue) | `\u002Fauth\u002Fv1` | email\u002Fpassword signup + sign-in, anonymous sign-in, session refresh with rotation, `getUser`, `updateUser`, sign-out, admin user CRUD (service key), GoTrue-shaped errors. JWTs are HS256 via WebCrypto; passwords are PBKDF2 |\n| Storage | `\u002Fstorage\u002Fv1` | bucket CRUD, upload (raw + multipart), download, public objects, signed URLs, signed upload URLs, list with folder entries, move\u002Fcopy, remove, size\u002FMIME limits. Metadata lives in `storage.objects` with RLS enforced; bytes go through a pluggable driver (fs in Node, memory anywhere) |\n| Edge Functions | `\u002Ffunctions\u002Fv1` | `supabase.functions.invoke()` - Supabase-style `Deno.serve(handler)` functions (with `Deno.env`) run unchanged, as do `export default` handlers; loaded from `supabase\u002Ffunctions\u002F\u003Cname>\u002Findex.{ts,js,mjs}` by the CLI or passed via `createBackend({ functions })`. Web-API functions work as-is; `npm:`\u002F`jsr:`\u002FURL imports still need bundling |\n| Queues (pgmq) | `pgmq.*` | Message queues via a pure-SQL pgmq subset (create\u002Fsend\u002Fread\u002Fpop\u002Fdelete\u002Farchive, visibility timeouts). Call from SQL or `supabase.schema('pgmq').rpc(...)`. No extension |\n| Cron | `cron.*` | Scheduled jobs, drop-in with pg_cron's API — `cron.schedule(name, '*\u002F5 * * * *', 'sql')` (also the `'N seconds'` form), `cron.unschedule(...)`, and the `cron.job` \u002F `cron.job_run_details` tables. Schedules match in **UTC**, like hosted pg_cron; an in-process scheduler runs due jobs and logs to `cron.job_run_details`. Jobs run while tinbase is up, with service-role privileges. No extension |\n| HTTP from SQL (pg_net) | `net.*` | `net.http_post` \u002F `net.http_get` \u002F `net.http_delete(...)` enqueue a request that an in-process worker sends, recording the reply in `net._http_response`. Lets a cron job or trigger call an Edge Function or any URL — the common Supabase `cron.schedule(..., $$ select net.http_post(...) $$)` pattern works unchanged. No extension |\n| Database Webhooks | config | Fire HTTP requests on table INSERT\u002FUPDATE\u002FDELETE with Supabase's exact payload (`type`\u002F`table`\u002F`schema`\u002F`record`\u002F`old_record`). Configured via `createBackend({ webhooks })`, `backend.webhooks.register()`, or `supabase\u002Fwebhooks.json`. Built on the CDC pipeline — no extension needed |\n| Studio (Admin UI) | `\u002F_\u002F` | A Supabase-Studio-style dashboard (React + Radix + Tailwind): Table Editor with full row CRUD, SQL editor, user management, bucket\u002Fobject CRUD, and a database overview. One self-contained HTML file (works in the single binary); log in with the service_role key |\n| Realtime | `\u002Frealtime\u002Fv1` | Phoenix protocol (v1 JSON and v2 array\u002Fbinary serializers), `postgres_changes` (INSERT\u002FUPDATE\u002FDELETE, filters) fed by triggers + `pg_notify`, broadcast (incl. binary payloads), presence. WebSocket server is a ~150-line RFC 6455 implementation - no `ws` dependency |\n\n### RLS works like real Supabase\n\nEvery REST\u002Fstorage request runs inside a transaction with `SET LOCAL role` and `request.jwt.claims`, so policies like this behave identically to hosted Supabase:\n\n```sql\ncreate policy \"own rows\" on todos\n  for all to authenticated\n  using (user_id = auth.uid()) with check (user_id = auth.uid());\n```\n\n## Studio\n\ntinbase ships with a built-in dashboard at [`\u002F_\u002F`](http:\u002F\u002F127.0.0.1:54321\u002F_\u002F) - the same shape as Supabase Studio:\n\n- **Table Editor** - browse tables with pagination and row counts; insert, edit, and delete rows (type-aware, primary-key based)\n- **SQL Editor** - run arbitrary SQL with result grids and Postgres error details\n- **Authentication** - list, create, delete users and reset passwords\n- **Storage** - create\u002Fdelete buckets, upload\u002Fdelete objects, toggle public access\n- **RLS Policies** - list, create, and drop policies per table\n- **Database** - stats, migrations, functions, and triggers\n\nIt is a React app compiled to a single self-contained HTML file, so it also works inside the single binary. Sign in with the `service_role` key printed at startup.\n\n## Extensions\n\nThe extensions Supabase enables by default are available out of the box, so migrations that call `uuid_generate_v4()`, `gen_random_uuid()`, `crypt()`, `citext`, `pg_trgm`, and friends just work: **uuid-ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch**. They live in the `extensions` schema (like hosted Supabase) and are on the search path, so both qualified and unqualified calls resolve.\n\n## Typed clients\n\nGenerate a Supabase-shaped `Database` type from the live schema, the same as `supabase gen types typescript`:\n\n```bash\ntinbase gen types typescript > database.types.ts\n```\n\n```ts\nimport { createClient } from '@supabase\u002Fsupabase-js'\nimport type { Database } from '.\u002Fdatabase.types'\nconst supabase = createClient\u003CDatabase>(url, anonKey)  \u002F\u002F fully typed queries\n```\n\nEmits Tables (Row\u002FInsert\u002FUpdate\u002FRelationships), Views, Functions, and Enums.\n\n## Footprint: tinbase vs PocketBase vs Supabase local\n\n![Memory footprint comparison](assets\u002Ffootprint.svg)\n\nMeasured on an Apple Silicon Mac (48 GB), macOS 15. Same workload for all three: boot with one migrated table, then 1,000 single-row inserts followed by 1,000 filtered list queries. Memory is physical footprint (`vmmap`) for native processes and the sum of `docker stats` for containers. Reproduce with [`bench\u002Ffootprint.ts`](bench\u002Ffootprint.ts); raw numbers in [`bench\u002Fresults.json`](bench\u002Fresults.json).\n\n| | tinbase (single binary) | tinbase (native) | tinbase (pg-mem) | tinbase (wasm) | PocketBase v0.39.5 | Supabase local |\n| --- | --- | --- | --- | --- | --- | --- |\n| Database | real Postgres 17 + RLS | real Postgres 17 + RLS | in-memory subset¹ | real Postgres (PGlite) + RLS | SQLite | Postgres 17 |\n| Runtime memory at boot | 49 MB | 59 MB | 71 MB | ~610 MB² | 15 MB | 1,441 MB |\n| Runtime memory after workload | 66 MB | 100 MB | 185 MB | ~640 MB² | 24 MB | 1,626 MB |\n| Data on disk (1k rows) | 39 MB | 39 MB | 0 (in-memory) | 40 MB | 7 MB | 70 MB |\n| Install size | 92 MB (58 MB binary + PG) | 36 MB³ | 3.6 MB³ | 27 MB³ | 30 MB | 2,291 MB⁴ |\n| Processes | 2 | 2 | 1 | 1 | 1 | 12 containers + Docker |\n| 1,000 inserts | 0.4 s | 0.5 s | 0.8 s | 0.8 s | 0.3 s | 1.1 s |\n| 1,000 filtered reads | 0.3 s | 0.4 s | 0.8 s | 0.9 s | 0.3 s | 1.0 s |\n\n¹ **pg-mem** is a pure-JS in-memory subset (local dev \u002F preview) — no RLS, cron, or pgmq (realtime\u002Fwebhooks work but deliver unfiltered), but a **3.6 MB install** and pure JS (no WASM), the lightest option for the browser. See [Engines](#engines).\n² 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. It does not shrink under load.\n³ Native: Postgres 17 binaries + `dist`. pg-mem: `dist` + `pg-mem`. Wasm: `dist` + `@electric-sql\u002Fpglite`. All exclude the Node runtime you already have.\n⁴ Sum of the Docker image sizes the default local stack runs, excluding Docker Desktop itself.\n\n**How to read this honestly:**\n\n- **vs Supabase local**: same SDK, same APIs, ~16-24x less memory (native engine \u002F single binary), ~25-65x smaller install, 2 processes instead of a 12-container stack, and boots in ~2 s instead of a minute. That's the entire point of the project.\n- **vs PocketBase**: the single binary lands in PocketBase's weight class - ~2.7x the RAM (66 vs 24 MB under load), one downloadable file, no runtime prerequisite - while running *real Postgres* (RLS, jsonb, FKs, triggers) behind Supabase's exact wire APIs, so your code and migration files move to hosted Supabase unchanged. PocketBase is still the lightest option if you don't need any of that.\n- The wasm engine trades memory for portability: its ~575-650 MB is PGlite's WASM heap (the API layers add single-digit MB), and it's the only engine that runs in a browser. On servers, use the native engine or single binary.\n\n## How complete is it?\n\nRough coverage of the supabase-js SDK surface, measured against what each sub-library can express (all \"supported\" claims are exercised by the test suite):\n\n| Module | Coverage | Supported | Missing |\n| --- | --- | --- | --- |\n| Database (`postgrest-js`) | ~85% | full filter grammar, embeds (to-one\u002Fto-many\u002Fm2m\u002Fnested\u002F`!inner`), JSON paths, upsert, count, single\u002FmaybeSingle, RPC | aggregates in select, full spread embeds, `.explain()`, `.csv()`, geojson |\n| Auth (`auth-js`) | ~80% | email\u002Fpassword, anonymous sign-in, OTP + magic links + password recovery (pluggable mailer), OAuth providers (Google\u002FGitHub presets + generic) with PKCE and identity linking, refresh rotation, user updates, admin CRUD | MFA, SSO\u002FSAML, phone auth |\n| Storage (`storage-js`) | ~80% | buckets, upload\u002Fdownload, signed URLs + signed uploads, list\u002Fmove\u002Fcopy\u002Fremove, size\u002FMIME limits | resumable (TUS) uploads, image transformations |\n| Realtime (`realtime-js`) | ~85% | postgres_changes with filters + **per-subscriber RLS filtering** (INSERT\u002FUPDATE), broadcast (incl. binary), presence, v1+v2 serializers | per-row DELETE RLS, private channel auth, DB-triggered broadcast |\n| Edge Functions (`functions-js`) | ~70% | `invoke()`; **Deno.serve\u002FDeno.env-style functions run unchanged** + export-default; auth context, project-dir loading | `npm:`\u002F`jsr:`\u002FURL import resolution, `supabase functions deploy` |\n| Type generation | ~85% | `tinbase gen types typescript` → `Database` type (Tables\u002FViews\u002FFunctions\u002FEnums\u002FRelationships) | composite-type args, multi-schema output |\n\n**Overall: roughly 80% of the supabase-js SDK surface - and ~90% of what a typical CRUD + auth + storage + realtime app actually calls.**\n\nBeyond the client SDK, the local platform features real projects depend 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 (`db reset` \u002F `db diff`). The remaining gaps are OAuth logins' provider variety, the Deno edge-function runtime, and pgvector (needs an extension binary).\n\n## Known gaps\n\n- `postgres_changes` applies RLS per subscriber for INSERT\u002FUPDATE (the row is re-checked by primary key as that user). DELETE events can't be re-queried (the row is gone), so they are delivered to authenticated\u002Fservice subscribers but not filtered per-row — hosted Supabase does this via WAL-level policy evaluation (WALRUS).\n- Spread embeds (`...rel(col)`) support flat column lists only; aggregate functions in `select` are not implemented.\n- Auth: OAuth works for any OAuth2\u002FOIDC provider (Google & GitHub have built-in presets; configure via `TINBASE_OAUTH_\u003CPROVIDER>_CLIENT_ID`\u002F`_CLIENT_SECRET`). Still missing: MFA, SSO\u002FSAML, phone auth. OTP\u002Fmagic-link\u002Frecovery emails go through a pluggable `mailer` (default logs to console).\n- `pg_notify` payloads cap at ~8 kB - realtime events for larger rows arrive with `record: null` and an `errors` entry, like Supabase's \"payload too large\".\n- One writer at a time: PGlite is single-connection, and the native engine currently serializes requests over one connection for parity (a connection pool is a straightforward future upgrade). Fine for dev tools and small apps, not for high-concurrency production.\n\n## Tests\n\nThe integration suite drives the real `@supabase\u002Fsupabase-js` against the backend (REST via in-process fetch, realtime over actual WebSockets); the full suite — **168 tests** — passes on both the wasm and native engines:\n\n```bash\nnpm test\n```\n\n## Roadmap\n\ntinbase aims to be a local, Docker-free replacement for `supabase start` where **almost everything just works**. The North Star, the in-scope\u002Fout-of-scope line, the current coverage table, and the phased plan live in [ROADMAP.md](ROADMAP.md).\n\n## Why\n\ntinbase was built for [lifo](https:\u002F\u002Flifo.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, realtime, no server). It is part of [RapidNative](https:\u002F\u002Frapidnative.com). That origin drives the architecture:\n\n1. Every service is a pure fetch handler, and the `wasm` engine is Postgres compiled to WASM (PGlite), so with it the whole backend can run **in-process inside a browser tab**.\n2. The same design makes a lighter Supabase for local dev and self-contained apps - `npx tinbase start` instead of Docker Compose.\n","tinbase 是一个轻量级、Supabase 兼容的本地后端，以单二进制形式提供真实 PostgreSQL 语义（含 RLS、Auth、Realtime 等核心能力），无需 Docker 或外部服务。它基于 PGlite（WASM 编译版 Postgres）、嵌入式原生 Postgres（macOS\u002FLinux）或 pg-mem（纯 JS 内存引擎）实现三引擎统一 API，完全兼容 supabase-js SDK 和 Supabase CLI 的迁移\u002F种子约定。适用于本地开发、快速原型验证及浏览器内嵌场景，尤其适合需离线运行、低运维开销且保持与托管 Supabase 行为一致的中小型项目。当前处于 Alpha 阶段，不建议用于生产环境。",2,"2026-07-09 02:30:25","CREATED_QUERY"]