@supabase/lite 0.9.0 → 0.9.1-next.2

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.
Files changed (64) hide show
  1. package/FEATURES.md +176 -0
  2. package/LIMITATIONS.md +9 -6
  3. package/PATTERNS.md +66 -6
  4. package/README.md +16 -8
  5. package/STATUS.md +33 -20
  6. package/dist/{Connection-ZWTDByQ5.d.ts → Connection-f_d5HhQ0.d.ts} +301 -293
  7. package/dist/cli/index.js +127 -124
  8. package/dist/cli/lib.d.ts +1 -12
  9. package/dist/cli/lib.js +44 -39
  10. package/dist/db/fallback.d.ts +1 -1
  11. package/dist/db/postgres/PostgresConnection.js +18 -18
  12. package/dist/db/postgres/pglite/PgliteConnection.js +17 -17
  13. package/dist/index.d.ts +116 -40
  14. package/dist/index.js +181 -73
  15. package/dist/static/.vite/manifest.json +34 -2
  16. package/dist/static/assets/InterVariable-Dx4kXJAl.woff2 +0 -0
  17. package/dist/static/assets/InterVariable-Italic-DpCbqKDY.woff2 +0 -0
  18. package/dist/static/assets/SourceCodePro-Variable-BP8Zz55n.woff2 +0 -0
  19. package/dist/static/assets/SourceCodePro-Variable-Italic-eALmlzX7.woff2 +0 -0
  20. package/dist/static/assets/main-BY4iigay.css +1 -0
  21. package/dist/static/assets/main-BnQ-v4V9.js +199 -0
  22. package/dist/static/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
  23. package/dist/static/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
  24. package/dist/vite/index.d.ts +627 -31
  25. package/dist/vite/index.js +2 -2
  26. package/docs/auth/email.mdx +214 -0
  27. package/docs/auth/not-supported.mdx +57 -0
  28. package/docs/auth/overview.mdx +52 -0
  29. package/docs/auth/supported-flows.mdx +120 -0
  30. package/docs/cli/overview.mdx +112 -0
  31. package/docs/cli/telemetry.mdx +34 -0
  32. package/docs/compatibility.mdx +115 -0
  33. package/docs/database/backends.mdx +118 -0
  34. package/docs/database/data-api.mdx +90 -0
  35. package/docs/database/functions-triggers.mdx +93 -0
  36. package/docs/database/migrations.mdx +95 -0
  37. package/docs/database/overview.mdx +66 -0
  38. package/docs/database/postgres-sqlite-translation.mdx +130 -0
  39. package/docs/database/rls.mdx +161 -0
  40. package/docs/database/schemas.mdx +58 -0
  41. package/docs/index.mdx +49 -0
  42. package/docs/integrations/embedded.mdx +100 -0
  43. package/docs/integrations/frameworks.mdx +83 -0
  44. package/docs/integrations/vite.mdx +87 -0
  45. package/docs/llms.txt +52 -0
  46. package/docs/other/edge-functions.mdx +34 -0
  47. package/docs/other/realtime.mdx +22 -0
  48. package/docs/quickstart.mdx +150 -0
  49. package/docs/running.mdx +127 -0
  50. package/docs/storage/adapters.mdx +75 -0
  51. package/docs/storage/limitations.mdx +21 -0
  52. package/docs/storage/overview.mdx +88 -0
  53. package/docs/upgrade.mdx +108 -0
  54. package/package.json +5 -1
  55. package/skills/supalite/SKILL.md +6 -4
  56. package/dist/static/assets/main-1bwWb_1q.js +0 -40996
  57. package/dist/static/assets/main-BDsRycsc.css +0 -4045
  58. package/dist/static/fonts/CustomFont-Black.woff2 +0 -0
  59. package/dist/static/fonts/CustomFont-BlackItalic.woff2 +0 -0
  60. package/dist/static/fonts/CustomFont-Bold.woff2 +0 -0
  61. package/dist/static/fonts/CustomFont-BoldItalic.woff2 +0 -0
  62. package/dist/static/fonts/CustomFont-Book.woff2 +0 -0
  63. package/dist/static/fonts/CustomFont-BookItalic.woff2 +0 -0
  64. package/dist/static/fonts/CustomFont-Medium.woff2 +0 -0
@@ -0,0 +1,130 @@
1
+ ---
2
+ title: "Postgres → SQLite translation"
3
+ description: "How Supabase Lite auto-translates Postgres DDL to SQLite, and the exact errors you'll see when something isn't supported."
4
+ ---
5
+
6
+ import { Aside } from '@astrojs/starlight/components';
7
+
8
+ On the SQLite path, every SQL schema you write in `supabase/schemas/*.sql` or `supabase/migrations/*.sql` is Postgres DDL. Supabase Lite's translator (an extension of the Postgres deparser) rewrites it to SQLite on the fly at migration time: 1:1-compatible syntax passes through unchanged, constructs with a SQLite equivalent get rewritten (`SERIAL` → `INTEGER PRIMARY KEY AUTOINCREMENT`, `NOW()` → `datetime('now')`), Postgres-only decorators and privilege metadata (storage parameters, locking clauses, grants, `OWNER TO`) are silently dropped, and constructs with no SQLite counterpart (`LATERAL` joins, table inheritance, function grants) throw a descriptive error at translation time. This page is the reference for debugging translation-time surprises. It does not apply to PGlite/Postgres: those run your DDL as real Postgres, unmodified.
9
+
10
+ The full auto-generated 74-entry compatibility table ships in the package at `app/POSTGRES-SQLITE-COMPAT.md`. This page covers the parts you're most likely to hit: extensions, type mapping, column defaults, and `CHECK` constraint functions.
11
+
12
+ ## Example translation
13
+
14
+ ```sql
15
+ -- Postgres DDL
16
+ CREATE TABLE users (
17
+ id SERIAL PRIMARY KEY,
18
+ email VARCHAR(255) UNIQUE NOT NULL,
19
+ is_active BOOLEAN DEFAULT true,
20
+ tags TEXT[],
21
+ metadata JSONB,
22
+ created_at TIMESTAMP DEFAULT NOW()
23
+ );
24
+ ```
25
+
26
+ ```sql
27
+ -- Translated to SQLite
28
+ CREATE TABLE users (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ email TEXT UNIQUE NOT NULL CHECK (length(email) <= 255),
31
+ is_active INTEGER DEFAULT true CHECK (is_active IN (0, 1)),
32
+ tags TEXT CHECK (tags IS NULL OR (json_valid(tags) AND json_type(tags) = 'array')),
33
+ metadata TEXT CHECK (metadata IS NULL OR json_valid(metadata)),
34
+ created_at TEXT DEFAULT (datetime('now')) CHECK (created_at IS NULL OR datetime(created_at) IS NOT NULL)
35
+ ) STRICT;
36
+ ```
37
+
38
+ ## Extensions
39
+
40
+ On SQLite, `CREATE EXTENSION` is accepted only for `plpgsql`, `pgcrypto`, and `uuid-ossp`, and those declarations are removed from the translated DDL. The `plpgsql` declaration supports `pg_dump` schema prologues; PL/pgSQL support remains limited to [trigger functions that the translator can inline](/database/functions-triggers/). The UUID declarations support `gen_random_uuid()` and `uuid_generate_v4()` defaults, but no other extension APIs are provided. Creation modifiers such as `IF NOT EXISTS`, `WITH SCHEMA`, `VERSION`, and `CASCADE` are accepted for those three exact names; quoted names are case-sensitive.
41
+
42
+ `ALTER EXTENSION <accepted-name> SET SCHEMA ...` and `DROP EXTENSION` containing only accepted names are also removed so declarative `db diff` migrations can reconcile extension metadata. Every other create, update, member change, schema move, or drop fails during translation before the migration executes. PGlite and PostgreSQL run extension statements natively instead; see [Supabase's extension guide](https://supabase.com/docs/guides/database/extensions) for the native behavior.
43
+
44
+ ## Type mapping
45
+
46
+ | Postgres type / syntax | SQLite storage | Notes |
47
+ |---|---|---|
48
+ | `int2`, `smallint`, `int4`, `integer`, `int`, `int8`, `bigint` | `INTEGER` | |
49
+ | `serial`, `bigserial`, `smallserial` (and `serial2`/`4`/`8`) | `INTEGER` | Primary keys become `INTEGER PRIMARY KEY AUTOINCREMENT` |
50
+ | `float4`, `real`, `float8`, `double precision` | `REAL` | |
51
+ | `numeric`, `decimal` | `REAL` | Precision/scale emits a portable `CHECK` |
52
+ | `text`, `varchar(n)`, `char(n)`, `bpchar` | `TEXT` | Length-constrained forms emit `length(...) <= n` |
53
+ | `name` | `TEXT` | 63-character length check |
54
+ | `bytea` | `BLOB` | |
55
+ | `bool`, `boolean` | `INTEGER` | `0`/`1` with `CHECK (... IN (0, 1))` |
56
+ | `date` | `TEXT` | `date(...) IS NOT NULL` validation |
57
+ | `time`, `timetz` | `TEXT` | `time(...) IS NOT NULL` validation |
58
+ | `timestamp`, `timestamptz` | `TEXT` | `datetime(...) IS NOT NULL` validation |
59
+ | `interval` | `TEXT` | Stored as text; interval arithmetic is not emulated |
60
+ | `json`, `jsonb` | `TEXT` | `json_valid(...)` check |
61
+ | `uuid` | `TEXT` | Validates shape, normalizes to lowercase |
62
+ | `inet` | `TEXT` | Validates IPv4/IPv6 with optional CIDR prefix |
63
+ | `CREATE TYPE ... AS ENUM` | `TEXT` | `CHECK (... IN (...))` of allowed values |
64
+ | `<type>[]` / `_type` arrays | `TEXT` | JSON array storage |
65
+
66
+ **Not supported at all:** `oid`, `xid`, `xid8`, `cid`, `money`, `citext`, `cidr`, `macaddr`, `macaddr8`, `bit`/`bit varying`/`varbit`, geometric types (`point`, `line`, `lseg`, `box`, `path`, `polygon`, `circle`), `xml`, text-search types (`tsvector`, `tsquery`), range and multirange types, `reg*` catalog reference types, and internal/pseudo/handler types.
67
+
68
+ <Aside type="note">
69
+ On the Postgres path, columns typed as a domain with a `CAST(<domain> AS json)` (PostgREST "data representations") render through that cast on read and `RETURNING`. On SQLite, the known representation types (`color`, `unixtz`, `isodate`, `monetary`, `bytea_b64`) are handled by built-in field shims instead. See the full domain-formatting notes in the package's `STATUS.md` under "Translated Field Types" if you're using custom domains.
70
+ </Aside>
71
+
72
+ ## Column defaults
73
+
74
+ Only constant expressions and a small allow-list of functions are honored in `DEFAULT` clauses. Everything else fails at translation time.
75
+
76
+ **Honored:**
77
+
78
+ | Default expression | SQLite emission |
79
+ |---|---|
80
+ | Literals (`'pending'`, `true`, `0`, `NULL`) | As-is (booleans map to `0`/`1`) |
81
+ | `gen_random_uuid()`, `uuid_generate_v4()` | Inline `randomblob`-based UUID expression (RFC 4122 v4 shape) |
82
+ | `now()`, `current_timestamp` | `datetime('now')` |
83
+ | `current_date` | `date('now')` |
84
+ | `current_time` | `time('now')` |
85
+ | `random()` | `random()` |
86
+
87
+ **Not honored**: fails with `Function call "<name>" not supported`:
88
+
89
+ - `auth.uid()`, `auth.role()`, `auth.email()`, `auth.jwt()`: these read JWT claims and have no SQLite equivalent as a column default. (They *do* work inside RLS `USING`/`WITH CHECK`, where they're rewritten on the AST instead of evaluated as a scalar default; see [Row Level Security](/database/rls).)
90
+ - `nextval(...)`, `currval(...)`: sequences aren't modeled on SQLite.
91
+ - `clock_timestamp()`, `statement_timestamp()`, `txid_current()`, and other volatile catalog functions.
92
+ - Any user-defined function, or any function not in the allow-list above.
93
+
94
+ **Workaround for `default auth.uid()`:** drop the default, pass `user_id` from the client on insert, and enforce ownership with a `WITH CHECK` policy instead:
95
+
96
+ ```sql
97
+ -- Instead of:
98
+ user_id uuid not null default auth.uid() references auth.users (id),
99
+
100
+ -- Use:
101
+ user_id uuid not null references auth.users (id),
102
+ -- and rely on a WITH CHECK policy to bind the row to the caller.
103
+ ```
104
+
105
+ This limitation is SQLite-only. On PGlite/Postgres, `auth.uid()` works as a column default natively: the function is bootstrapped as part of connection setup.
106
+
107
+ ## CHECK constraint functions
108
+
109
+ `CHECK (...)` expressions are passed to SQLite as-is and evaluated by SQLite itself at write time. Only functions SQLite recognizes as valid are allowed. Postgres-only scalars commonly reached for in `CHECK` are rejected by the translator with the same `Function call "<name>" not supported` error:
110
+
111
+ - `trim`, `btrim`, `ltrim`, `rtrim`: use a literal/operator comparison instead, or move the validation to the app layer.
112
+ - `length` (on text): SQLite has `length()`, but the translator currently rejects it specifically inside `CHECK`.
113
+ - `lower`, `upper`: same story, these work in `WHERE` but not currently inside `CHECK`.
114
+ - Any custom function or extension scalar (`regexp_replace`, etc.).
115
+
116
+ **Workaround:** keep `CHECK` constraints to literal/operator comparisons (`col <> ''` is fine, `length(col) > 0` is not) and validate richer rules in the app layer or via an RLS `WITH CHECK` predicate.
117
+
118
+ On the PGlite/Postgres path, all of these functions work exactly as they do in upstream Postgres, since `CHECK` constraints run as real Postgres expressions there.
119
+
120
+ ## SQLite version floor
121
+
122
+ Introspection (the schema snapshot query that runs on every request) uses `ORDER BY` inside an aggregate (`json_group_array(x ORDER BY y)`), which requires **SQLite 3.44.0 or newer**. Below that, every `select`/`insert`/`update`/`delete` fails with `near "ORDER": syntax error`, not just migrations. `node:sqlite`, `bun:sqlite`, and the Cloudflare workerd target all meet this floor on current releases; the one historical hazard is Bun 1.1.x, which shipped SQLite 3.43.2 (below the floor) before Bun 1.3.x moved to 3.51.0.
123
+
124
+ ## See also
125
+
126
+ - [Row Level Security](/database/rls): how `CREATE POLICY` and `ENABLE ROW LEVEL SECURITY` DDL are extracted rather than emitted as SQLite DDL.
127
+ - [Functions & triggers](/database/functions-triggers): the trigger-function-body translation this page's DDL translator feeds into.
128
+ - [Backends](/database/backends): none of this applies on PGlite/Postgres, your DDL runs unmodified there.
129
+ - [Supabase declarative schemas](https://supabase.com/docs/guides/local-development/declarative-database-schemas): the upstream workflow this DDL is authored for before translation.
130
+ - [Postgres data types](https://www.postgresql.org/docs/current/datatype.html): the upstream reference for every type mapped above.
@@ -0,0 +1,161 @@
1
+ ---
2
+ title: "Row Level Security"
3
+ description: "RLS works on every Supabase Lite backend; SQLite enforces it at the application layer with specific caveats."
4
+ ---
5
+
6
+ import { Aside } from '@astrojs/starlight/components';
7
+
8
+ Row Level Security works the same way you'd expect from Supabase on every backend: default-deny when RLS is enabled with no matching policies, `USING`/`WITH CHECK` clauses, `PERMISSIVE`/`RESTRICTIVE` combination rules, per-command policy targeting (`FOR SELECT/INSERT/UPDATE/DELETE/ALL`), role targeting (`TO anon, authenticated, PUBLIC`), and the `auth.uid()`/`auth.jwt()` placeholders. If you already know Postgres RLS, none of that behavior needs re-explaining here: see the [Supabase RLS guide](https://supabase.com/docs/guides/database/postgres/row-level-security) for the concepts.
9
+
10
+ What differs is *how* it's enforced, and a handful of SQLite-specific gaps.
11
+
12
+ ## Enforcement model
13
+
14
+ - **SQLite:** policies are extracted from the Postgres DDL during translation and enforced at the application layer by rewriting the PostgREST AST before query execution. `USING` conditions merge into the query's `WHERE` clause; `WITH CHECK` conditions are evaluated in-memory against the proposed INSERT/UPDATE values.
15
+ - **PGlite / Postgres:** native Postgres RLS. Each request runs inside a transaction with `SET LOCAL role` and `set_config('request.jwt.claim.sub', ...)`, so the database enforces policies directly, identical to hosted Supabase.
16
+
17
+ `auth.role()` resolves from the JWT `role` claim (defaulting to `"anon"` if missing, matching PostgREST). `auth.uid()` resolves to the JWT `sub` claim. `auth.jwt()` exposes the full payload. These placeholders are supported in policy expressions on both dialects.
18
+
19
+ On `sqlite-postgres`, runtime metadata is persisted to the disposable cache file `supabase/.temp/.runtime-metadata-cache.json`. `lite start` accepts only migration-derived entries and trusts only the ordered SQL recorded in `supabase_migrations.schema_migrations`; migration files and `schemas/*.sql` are never startup inputs. Missing, corrupt, old, stale, or tampered caches rebuild automatically from applied history, including the complete RLS table/policy lifecycle. A rename carries RLS and policies to the new table name; dropping a table removes them, even if a table with the same name is created later.
20
+
21
+ Rebuilds also replay the physical migrations in memory and compare their raw SQLite structure with the live database. Invalid/null recorded statements or out-of-band structural changes stop startup with a `lite db reset` hint. Introspection only proves equality and never generates policies. `lite dev` and the Vite plugin instead recompute metadata from the declarative schema they apply; use exactly one backend process per project, and capture declarative changes in migrations before switching to `lite start`.
22
+
23
+ <Aside type="caution">
24
+ While [admin mode](/running#admin-mode) is on (the default for loopback `lite dev`, `lite start`, and Vite dev listeners), requests that carry no credential at all run as `service_role` and skip RLS entirely. To exercise your policies, send an `apikey`:
25
+
26
+ ```bash
27
+ # anon
28
+ curl -H "apikey: $PUBLISHABLE_KEY" 127.0.0.1:54321/rest/v1/notes
29
+ # authenticated — the apikey is required IN ADDITION to the user JWT
30
+ curl -H "apikey: $PUBLISHABLE_KEY" -H "Authorization: Bearer $USER_JWT" \
31
+ 127.0.0.1:54321/rest/v1/notes
32
+ ```
33
+
34
+ Neither request is ever elevated, so both behave exactly as they will in production. A bearer token alone (no `apikey`) is rejected with 401 before RLS is reached — opaque keys are only sourced from `apikey`, matching upstream. Or start with `--no-admin`.
35
+ </Aside>
36
+
37
+ <Aside>
38
+ Every RLS behavior listed as supported here is regression-tested against both SQLite and PGlite in the package's test suite, so a silent SQLite-only bypass would fail CI rather than ship quietly.
39
+ </Aside>
40
+
41
+ ## SQLite-specific caveats
42
+
43
+ <Aside type="caution">
44
+ These four apply to the SQLite path only. None of them apply on PGlite/Postgres: RLS there is native Postgres RLS with no emulation gaps.
45
+ </Aside>
46
+
47
+ ### No `DEFAULT auth.uid()` on columns
48
+
49
+ SQLite can't evaluate `auth.uid()` as a column default (it's a JWT-bound function, not a SQLite builtin). Drop the default, pass `user_id` from the client on insert, and let `WITH CHECK` enforce ownership server-side:
50
+
51
+ ```sql
52
+ -- Instead of:
53
+ user_id uuid not null default auth.uid() references auth.users (id),
54
+
55
+ -- Use:
56
+ user_id uuid not null references auth.users (id),
57
+ -- and rely on a WITH CHECK policy to bind the row to the caller.
58
+ ```
59
+
60
+ ```ts
61
+ const { data: { session } } = await supabase.auth.getSession();
62
+ await supabase.from("<thing>").insert({
63
+ user_id: session.user.id,
64
+ // ...
65
+ });
66
+ ```
67
+
68
+ ### Subquery `WITH CHECK` on `INSERT` throws
69
+
70
+ `WITH CHECK` expressions containing subqueries (`user_id IN (SELECT ...)`, `EXISTS (...)`) can't be evaluated in-memory, so SQLite throws rather than silently passing or failing. This is the classic "child belongs to a parent owned by the caller" pattern:
71
+
72
+ ```sql
73
+ -- Fails on SQLite (subquery in WITH CHECK):
74
+ create policy milestones_insert_own on public.milestones
75
+ for insert to authenticated
76
+ with check (
77
+ exists (
78
+ select 1 from public.goals
79
+ where goals.id = milestones.goal_id
80
+ and goals.user_id = auth.uid()
81
+ )
82
+ );
83
+ ```
84
+
85
+ **Workaround:** denormalize the authorizing column onto the child table and compare directly:
86
+
87
+ ```sql
88
+ create policy milestones_insert_own on public.milestones
89
+ for insert to authenticated
90
+ with check (auth.uid() = user_id);
91
+ ```
92
+
93
+ This also matches [Supabase's own performance recommendation](https://supabase.com/docs/guides/database/postgres/row-level-security#tips): subquery policies execute per row even on real Postgres.
94
+
95
+ ### `UPSERT` applies `INSERT` policies only
96
+
97
+ Postgres applies `UPDATE` policies on conflict resolution during an upsert. Supabase Lite applies the `INSERT WITH CHECK` policy to all upsert rows regardless of outcome, because conflict resolution is unknown at the point the policy has to be evaluated.
98
+
99
+ ### `FORCE ROW LEVEL SECURITY` is accepted and ignored
100
+
101
+ `ALTER TABLE ... FORCE ROW LEVEL SECURITY` (and `NO FORCE`) is parsed and then ignored. On Postgres it only controls whether the table owner is subject to the table's policies; Supabase Lite has no table-owner exemption to begin with, so there is nothing to toggle. As on Postgres, `FORCE` alone does not enable RLS: only `ENABLE ROW LEVEL SECURITY` does, and only `DISABLE ROW LEVEL SECURITY` turns it back off. Policies may exist while RLS is disabled or has never been enabled; they remain stored but inert until RLS is enabled.
102
+
103
+ ### `RETURNING` isn't checked against the `SELECT` policy
104
+
105
+ Postgres errors if a `RETURNING` clause would expose a row not visible under the table's `SELECT` policy. Supabase Lite doesn't check this on SQLite; `RETURNING` returns whatever the mutation touched.
106
+
107
+ ## `DROP POLICY` / `ALTER POLICY`
108
+
109
+ Standalone `DROP POLICY [IF EXISTS]` and `ALTER POLICY` are handled during RLS collection rather than emitted as SQLite DDL, so they work on the SQLite path too, in both imperative migrations and declarative schemas:
110
+
111
+ - `DROP POLICY [IF EXISTS] <name> ON <table>` (schema-qualified names included) removes the policy from the collected RLS registry.
112
+ - `ALTER POLICY` updates the `TO` / `USING` / `WITH CHECK` clauses. The command type (`SELECT`/`INSERT`/…) and the `PERMISSIVE`/`RESTRICTIVE` flag can't be changed by `ALTER POLICY`, matching Postgres.
113
+ - `ALTER POLICY ... RENAME TO <new_name>` renames the policy in place.
114
+
115
+ ```sql
116
+ alter policy "select own" on todos using (auth.uid() = user_id and archived = false);
117
+ alter policy "select own" on todos rename to "select own todos";
118
+ drop policy if exists "select own todos" on todos;
119
+ ```
120
+
121
+ Both statements replay in order alongside every other RLS-affecting statement collected from your migrations and `schemas/*.sql`, so editing or removing a `CREATE POLICY` directly in a declarative schema file still works exactly as before. Dropping or altering a policy that doesn't exist errors, same as Postgres. On PGlite/Postgres these statements run natively and always have.
122
+
123
+ ## Per-user multi-tenant recipe
124
+
125
+ The most common shape, "each user sees only their own rows," works identically on SQLite, PGlite, and Postgres:
126
+
127
+ ```sql
128
+ create table <thing> (
129
+ id uuid primary key default gen_random_uuid(),
130
+ user_id uuid not null references auth.users(id) on delete cascade,
131
+ -- domain columns
132
+ created_at timestamptz not null default now()
133
+ );
134
+
135
+ alter table <thing> enable row level security;
136
+
137
+ create policy "select own" on <thing> for select to authenticated using (auth.uid() = user_id);
138
+ create policy "insert own" on <thing> for insert to authenticated with check (auth.uid() = user_id);
139
+ create policy "update own" on <thing> for update to authenticated using (auth.uid() = user_id) with check (auth.uid() = user_id);
140
+ create policy "delete own" on <thing> for delete to authenticated using (auth.uid() = user_id);
141
+ ```
142
+
143
+ The client supplies `user_id` on insert since SQLite has no `DEFAULT auth.uid()`:
144
+
145
+ ```ts
146
+ const { data: { session } } = await supabase.auth.getSession();
147
+ await supabase.from("<thing>").insert({
148
+ user_id: session.user.id,
149
+ // ...
150
+ });
151
+ ```
152
+
153
+ ## PGlite / Postgres auto-setup
154
+
155
+ When any table has `ENABLE ROW LEVEL SECURITY`, Supabase Lite automatically creates the `anon`, `authenticated`, and `service_role` roles if they're missing (`service_role` gets `BYPASSRLS`) and grants default privileges on all tables and sequences in the relevant schemas. No manual `CREATE ROLE` or `GRANT` statements needed.
156
+
157
+ ## See also
158
+
159
+ - [Backends](/database/backends): switching drivers moves you from application-layer RLS emulation to native Postgres RLS.
160
+ - [Postgres → SQLite translation](/database/postgres-sqlite-translation): how `CREATE POLICY` and `ENABLE ROW LEVEL SECURITY` DDL gets parsed and extracted.
161
+ - [Supabase RLS guide](https://supabase.com/docs/guides/database/postgres/row-level-security): the base concepts this page assumes.
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: "Declarative schemas"
3
+ description: "Writing Postgres DDL in supabase/schemas/*.sql, and how it maps onto SQLite's single-namespace model."
4
+ ---
5
+
6
+ Supabase Lite supports Supabase's declarative schema workflow: write Postgres DDL in `supabase/schemas/*.sql`, and the tooling diffs it against the live database to produce migrations. This page covers what's identical and what changes because the SQLite path has no real schema/namespace concept. For the base declarative-schema workflow, see the [Supabase declarative schemas guide](https://supabase.com/docs/guides/local-development/declarative-database-schemas).
7
+
8
+ ## Writing schemas
9
+
10
+ Write plain Postgres DDL:
11
+
12
+ ```sql
13
+ -- supabase/schemas/schema.sql
14
+ create table todos (
15
+ id uuid primary key default gen_random_uuid(),
16
+ user_id uuid not null references auth.users(id) on delete cascade,
17
+ title text not null,
18
+ done boolean default false,
19
+ created_at timestamptz not null default now()
20
+ );
21
+
22
+ alter table todos enable row level security;
23
+
24
+ create policy "select own" on todos for select to authenticated using (auth.uid() = user_id);
25
+ create policy "insert own" on todos for insert to authenticated with check (auth.uid() = user_id);
26
+ ```
27
+
28
+ Point `config.toml` at the file(s):
29
+
30
+ ```toml
31
+ [db.migrations]
32
+ schema_paths = ["./schemas/schema.sql"]
33
+ ```
34
+
35
+ `lite dev` and the Vite plugin watch `schemas/*.sql` and re-apply on change. `lite db diff` compares the live DB against these files and emits a migration for the delta.
36
+
37
+ ## SQLite is single-schema
38
+
39
+ Postgres supports `CREATE SCHEMA` and cross-schema references (`billing.invoices`, `SET search_path`) with real namespace isolation. SQLite has no schema concept: every object lives in one flat namespace. What you get on the SQLite path depends on which driver you're using:
40
+
41
+ - On `sqlite-postgres` (the default for declarative Postgres DDL), `CREATE SCHEMA` and schema-qualified tables are accepted but flattened into a single physical namespace. PostgREST records the schema each table came from, so `schema()` / the `Accept-Profile` header routes to the right tables. There's no true isolation, though: two schemas with a same-named table collide once the prefix is stripped.
42
+ - On the bare `sqlite` driver (raw SQLite DDL, no translation), there's no schema handling at all. Everything is `public`.
43
+
44
+ Either way, this is not real Postgres multi-schema. `auth.users`, `storage.objects`, and other system-schema tables are handled internally by Supabase Lite; you don't need to (and can't) create competing user-defined schemas with the same names. The protected `auth` and `storage` schemas are not exposed through the Data API by default. `storage` can be added explicitly to `api.schemas` when direct metadata endpoints are intentional; `auth` should remain private.
45
+
46
+ See [Postgres → SQLite translation](/database/postgres-sqlite-translation) for the full list of DDL constructs that don't survive the SQLite path.
47
+
48
+ On PGlite and Postgres, multiple schemas and `schema()` work exactly as they do on hosted Supabase. See [Backends](/database/backends).
49
+
50
+ ## What else changes on SQLite
51
+
52
+ DDL you write here goes through the same translator described in [Postgres → SQLite translation](/database/postgres-sqlite-translation): types get mapped (`SERIAL` → `INTEGER PRIMARY KEY AUTOINCREMENT`, `JSONB` → `TEXT` with a `json_valid()` check, and so on), column defaults are restricted to a small allow-list, and RLS statements are extracted rather than emitted as SQLite DDL (see [Row Level Security](/database/rls)). Postgres-only constructs with no SQLite equivalent, such as `LATERAL` joins, table inheritance, and range types, throw a descriptive error at translation time rather than silently dropping.
53
+
54
+ ## See also
55
+
56
+ - [Migrations](/database/migrations): how the declarative diff turns `schemas/*.sql` changes into an applied migration.
57
+ - [Postgres → SQLite translation](/database/postgres-sqlite-translation): the type-mapping and defaults reference.
58
+ - [Backends](/database/backends): schemas and `schema()` work unmodified on PGlite/Postgres.
package/docs/index.mdx ADDED
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: "Introduction"
3
+ description: "What Supabase Lite is, what's supported today, and how it relates to hosted Supabase."
4
+ ---
5
+
6
+ import { Aside, Card, CardGrid } from '@astrojs/starlight/components';
7
+ import banner from '../../../../.github/assets/banner.png';
8
+
9
+ ![Supabase Lite banner](/banner.png)
10
+
11
+ Supabase Lite (`@supabase/lite`) is a lightweight, TypeScript-native reimplementation of Supabase features while being database agnostic and mainly focused on SQLite. It ships a PostgREST-compatible Data API and a GoTrue-compatible Auth API, so `@supabase/supabase-js` works unchanged against it for the methods it supports.
12
+
13
+ SQLite is the primary database (`bun:sqlite`, `node:sqlite`, SQLite WASM, Cloudflare D1/DO), with PGlite and Postgres available as alternative backends. Everything runs in one process: no Docker, no separate database container. You write Postgres DDL in `supabase/schemas/*.sql`, and on the SQLite path it gets translated on the fly. Once your project outgrows SQLite, you can seamlessly upgrade to Supabase.
14
+
15
+ Supabase Lite supplements Supabase rather than replacing it. It implements roughly 60% of the most-used `supabase-js` surface, the subset most useful for fast prototyping, with a clear upgrade path to hosted or self-hosted Supabase when a project outgrows it.
16
+
17
+ <Aside type="caution">
18
+ Supabase Lite is pre-1.0 / alpha. APIs, config shape, and the on-disk format may change between releases. Not for production use yet.
19
+ </Aside>
20
+
21
+ ## Scope at a glance
22
+
23
+ | Product | Status | Notes |
24
+ |---|---|---|
25
+ | Data API | Supported | 53/74 `supabase-js` query-builder methods on SQLite, 72/74 on Postgres/PGlite. Core CRUD, filters, embedding, and RLS all work. See [Compatibility](/compatibility). |
26
+ | Auth | Partial | 13 backend endpoints implemented (email/password, OTP, sessions, password recovery, OAuth sign-in for `github`/`google`). Other OAuth providers, anonymous sign-in, manual identity linking, admin API, and MFA are planned. See [Auth overview](/auth/overview). |
27
+ | Storage | Experimental | 20/20 `supabase-js` methods implemented, gated behind `EXPERIMENTAL_STORAGE`. Role-based access and RLS on storage objects are still pending. See [Storage overview](/storage/overview). |
28
+ | Realtime | Not yet | Config schema exists; no WebSocket server or channels yet. See [Realtime](/other/realtime). |
29
+ | Edge Functions | Not yet | Config schema exists; no runtime or `/functions/v1` routes yet. See [Edge Functions](/other/edge-functions). |
30
+
31
+ Compatibility is measured against the `@supabase/supabase-js` surface, not raw Postgres wire-protocol access. Direct SQL clients, `psql`, and third-party Postgres tooling are not a target.
32
+
33
+ <Aside type="note">
34
+ These are capability-level rollups. The package installed in your project ships an exact, per-method matrix in `STATUS.md` (and a companion `FEATURES.md` with effort/blocker notes for gaps) that updates every time you run `npm install`. Treat that file as the source of truth for exact counts.
35
+ </Aside>
36
+
37
+ For the concepts Supabase Lite implements unchanged from upstream (the general Supabase architecture, product list, and terminology), see the [Supabase docs](https://supabase.com/docs) and the [architecture guide](https://supabase.com/docs/guides/getting-started/architecture). This site only documents where Supabase Lite diverges.
38
+
39
+ <CardGrid>
40
+ <Card title="Quickstart">
41
+ Install, scaffold a project, and run your first query. [Read more →](/quickstart/)
42
+ </Card>
43
+ <Card title="Compatibility">
44
+ Capability-level support matrix across SQLite, PGlite, and Postgres. [Read more →](/compatibility/)
45
+ </Card>
46
+ <Card title="Database">
47
+ Backends, the Data API, RLS, schemas, and migrations. [Read more →](/database/overview/)
48
+ </Card>
49
+ </CardGrid>
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: "Embedded / programmatic"
3
+ description: "Use the App class directly in Bun, Node, browser, or edge runtimes, without the CLI."
4
+ ---
5
+
6
+ import { Tabs, TabItem } from '@astrojs/starlight/components';
7
+
8
+ Supabase Lite has no required CLI dependency at runtime. The `App` class is a Web-API-compliant request handler (`app.fetch`) you can construct directly in Bun, Node, the browser, or an edge runtime, and either call in-process or serve over HTTP yourself.
9
+
10
+ ## Constructing an App
11
+
12
+ ```ts
13
+ import { App } from "@supabase/lite";
14
+ import { createConnection } from "@supabase/lite/sqlite";
15
+
16
+ const connection = await createConnection({ url: "file:./data.db" });
17
+ const app = new App({ connection, auth: { enabled: true } });
18
+
19
+ // optionally apply schema on boot
20
+ const schema = await Bun.file("./schema.sql").text();
21
+ await app.connection.createMigrator(schema).migrate();
22
+
23
+ export default app; // app.fetch handles requests
24
+ ```
25
+
26
+ `App` takes a config object whose required field is `connection` (a `Connection` or a `Promise<Connection>`, useful when the connection itself needs an async setup step, as with Cloudflare's `getPlatformProxy`). The rest of the config is the same schema `config.toml` validates against (`auth`, `db`, `api`, `studio`, ...), plus an `options` object for `server` and `drivers` overrides. The `README.md` shipped in the package documents the full config shape.
27
+
28
+ ## Two ways to use it
29
+
30
+ <Tabs>
31
+ <TabItem label="In-process (no network)">
32
+ Skip HTTP entirely and call the app's own client, which routes through `app.fetch` internally in the same process:
33
+
34
+ ```ts
35
+ const client = app.getClient();
36
+ const { data } = await client.from("todos").select("*");
37
+ ```
38
+
39
+ Same `@supabase/supabase-js` API, no HTTP round trip. Useful for scripts, tests, and server-side code that doesn't need a network boundary. `getClient()` defaults to the configured `auth.publishable_key` (override with `getClient({ apikey })`). Disable API-key enforcement entirely with `options.server.apiKeys: false`, or supply a custom key→role resolver via `options.server.apiKeys.resolver`. See [API keys](/auth/overview#api-keys).
40
+ </TabItem>
41
+ <TabItem label="Over HTTP">
42
+ Serve `app.fetch` with any Web-API-compatible server and point `@supabase/supabase-js` at the URL, same as talking to a hosted Supabase project:
43
+
44
+ ```ts
45
+ // Bun
46
+ const server = Bun.serve({
47
+ hostname: "127.0.0.1",
48
+ fetch(request) {
49
+ return app.fetch(request, {
50
+ peerAddress: server.requestIP(request)?.address ?? null,
51
+ });
52
+ },
53
+ });
54
+ ```
55
+
56
+ ```ts
57
+ // Node, via @hono/node-server
58
+ import { serve } from "@hono/node-server";
59
+ serve({
60
+ port: 3000,
61
+ hostname: "127.0.0.1",
62
+ fetch(request, env) {
63
+ return app.fetch(request, {
64
+ peerAddress: env.incoming.socket.remoteAddress ?? null,
65
+ });
66
+ },
67
+ });
68
+ ```
69
+
70
+ ```ts
71
+ import { createClient } from "@supabase/supabase-js";
72
+ const client = createClient("http://127.0.0.1:3000", "<sb_publishable_...>");
73
+ ```
74
+
75
+ `AppRequestContext.peerAddress` is trusted transport metadata, not a forwarded header. Supply it only from the socket object owned by your server adapter. If the adapter cannot determine the peer, pass `null` so admin mode fails closed. Omitting the context preserves the in-process/edge fallback, where only the loopback hostname guard is available.
76
+ </TabItem>
77
+ </Tabs>
78
+
79
+ ## Drivers per runtime
80
+
81
+ `@supabase/lite/sqlite` resolves to the right SQLite binding automatically based on the runtime's export condition (`bun`, `node`, `browser`); you don't need to pick a driver yourself for local SQLite use. For other backends, import the matching subpath explicitly:
82
+
83
+ | Runtime/DB | Import |
84
+ |---|---|
85
+ | Bun | `@supabase/lite/sqlite` (resolves to `bun:sqlite`) |
86
+ | Node.js ≥ 22 | `@supabase/lite/sqlite` (resolves to `node:sqlite`) |
87
+ | Browser | `@supabase/lite/sqlite` (resolves to `@sqlite.org/sqlite-wasm`) |
88
+ | Cloudflare Workers (D1) | `@supabase/lite/workerd` |
89
+ | PGlite | `@supabase/lite/pglite` |
90
+ | Postgres | `@supabase/lite/postgres` |
91
+ | libsql / Turso | `@supabase/lite/libsql` |
92
+
93
+ See [Database backends](/database/backends) for how backend choice affects feature support (RLS enforcement, `rpc()`, ranges, and so on).
94
+
95
+ ## Related
96
+
97
+ - [Running Supabase Lite](/running): how embedding compares to `lite dev`/`lite start`/the Vite plugin.
98
+ - [Database backends](/database/backends): connection config per backend.
99
+ - [Frameworks](/integrations/frameworks): the Next.js and Cloudflare Workers patterns built on this `App` API.
100
+ - Upstream: [Supabase local development](https://supabase.com/docs/guides/local-development). There's no embedded/programmatic mode in Supabase itself; this is a Supabase Lite-specific concept for running the backend inside your own process instead of Docker.
@@ -0,0 +1,83 @@
1
+ ---
2
+ title: "Framework guides"
3
+ description: "Running Supabase Lite inside Next.js, Cloudflare Workers, and Docker."
4
+ ---
5
+
6
+ Supabase Lite mounts as a request handler (`app.fetch`), so it fits into most frameworks without a custom server.
7
+
8
+ ## Next.js App Router
9
+
10
+ Run Supabase Lite in the same `next dev` process via App Router catch-all route handlers, no webpack plugin or custom server needed.
11
+
12
+ Add `@supabase/lite` to `serverExternalPackages` so Next treats it as server-only:
13
+
14
+ ```ts
15
+ // next.config.ts
16
+ import type { NextConfig } from "next";
17
+
18
+ const nextConfig: NextConfig = {
19
+ serverExternalPackages: ["@supabase/lite"],
20
+ };
21
+
22
+ export default nextConfig;
23
+ ```
24
+
25
+ Create one server singleton that boots the app in-process (cache the boot promise on `globalThis` so hot-reload doesn't reboot it), then re-export it from three catch-all routes:
26
+
27
+ ```txt
28
+ app/auth/v1/[[...path]]/route.ts
29
+ app/rest/v1/[[...path]]/route.ts
30
+ app/%5Fsystem/[[...path]]/route.ts
31
+ ```
32
+
33
+ ```ts
34
+ export const runtime = "nodejs";
35
+ export { DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT } from "@/lib/server";
36
+ ```
37
+
38
+ The `%5Fsystem` folder name is intentional: Next treats folders starting with `_` as private, so the URL-encoded form is what produces a public `/_system` route.
39
+
40
+ What's different from a standard Supabase + Next.js setup: no Docker, no `supabase start`, and no separate origin, since the client just talks to `window.location.origin`.
41
+
42
+ ## Cloudflare Workers
43
+
44
+ D1 is reached through `@supabase/lite/workerd`. Two schema-management approaches work:
45
+
46
+ - **CLI-managed:** a `supabase/` directory with declarative schemas and seed SQL, applied against the D1 binding through the `lite` CLI.
47
+ - **Wrangler-native:** no Supabase Lite CLI involved. Schema lives in `migrations/0_schema.sql` and is applied with `wrangler d1 migrations apply`, wrangler's own tooling.
48
+
49
+ The Worker itself is identical either way:
50
+
51
+ ```ts
52
+ import { d1 } from "@supabase/lite/workerd";
53
+ import { App } from "@supabase/lite";
54
+
55
+ export default {
56
+ async fetch(request, env, _ctx): Promise<Response> {
57
+ const connection = d1({ binding: env.DB });
58
+ const app = new App({ connection, studio: { enabled: true } });
59
+ return app.fetch(request);
60
+ },
61
+ } satisfies ExportedHandler<Env>;
62
+ ```
63
+
64
+ `wrangler.json(c)` declares the D1 binding as usual:
65
+
66
+ ```json
67
+ "d1_databases": [
68
+ { "binding": "DB", "database_name": "db", "migrations_dir": "./migrations" }
69
+ ]
70
+ ```
71
+
72
+ What's different from a standard Supabase setup: there's no separate database service at all, D1 runs as part of the Worker's own bindings. `experimental` features (Storage, etc.) default to off in workerd since `process.env` isn't available there; toggle them explicitly from a binding with `setExperimental("storage", env.EXPERIMENTAL_STORAGE === "1")`.
73
+
74
+ ## Docker
75
+
76
+ Package the published `@supabase/lite` npm package into a container with `supabase/` bind-mounted for config, schema, seed, and the SQLite file. Run `lite migration up` (or `lite db diff -f <name>` then `lite migration up`) as a one-off bootstrap step, then serve with `lite start --host --no-admin`: `--host` listens beyond the container loopback and `--no-admin` makes the public-boundary choice explicit.
77
+
78
+ ## Related
79
+
80
+ - [Embedded / programmatic](/integrations/embedded): the `App` API these guides build on.
81
+ - [Vite plugin](/integrations/vite): the equivalent pattern for Vite frontends instead of Next.js/Workers.
82
+ - [Database backends](/database/backends): D1, PGlite, and Postgres connection setup.
83
+ - Upstream: [Supabase local development](https://supabase.com/docs/guides/local-development), the Docker-based setup these framework integrations replace.