tinbase 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,9 @@ A pure-JS, Docker-free Supabase backend built on [PGlite](https://pglite.dev) (P
6
6
 
7
7
  One command, one small binary, real Postgres with Row Level Security - and 1:1 with Supabase's APIs and migration conventions.
8
8
 
9
+ > [!WARNING]
10
+ > **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.
11
+
9
12
  ```
10
13
  npx tinbase start
11
14
  ```
@@ -52,7 +55,7 @@ tinbase migrate # apply pending migrations and exit
52
55
  tinbase status # list applied migrations
53
56
  tinbase keys # print anon / service_role keys
54
57
 
55
- -p, --port <n> port (default 54321)
58
+ -p, --port <n> port (default 54321; or TINBASE_PORT / PORT env)
56
59
  --dir <path> project dir containing supabase/ (default cwd)
57
60
  --data-dir <path> PGlite data dir (default <dir>/.tinbase/db)
58
61
  --jwt-secret <s> JWT secret (or TINBASE_JWT_SECRET)
@@ -67,7 +70,7 @@ tinbase keys # print anon / service_role keys
67
70
 
68
71
  Both engines run the identical bootstrap, migrations, RLS, and realtime CDC - the test suite passes on both (`TINBASE_TEST_ENGINE=native npm test`).
69
72
 
70
- ### Single-binary build (PocketBase-style)
73
+ ### Single-binary build
71
74
 
72
75
  ```bash
73
76
  npm run build:binary # requires bun; emits dist-bin/tinbase (~57 MB)
@@ -110,6 +113,8 @@ const server = await serve(backend, { port: 54321 })
110
113
  | REST (PostgREST) | `/rest/v1` | 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`/`and` trees, full-text search, order/limit/offset (top-level and per-embed), `single`/`maybeSingle`, `count`, insert/bulk insert, upsert (merge/ignore), update, delete, `Prefer` handling, RPC (scalar, `setof`, void, filters on results), PostgREST-shaped errors |
111
114
  | Auth (GoTrue) | `/auth/v1` | email/password 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 |
112
115
  | Storage | `/storage/v1` | bucket CRUD, upload (raw + multipart), download, public objects, signed URLs, signed upload URLs, list with folder entries, move/copy, remove, size/MIME limits. Metadata lives in `storage.objects` with RLS enforced; bytes go through a pluggable driver (fs in Node, memory anywhere) |
116
+ | Edge Functions | `/functions/v1` | `supabase.functions.invoke()` - functions are plain fetch handlers, loaded from `supabase/functions/<name>/index.{js,mjs,ts}` (default export) by the CLI or passed programmatically via `createBackend({ functions })`; each call receives the verified auth context and env keys |
117
+ | Studio (Admin UI) | `/_/` | A Supabase-Studio-style dashboard (React + Radix + Tailwind): Table Editor with full row CRUD, SQL editor, user management, bucket/object CRUD, and a database overview. One self-contained HTML file (works in the single binary); log in with the service_role key |
113
118
  | Realtime | `/realtime/v1` | Phoenix protocol (v1 JSON and v2 array/binary serializers), `postgres_changes` (INSERT/UPDATE/DELETE, filters) fed by triggers + `pg_notify`, broadcast (incl. binary payloads), presence. WebSocket server is a ~150-line RFC 6455 implementation - no `ws` dependency |
114
119
 
115
120
  ### RLS works like real Supabase
@@ -122,6 +127,22 @@ create policy "own rows" on todos
122
127
  using (user_id = auth.uid()) with check (user_id = auth.uid());
123
128
  ```
124
129
 
130
+ ## Studio
131
+
132
+ tinbase ships with a built-in dashboard at [`/_/`](http://127.0.0.1:54321/_/) - the same shape as Supabase Studio:
133
+
134
+ - **Table Editor** - browse tables with pagination and row counts; insert, edit, and delete rows (type-aware, primary-key based)
135
+ - **SQL Editor** - run arbitrary SQL with result grids and Postgres error details
136
+ - **Authentication** - list, create, delete users and reset passwords
137
+ - **Storage** - create/delete buckets, upload/delete objects, toggle public access
138
+ - **Database** - stats overview and applied migrations
139
+
140
+ It 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.
141
+
142
+ ## Extensions
143
+
144
+ The 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.
145
+
125
146
  ## Footprint: tinbase vs PocketBase vs Supabase local
126
147
 
127
148
  ![Memory footprint comparison](assets/footprint.svg)
@@ -156,18 +177,18 @@ Rough coverage of the supabase-js SDK surface, measured against what each sub-li
156
177
  | Module | Coverage | Supported | Missing |
157
178
  | --- | --- | --- | --- |
158
179
  | Database (`postgrest-js`) | ~85% | full filter grammar, embeds (to-one/to-many/m2m/nested/`!inner`), JSON paths, upsert, count, single/maybeSingle, RPC | aggregates in select, full spread embeds, `.explain()`, `.csv()`, geojson |
159
- | Auth (`auth-js`) | ~40% | email/password, anonymous sign-in, refresh rotation, user updates, admin CRUD | OAuth, magic links, OTP, MFA, SSO/SAML, PKCE, phone auth, email delivery |
180
+ | Auth (`auth-js`) | ~65% | email/password, anonymous sign-in, OTP + magic links + password recovery (pluggable mailer, console default), refresh rotation, user updates, admin CRUD | OAuth providers, MFA, SSO/SAML, PKCE, phone auth |
160
181
  | Storage (`storage-js`) | ~80% | buckets, upload/download, signed URLs + signed uploads, list/move/copy/remove, size/MIME limits | resumable (TUS) uploads, image transformations |
161
182
  | Realtime (`realtime-js`) | ~70% | postgres_changes with filters, broadcast (incl. binary), presence, v1+v2 serializers | RLS-filtered fan-out (WALRUS), private channel auth, DB-triggered broadcast |
162
- | Edge Functions (`functions-js`) | 0% | - | everything (`supabase.functions.invoke()`) |
183
+ | Edge Functions (`functions-js`) | ~60% | `invoke()` with JSON/text bodies, auth context, project-dir loading | Deno runtime compat, import maps, `supabase functions deploy` |
163
184
 
164
- **Overall: roughly 65% of the SDK surface - but ~90% of what a typical CRUD + auth + storage + realtime app actually calls.** The biggest real-world gaps are OAuth logins and edge functions.
185
+ **Overall: roughly 75% of the SDK surface - but ~90% of what a typical CRUD + auth + storage + realtime app actually calls.** The biggest real-world gaps are OAuth logins and edge functions.
165
186
 
166
187
  ## Known gaps
167
188
 
168
189
  - `postgres_changes` does not apply RLS to fan-out (all subscribers see change events); hosted Supabase filters via WALRUS.
169
190
  - Spread embeds (`...rel(col)`) support flat column lists only; aggregate functions in `select` are not implemented.
170
- - Auth: no OAuth providers, magic links, or OTP (endpoints accept and no-op); no email delivery.
191
+ - Auth: no OAuth providers, MFA, or SSO. OTP/magic-link/recovery emails go through a pluggable `mailer` (the default just logs them to the console).
171
192
  - `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".
172
193
  - 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.
173
194
 
@@ -181,5 +202,7 @@ npm test
181
202
 
182
203
  ## Why
183
204
 
184
- 1. A lighter Supabase for local dev and self-contained apps - `npx tinbase start` instead of Docker Compose.
185
- 2. The whole backend can run **in the browser** (PGlite WASM + fetch-handler architecture), e.g. inside [lifo.sh](https://lifo.sh).
205
+ tinbase was built for [lifo](https://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, realtime, no server). It is part of [RapidNative](https://rapidnative.com). That origin drives the architecture:
206
+
207
+ 1. Every service is a pure fetch handler and the default engine is Postgres compiled to WASM, so the whole backend can run **in-process inside a browser tab**.
208
+ 2. The same design makes a lighter Supabase for local dev and self-contained apps - `npx tinbase start` instead of Docker Compose.
@@ -0,0 +1,12 @@
1
+ import type { Database } from '../db/database.js';
2
+ import type { RequestContext } from '../types.js';
3
+ export declare class AdminApi {
4
+ private db;
5
+ constructor(db: Database);
6
+ handle(req: Request, ctx: RequestContext, url: URL): Promise<Response>;
7
+ private listTables;
8
+ private runSql;
9
+ private schemas;
10
+ private migrations;
11
+ private stats;
12
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Admin API (/admin/v1/*) backing tinbase studio at /_/.
3
+ * Every endpoint requires the service_role key. This is a thin, introspection-
4
+ * and-SQL surface; row CRUD goes through the normal PostgREST layer (with RLS
5
+ * bypassed by the service_role), so the studio uses the same paths an app does.
6
+ */
7
+ import { quoteIdent } from '../db/database.js';
8
+ function json(status, body) {
9
+ return new Response(JSON.stringify(body), {
10
+ status,
11
+ headers: { 'content-type': 'application/json; charset=utf-8' },
12
+ });
13
+ }
14
+ export class AdminApi {
15
+ db;
16
+ constructor(db) {
17
+ this.db = db;
18
+ }
19
+ async handle(req, ctx, url) {
20
+ if (ctx.role !== 'service_role') {
21
+ return json(403, { error: 'admin API requires the service_role key' });
22
+ }
23
+ const path = url.pathname.replace(/^\/admin\/v1\/?/, '');
24
+ const method = req.method.toUpperCase();
25
+ try {
26
+ if (path === 'tables' && method === 'GET')
27
+ return await this.listTables(url);
28
+ if (path === 'sql' && method === 'POST')
29
+ return await this.runSql(req);
30
+ if (path === 'stats' && method === 'GET')
31
+ return await this.stats();
32
+ if (path === 'schemas' && method === 'GET')
33
+ return await this.schemas();
34
+ if (path === 'migrations' && method === 'GET')
35
+ return await this.migrations();
36
+ return json(404, { error: `unknown admin endpoint: ${path}` });
37
+ }
38
+ catch (e) {
39
+ return json(500, { error: e instanceof Error ? e.message : String(e) });
40
+ }
41
+ }
42
+ async listTables(url) {
43
+ const schema = url.searchParams.get('schema') ?? 'public';
44
+ this.db.invalidateSchemaCache();
45
+ const info = await this.db.getSchemaInfo(schema);
46
+ const tables = [];
47
+ for (const t of info.tables.values()) {
48
+ const count = await this.db.query(`select count(*)::int as n from ${quoteIdent(schema)}.${quoteIdent(t.name)}`);
49
+ // foreign keys originating from this table, for column hints
50
+ const fks = info.foreignKeys
51
+ .filter((fk) => fk.srcSchema === schema && fk.srcTable === t.name)
52
+ .map((fk) => ({ columns: fk.srcColumns, target: `${fk.tgtSchema}.${fk.tgtTable}`, targetColumns: fk.tgtColumns }));
53
+ tables.push({
54
+ name: t.name,
55
+ primaryKey: t.primaryKey,
56
+ rowCount: count.rows[0]?.n ?? 0,
57
+ foreignKeys: fks,
58
+ columns: t.columns.map((c) => ({
59
+ name: c.name,
60
+ type: c.udtName,
61
+ nullable: c.isNullable,
62
+ hasDefault: c.hasDefault,
63
+ isPrimaryKey: c.isPrimaryKey,
64
+ })),
65
+ });
66
+ }
67
+ return json(200, { schema, tables });
68
+ }
69
+ async runSql(req) {
70
+ const body = (await req.json().catch(() => ({})));
71
+ if (!body.query?.trim())
72
+ return json(400, { error: 'query is required' });
73
+ const started = Date.now();
74
+ try {
75
+ const res = await this.db.query(body.query);
76
+ this.db.invalidateSchemaCache();
77
+ return json(200, {
78
+ rows: res.rows.slice(0, 2000),
79
+ rowCount: res.rows.length,
80
+ affectedRows: res.affectedRows ?? null,
81
+ ms: Date.now() - started,
82
+ });
83
+ }
84
+ catch (e) {
85
+ const pg = e;
86
+ return json(400, { error: pg.message, code: pg.code, detail: pg.detail, hint: pg.hint });
87
+ }
88
+ }
89
+ async schemas() {
90
+ const res = await this.db.query(`select schema_name as name from information_schema.schemata
91
+ where schema_name not in ('pg_catalog','information_schema','pg_toast')
92
+ order by schema_name`);
93
+ return json(200, { schemas: res.rows.map((r) => r.name) });
94
+ }
95
+ async migrations() {
96
+ const res = await this.db.query(`select version, name, applied_at from supabase_migrations.schema_migrations order by version`);
97
+ return json(200, { migrations: res.rows });
98
+ }
99
+ async stats() {
100
+ const one = async (sql) => (await this.db.query(sql)).rows[0]?.n ?? 0;
101
+ const users = await one(`select count(*)::int as n from auth.users`);
102
+ const buckets = await one(`select count(*)::int as n from storage.buckets`);
103
+ const objects = await one(`select count(*)::int as n from storage.objects`);
104
+ const migrations = await one(`select count(*)::int as n from supabase_migrations.schema_migrations`);
105
+ const tables = await one(`select count(*)::int as n from information_schema.tables where table_schema='public' and table_type='BASE TABLE'`);
106
+ const size = await this.db.query(`select pg_size_pretty(pg_database_size(current_database())) as s, version() as v`);
107
+ return json(200, {
108
+ users,
109
+ buckets,
110
+ objects,
111
+ migrations,
112
+ tables,
113
+ dbSize: size.rows[0]?.s ?? '?',
114
+ version: (size.rows[0]?.v ?? '').split(' on ')[0],
115
+ });
116
+ }
117
+ }
118
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"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"}