@supabase/lite 0.0.1-next.2 → 0.0.1-next.4

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/STATUS.md ADDED
@@ -0,0 +1,844 @@
1
+ # Status & Compatibility
2
+
3
+ Feature and API compatibility tracking for @supabase/lite. For usage docs, see [README.md](https://github.com/supabase-community/lite/blob/HEAD/README.md).
4
+
5
+ ## Status Legend
6
+
7
+ | Status | Icon | Meaning |
8
+ |------------------|------|----------------------------------------------------------|
9
+ | **Supported** | ✅ | Works end-to-end |
10
+ | **Partial** | ⚠️ | Parsed/recognized but limited (see notes) |
11
+ | **Incompatible** | ❌ | Not possible due to platform limitations |
12
+ | **Backlog** | 🔄 | Backlog for future consideration/implementation |
13
+ | **N/A** | ⚫ | Not applicable (client-side only, TypeScript-only, etc.) |
14
+
15
+ ---
16
+
17
+ ## Database Support
18
+
19
+ | Runtime | Module | Status |
20
+ |----------------------------|---------------------------|--------|
21
+ | Node.js (SQLite) | `node:sqlite` | ✅ |
22
+ | Bun (SQLite) | `bun:sqlite` | ✅ |
23
+ | Browser (SQLite WASM) | `@sqlite.org/sqlite-wasm` | ✅ |
24
+ | Cloudflare D1 | Workers binding | ✅ |
25
+ | Cloudflare Durable Objects | DO SQLite | ✅ |
26
+ | PGlite | `@electric-sql/pglite` | ✅ |
27
+ | PostgreSQL | `postgres` (node driver) | ✅ |
28
+ | Supabase Cloud | Cloud connection | ✅ |
29
+ | LibSQL | `@libsql/client` | 🔄 |
30
+ | Turso | `@tursodatabase/database` | 🔄 |
31
+ | SQLite.ai | | 🔄 |
32
+
33
+ ---
34
+
35
+ ## Postgres-to-SQLite Translation
36
+
37
+ When using SQLite databases, SQL schemas written in Postgres dialect are translated on the fly. The translator extends the Postgres deparser. It passes through 1:1 compatible syntax unchanged, rewrites constructs that have SQLite equivalents (for example `SERIAL` → `INTEGER PRIMARY KEY AUTOINCREMENT`, `NOW()` → `datetime('now')`), silently drops Postgres-only decorators (storage parameters, locking clauses), and errors on features that have no SQLite counterpart (`LATERAL` joins, table inheritance).
38
+
39
+ 📋 See [`app/POSTGRES-SQLITE-COMPAT.md`](https://github.com/supabase-community/lite/blob/HEAD/app/POSTGRES-SQLITE-COMPAT.md) for the full auto-generated compatibility reference (82 entries).
40
+
41
+ Here is an example of a Postgres schema that is translated to SQLite:
42
+
43
+ ```sql
44
+ -- Postgres DDL
45
+ CREATE TYPE order_status AS ENUM('pending', 'processing', 'shipped', 'delivered');
46
+
47
+ CREATE TABLE users
48
+ (
49
+ id SERIAL PRIMARY KEY,
50
+ email VARCHAR(255) UNIQUE NOT NULL,
51
+ name TEXT NOT NULL,
52
+ is_active BOOLEAN DEFAULT true,
53
+ tags TEXT[],
54
+ metadata JSONB,
55
+ created_at TIMESTAMP DEFAULT NOW()
56
+ );
57
+
58
+ CREATE TABLE orders
59
+ (
60
+ id BIGSERIAL PRIMARY KEY,
61
+ user_id INTEGER REFERENCES users (id) ON DELETE CASCADE,
62
+ status order_status DEFAULT 'pending',
63
+ total NUMERIC(10, 2) CHECK (total >= 0) NOT NULL,
64
+ items JSONB,
65
+ notes TEXT,
66
+ ordered_at TIMESTAMP DEFAULT NOW()
67
+ );
68
+
69
+ ALTER TABLE users ENABLE ROW LEVEL SECURITY;
70
+ ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
71
+
72
+ CREATE
73
+ POLICY user_orders ON orders FOR
74
+ SELECT
75
+ USING (
76
+ user_id = auth.uid (
77
+ ))
78
+ ```
79
+
80
+ Translated to SQLite:
81
+
82
+ ```sql
83
+ -- CREATE TYPE not emitted
84
+
85
+ CREATE TABLE users
86
+ (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ email TEXT UNIQUE NOT NULL CHECK (length(email) <= 255),
89
+ name TEXT NOT NULL,
90
+ is_active INTEGER DEFAULT true CHECK (is_active IN (0, 1)),
91
+ tags TEXT CHECK (
92
+ tags IS NULL
93
+ OR (
94
+ json_valid(tags)
95
+ AND json_type(tags) = 'array'
96
+ )
97
+ ),
98
+ metadata TEXT CHECK (
99
+ metadata IS NULL
100
+ OR json_valid(metadata)
101
+ ),
102
+ created_at TEXT DEFAULT (datetime('now')) CHECK (
103
+ created_at IS NULL
104
+ OR datetime(created_at) IS NOT NULL
105
+ )
106
+ ) STRICT;
107
+
108
+ CREATE TABLE orders
109
+ (
110
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
111
+ user_id INTEGER REFERENCES users (id) ON DELETE CASCADE,
112
+ status TEXT DEFAULT 'pending' CHECK (
113
+ status IN ('pending', 'processing', 'shipped', 'delivered')
114
+ ),
115
+ total REAL CHECK (total >= 0) NOT NULL CHECK (
116
+ ABS(ROUND(total * 100) - total * 100) < 0.0001
117
+ AND ABS(total) < 100000000
118
+ ),
119
+ items TEXT CHECK (
120
+ items IS NULL
121
+ OR json_valid(items)
122
+ ),
123
+ notes TEXT,
124
+ ordered_at TEXT DEFAULT (datetime('now')) CHECK (
125
+ ordered_at IS NULL
126
+ OR datetime(ordered_at) IS NOT NULL
127
+ )
128
+ ) STRICT;
129
+
130
+ -- RLS statements not emitted
131
+ ```
132
+
133
+ ### Translated Field Types
134
+
135
+ | PostgreSQL type / syntax | SQLite storage | Field implementation | Mapping / validation |
136
+ |--------------------------|----------------|----------------------|----------------------|
137
+ | `int2`, `smallint`, `int4`, `integer`, `int`, `int8`, `bigint` | `INTEGER` | `IntegerField` | Integer storage. |
138
+ | `serial`, `serial4`, `bigserial`, `serial8`, `smallserial`, `serial2` | `INTEGER` | `SerialField` | Primary keys become `INTEGER PRIMARY KEY AUTOINCREMENT`. |
139
+ | `float4`, `real`, `float8`, `double precision` | `REAL` | `RealField` | Real-number storage. |
140
+ | `numeric`, `decimal` | `REAL` | `RealField` | Precision/scale emits portable `CHECK` constraints when declared. |
141
+ | `text` | `TEXT` | `TextField` | Text storage. |
142
+ | `varchar`, `varchar(n)`, `character varying`, `character varying(n)`, `char`, `char(n)`, `character`, `character(n)` | `TEXT` | `TextField` | Length-constrained forms emit `length(...) <= n`. |
143
+ | `bpchar` | `TEXT` | `TextField` | Text storage. |
144
+ | `name` | `TEXT` | `TextField` | Emits a 63-character length check. |
145
+ | `bytea` | `BLOB` | `BlobField` | Binary storage. |
146
+ | `bool`, `boolean` | `INTEGER` | `BooleanField` | Stored as `0`/`1` with `CHECK (... IN (0, 1))`. |
147
+ | `date` | `TEXT` | `DateField` | Emits `date(...) IS NOT NULL` validation. |
148
+ | `time`, `time without time zone`, `timetz`, `time with time zone` | `TEXT` | `TimeField` | Emits `time(...) IS NOT NULL` validation. |
149
+ | `timestamp`, `timestamp without time zone`, `timestamptz`, `timestamp with time zone` | `TEXT` | `TimestampField` | Emits `datetime(...) IS NOT NULL` validation. |
150
+ | `interval` | `TEXT` | `IntervalField` | Stored as text; interval arithmetic is not emulated. |
151
+ | `json`, `jsonb` | `TEXT` | `JsonField` | Stored as JSON text with `json_valid(...)` checks. |
152
+ | `uuid` | `TEXT` | `UuidField` | Validates UUID shape and normalizes to lowercase. |
153
+ | `inet` | `TEXT` | `InetField` | Validates IPv4/IPv6 strings with optional CIDR prefixes. |
154
+ | `CREATE TYPE ... AS ENUM` | `TEXT` | `EnumField` | Emits allowed-value `CHECK (... IN (...))`. |
155
+ | `<type>[]`, `_type` arrays | `TEXT` | `ArrayField` | Stores JSON arrays and delegates element serialization where possible. |
156
+
157
+ Unsupported PostgreSQL data types currently include `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, internal types (`tid`, `pg_lsn`, `internal`), pseudo-types, and handler types.
158
+
159
+ ### Row Level Security (RLS)
160
+
161
+ RLS is supported on all database backends. The enforcement strategy differs by dialect:
162
+
163
+ - **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 are merged into the query's `WHERE` clause; `WITH CHECK` conditions are evaluated in-memory against INSERT/UPDATE values.
164
+ - **PGlite / PostgreSQL:** 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.
165
+
166
+ **Auth context:** `auth.role` is resolved from the JWT `role` claim (set during token signing). A missing JWT or missing claim defaults to `"anon"`. This matches PostgREST behavior, where `request.jwt.claim.role` determines the database role. `auth.uid()` resolves to the JWT `sub` claim. `auth.jwt()` exposes the full JWT payload.
167
+
168
+ **Supported:**
169
+
170
+ | Feature | SQLite | PGlite/Postgres | Notes |
171
+ |------------------------------------------------|--------|-----------------|-------------------------------------------------------------|
172
+ | `ENABLE ROW LEVEL SECURITY` | ✅ | ✅ | Default-deny when no policies match |
173
+ | `CREATE POLICY ... USING (expr)` | ✅ | ✅ | SQLite: merged into `WHERE`; Postgres: native |
174
+ | `CREATE POLICY ... WITH CHECK (expr)` | ✅ | ✅ | SQLite: validated in-memory; Postgres: native |
175
+ | `AS PERMISSIVE` (default) | ✅ | ✅ | Multiple permissive policies `OR`'d |
176
+ | `AS RESTRICTIVE` | ✅ | ✅ | `AND`'d with combined permissive result |
177
+ | `FOR SELECT / INSERT / UPDATE / DELETE / ALL` | ✅ | ✅ | Per-command policy targeting |
178
+ | `TO role` (`anon`, `authenticated`, `PUBLIC`) | ✅ | ✅ | Role-based policy filtering; omitting `TO` = PUBLIC (warns) |
179
+ | Auth placeholders (`auth.uid()`, `auth.jwt()`) | ✅ | ✅ | Resolved at runtime from JWT context |
180
+
181
+ **Known limitations (SQLite):**
182
+
183
+ | Limitation | Details |
184
+ |-----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
185
+ | Subquery `WITH CHECK` on `INSERT` | `WITH CHECK` expressions containing subqueries (e.g. `user_id IN (SELECT ...)`) cannot be evaluated in-memory. Currently throws an error. |
186
+ | `UPSERT` applies `INSERT` policies only | Postgres applies `UPDATE` policies on conflict; supalite applies `INSERT WITH CHECK` to all upsert rows since conflict resolution is unknown pre-execution. |
187
+ | `FORCE ROW LEVEL SECURITY` | Not differentiated from `ENABLE ROW LEVEL SECURITY`; table owner is not exempt. |
188
+ | `RETURNING` + `SELECT` policy | Postgres errors if `RETURNING` references rows not visible to `SELECT` policy. Not checked. |
189
+ | `DROP POLICY` / `ALTER POLICY` | Not yet supported in the DDL translator. |
190
+
191
+ 📖 See [`internal/docs/postgres/rls.md`](https://github.com/supabase-community/lite/blob/HEAD/internal/docs/postgres/rls.md) for the full RLS reference and behavior matrix.
192
+
193
+ **PGlite / PostgreSQL auto-setup:** When any table has `ENABLE ROW LEVEL SECURITY`, supalite creates `anon` and `authenticated` roles (if missing) and grants default privileges on all tables and sequences in the relevant schemas. No manual `CREATE ROLE` or `GRANT` statements needed.
194
+
195
+ ### PL/pgSQL Trigger Functions
196
+
197
+ Postgres trigger functions (`RETURNS TRIGGER`, `LANGUAGE plpgsql`) are translated into inline SQLite `CREATE TRIGGER` statements. The function body is parsed and stored in a registry. When the corresponding `CREATE TRIGGER` is reached, the body is inlined directly.
198
+
199
+ **Supported statements:**
200
+
201
+ | Statement | Example | SQLite translation |
202
+ |-----------------------------|---------------------------------------------|-------------------------------------------------------------------------|
203
+ | `NEW.col = expr` | `NEW.updated_at = now()` | `UPDATE table SET updated_at = datetime('now') WHERE rowid = NEW.rowid` |
204
+ | `NEW.col := expr` | `NEW.name := upper(NEW.name)` | Same as above |
205
+ | `INSERT INTO ...` | `INSERT INTO profiles (id) VALUES (NEW.id)` | Pass-through inside trigger body |
206
+ | `UPDATE ...` / `DELETE ...` | Any DML | Pass-through inside trigger body |
207
+ | `RETURN NEW` / `RETURN OLD` | Required in PG | Not emitted (implicit in SQLite) |
208
+
209
+ Multiple `NEW.col = expr` assignments are merged into a single `UPDATE ... SET` statement.
210
+
211
+ **Trigger options:** `BEFORE`/`AFTER` timing, `INSERT`/`UPDATE`/`DELETE` events (and combinations), `UPDATE OF col1, col2`, `FOR EACH ROW`. `SECURITY DEFINER` is silently ignored.
212
+
213
+ **Common Supabase patterns:** `handle_new_user` (insert into profiles on signup), `updated_at` timestamps, audit logging with `OLD`/`NEW` refs, counter updates.
214
+
215
+ **Unsupported (throws descriptive error):** `DECLARE`, `IF`/`ELSIF`/`ELSE`, `LOOP`, `RAISE`, `PERFORM`, `SELECT INTO`, `EXECUTE`, variables, non-trigger functions.
216
+
217
+ ---
218
+
219
+ ## Database API (PostgREST-compatible)
220
+
221
+ Request pipeline: HTTP request → PostgREST parser → internal AST → dialect-specific SQL (SQLite or Postgres).
222
+
223
+ SQLite compatibility targets the [Cloudflare workerd SQLite version](https://github.com/cloudflare/workerd/blob/3861a920e639af7c3ab6bf10ee42f1235c415ac2/MODULE.bazel#L26) (currently 3.47).
224
+
225
+ All operators are parsed into the AST. The "Status" column reflects whether a working **deparser handler** exists for that operator on each dialect.
226
+
227
+ ### Core Operations
228
+
229
+ | Method | SQLite | Postgres | Notes |
230
+ |------------|--------|----------|---------------------------------------------------------------------|
231
+ | `from()` | ✅ | ✅ | Table/view selection |
232
+ | `select()` | ✅ | ✅ | Columns, aliases, aggregates (implicit GROUP BY), JSON paths, casts |
233
+ | `insert()` | ✅ | ✅ | Single/batch, RETURNING (SQLite 3.35+) |
234
+ | `update()` | ✅ | ✅ | With WHERE, RETURNING (SQLite 3.35+) |
235
+ | `delete()` | ✅ | ✅ | With WHERE, RETURNING (SQLite 3.35+) |
236
+ | `upsert()` | ✅ | ✅ | ON CONFLICT (SQLite 3.24+) |
237
+
238
+ ### Comparison Filters
239
+
240
+ | Method | SQLite | Postgres | Notes |
241
+ |----------------|--------|----------|--------------------------------|
242
+ | `eq()` | ✅ | ✅ | |
243
+ | `neq()` | ✅ | ✅ | |
244
+ | `gt()` | ✅ | ✅ | |
245
+ | `gte()` | ✅ | ✅ | |
246
+ | `lt()` | ✅ | ✅ | |
247
+ | `lte()` | ✅ | ✅ | |
248
+ | `in()` | ✅ | ✅ | |
249
+ | `notIn()` | ✅ | ✅ | |
250
+ | `is()` | ✅ | ✅ | `NULL`, `true`, `false` checks |
251
+ | `isDistinct()` | ✅ | ✅ | |
252
+
253
+ ### Pattern Matching
254
+
255
+ | Method | SQLite | Postgres | Notes |
256
+ |----------------|--------|----------|-------------------------------------------------------|
257
+ | `like()` | ✅ | ✅ | |
258
+ | `ilike()` | ✅ | ✅ | SQLite lowers both operands before `LIKE` |
259
+ | `likeAllOf()` | ✅ | ✅ | SQLite expands quantified `LIKE` to `AND` predicates |
260
+ | `likeAnyOf()` | ✅ | ✅ | SQLite expands quantified `LIKE` to `OR` predicates |
261
+ | `ilikeAllOf()` | ✅ | ✅ | SQLite lowers operands and expands to `AND` predicates |
262
+ | `ilikeAnyOf()` | ✅ | ✅ | SQLite lowers operands and expands to `OR` predicates |
263
+
264
+ ### Array & JSON Filters
265
+
266
+ | Method | SQLite | Postgres | Notes |
267
+ |-----------------|--------|----------|-----------------------------------------------------------------------------------------------|
268
+ | `contains()` | ⚠️ | ✅ | SQLite: scalar arrays via `json_each`; shallow objects via `json_extract`. See caveats below. |
269
+ | `containedBy()` | ⚠️ | ✅ | SQLite: scalar arrays via `NOT EXISTS` over `json_each`. Objects/nested not supported. |
270
+ | `overlaps()` | ⚠️ | ✅ | SQLite: scalar arrays via `EXISTS` over `json_each`. Objects/nested not supported. |
271
+ | `->` / `->>` | ✅ | ✅ | JSON path in `select`, `order`, and `where` filters |
272
+ | `jsonb` column | ✅ | ✅ | Stored as TEXT + `json_valid()` check on SQLite |
273
+
274
+ **SQLite containment caveats:**
275
+
276
+ - ✅ **Arrays of scalars** (`tags=cs.{a,b}`): matches Postgres `@>` semantics via `json_each`.
277
+ - ✅ **Shallow objects with scalar values** (`meta=cs.{"theme":"dark"}`): matched per-key with `json_extract(col, '$.key') = value`.
278
+ - ✅ **NULL columns** are safely skipped (no `json_each(NULL)` error).
279
+ - ✅ **Empty array input:** `cs []` → always true for non-null; `cd []` → only empty array matches; `ov []` → always false.
280
+ - ❌ **Arrays of objects** (`[{a:1}] @> [{a:1}]`): `json_each` yields JSON text for object elements; equality against JS-serialized binds is not reliable. _Follow-up:_ emit a per-element `EXISTS` with recursive key matching, or a correlated subquery comparing `json_extract` of each target key.
281
+ - ❌ **Nested objects in `cs` filter** (`data=cs.{"user":{"id":1}}`): `json_extract` returns the sub-object as JSON text which won't equal the JS-object bind. _Follow-up:_ recursively walk the filter object and emit one `json_extract` comparison per leaf scalar key (`json_extract(col, '$.user.id') = 1`).
282
+ - ❌ **`cd`/`ov` with object values:** the operators currently require array inputs. _Follow-up:_ define semantics (does `cd` mean "all top-level keys in set"?) and add handlers.
283
+
284
+ ### Full-Text Search
285
+
286
+ | Method | SQLite | Postgres | Notes |
287
+ |------------------------|--------|----------|----------------------------------------------|
288
+ | `textSearch()` (fts) | ❌ | ✅ | Not implemented on SQLite; FTS5 candidate (backlog) |
289
+ | `textSearch()` (plfts) | ❌ | ✅ | Not implemented on SQLite |
290
+ | `textSearch()` (phfts) | ❌ | ✅ | Not implemented on SQLite |
291
+ | `textSearch()` (wfts) | ❌ | ✅ | Not implemented on SQLite |
292
+
293
+ ### Advanced Filters
294
+
295
+ | Method | SQLite | Postgres | Notes |
296
+ |------------|--------|----------|-------------------------------------------------|
297
+ | `match()` | ✅ | ✅ | Multiple eq, expanded by parser, uses base ops |
298
+ | `or()` | ✅ | ✅ | Logical OR via `$or` in AST |
299
+ | `not()` | ✅ | ✅ | Logical NOT via `$not` in AST |
300
+ | `filter()` | ✅ | ✅ | Delegates to individual operator handlers |
301
+
302
+ ### Regex
303
+
304
+ | Method | SQLite | Postgres | Notes |
305
+ |-----------------|--------|----------|--------------------------------------------------|
306
+ | `regexMatch()` | ❌ | ✅ | Not implemented on SQLite; would require `regexp` extension |
307
+ | `regexIMatch()` | ❌ | ✅ | Not implemented on SQLite; would require `regexp` extension |
308
+
309
+ ### Range Operators
310
+
311
+ | Method | SQLite | Postgres | Notes |
312
+ |-------------------|--------|----------|--------------------------|
313
+ | `rangeGt()` | ❌ | ✅ | No range types in SQLite |
314
+ | `rangeGte()` | ❌ | ✅ | |
315
+ | `rangeLt()` | ❌ | ✅ | |
316
+ | `rangeLte()` | ❌ | ✅ | |
317
+ | `rangeAdjacent()` | ❌ | ✅ | |
318
+
319
+ ### Quantified Comparison Operators
320
+
321
+ | Method | SQLite | Postgres | Notes |
322
+ |-------------------------|--------|----------|------------------------------|
323
+ | `eq(any)` / `eq(all)` | ❌ | ✅ | Not implemented on SQLite |
324
+ | `neq(any)` / `neq(all)` | ❌ | ✅ | |
325
+ | `gt(any)` / `gt(all)` | ❌ | ✅ | |
326
+ | `gte(any)` / `gte(all)` | ❌ | ✅ | |
327
+ | `lt(any)` / `lt(all)` | ❌ | ✅ | |
328
+ | `lte(any)` / `lte(all)` | ❌ | ✅ | |
329
+
330
+ > Postgres `col=eq(any).{1,2,3}` compiles to `col = ANY(ARRAY[…])`. SQLite has no array type and no `ANY`/`ALL` quantified form on scalars; emulating it would require expanding to `OR`/`AND` chains over the literal list. Not implemented.
331
+
332
+ ### Transforms
333
+
334
+ | Method | SQLite | Postgres | Notes |
335
+ |-----------------|--------|----------|---------------------------------|
336
+ | `order()` | ✅ | ✅ | NULLS FIRST/LAST (SQLite 3.30+) |
337
+ | `limit()` | ✅ | ✅ | |
338
+ | `range()` | ✅ | ✅ | LIMIT + OFFSET |
339
+ | `single()` | ✅ | ✅ | Via Prefer header |
340
+ | `maybeSingle()` | ✅ | ✅ | Via Prefer header |
341
+
342
+ ### Resource Embedding
343
+
344
+ | Feature | SQLite | Postgres | Notes |
345
+ |-----------------------|--------|----------|-------------------------------------------------------------------------------------------------|
346
+ | Foreign key joins | ✅ | ✅ | Auto-resolved from introspection; embed null/not-null filters use relationship existence checks |
347
+ | Spread (`...`) | ✅ | ✅ | Inline embedded columns; array spreads preserve JSON object/array values and related ordering |
348
+ | Inner join (`!inner`) | ✅ | ✅ | |
349
+ | Nested embedding | ✅ | ✅ | Multi-level joins |
350
+ | Aggregates | ✅ | ✅ | count, sum, avg, min, max; spread aggregates return PGRST127 |
351
+
352
+ ### Response Shaping & Utilities
353
+
354
+ | Method | SQLite | Postgres | Notes |
355
+ |------------------|--------|----------|---------------------------------------|
356
+ | `csv()` | ✅ | ✅ | Input + output via `text/csv` Accept/Content-Type |
357
+ | `geojson()` | ❌ | ❌ | Requires SpatiaLite or PostGIS; not wired up |
358
+ | `explain()` | ⚠️ | ⚠️ | Returns compiled SQL via `application/vnd.pgrst.plan`; real plan output not surfaced |
359
+ | `abortSignal()` | ✅ | ✅ | Signal check before execution |
360
+ | `setHeader()` | ✅ | ✅ | HTTP layer |
361
+ | `throwOnError()` | ✅ | ✅ | JS error mode |
362
+ | `maxAffected()` | ✅ | ✅ | Check `changes()` after execution |
363
+
364
+ ### Control & Specialized
365
+
366
+ | Method | SQLite | Postgres | Notes |
367
+ |-------------------|--------|----------|-------------------------------------------------------|
368
+ | `rpc()` | ❌ | ✅ | Not supported on SQLite. Postgres RPC depends on stored procedures (`CREATE FUNCTION ... LANGUAGE sql/plpgsql`); SQLite has no stored-procedure model and we don't intend to introduce a parallel JS-function registry. Use a regular HTTP/server endpoint for custom logic. |
369
+ | `schema()` | ❌ | ✅ | SQLite has single schema |
370
+ | `rollback()` | ❌ | ❌ | PostgREST-specific, not implemented |
371
+ | `returns()` | ⚫ | ⚫ | TypeScript-only, no runtime effect |
372
+ | `overrideTypes()` | ⚫ | ⚫ | TypeScript-only, no runtime effect |
373
+
374
+ ### Summary: Database API
375
+
376
+ | Category | SQLite | Postgres |
377
+ |-----------------|-------------------------|-------------------------|
378
+ | Core CRUD (6) | ✅ 6 ⚠️ 0 ❌ 0 | ✅ 6 ⚠️ 0 ❌ 0 |
379
+ | Comparison (10) | ✅ 10 ⚠️ 0 ❌ 0 | ✅ 10 ⚠️ 0 ❌ 0 |
380
+ | Pattern (6) | ✅ 6 ⚠️ 0 ❌ 0 | ✅ 6 ⚠️ 0 ❌ 0 |
381
+ | Array/JSON (3) | ✅ 0 ⚠️ 3 ❌ 0 | ✅ 3 ⚠️ 0 ❌ 0 |
382
+ | Full-Text (4) | ✅ 0 ⚠️ 0 ❌ 4 | ✅ 4 ⚠️ 0 ❌ 0 |
383
+ | Advanced (4) | ✅ 4 ⚠️ 0 ❌ 0 | ✅ 4 ⚠️ 0 ❌ 0 |
384
+ | Regex (2) | ✅ 0 ⚠️ 0 ❌ 2 | ✅ 2 ⚠️ 0 ❌ 0 |
385
+ | Range (5) | ✅ 0 ⚠️ 0 ❌ 5 | ✅ 5 ⚠️ 0 ❌ 0 |
386
+ | Quantified (12) | ✅ 0 ⚠️ 0 ❌ 12 | ✅ 12 ⚠️ 0 ❌ 0 |
387
+ | Transforms (5) | ✅ 5 ⚠️ 0 ❌ 0 | ✅ 5 ⚠️ 0 ❌ 0 |
388
+ | Embedding (5) | ✅ 5 ⚠️ 0 ❌ 0 | ✅ 5 ⚠️ 0 ❌ 0 |
389
+ | Response (7) | ✅ 5 ⚠️ 1 ❌ 1 | ✅ 5 ⚠️ 1 ❌ 1 |
390
+ | Control (5) | ✅ 0 ⚠️ 0 ❌ 3 ⚫ 2 | ✅ 2 ⚠️ 0 ❌ 1 ⚫ 2 |
391
+ | **Total (74)** | **✅ 41 ⚠️ 4 ❌ 27 ⚫ 2** | **✅ 69 ⚠️ 1 ❌ 2 ⚫ 2** |
392
+
393
+ > Counting effective availability (✅ + ⚠️ + ⚫): **47/74 on SQLite**, **72/74 on Postgres** (`geojson()` and `rollback()` are the only gaps on Postgres). The 2 ⚫ methods (`returns`, `overrideTypes`) are TypeScript-only and need no backend.
394
+
395
+ ### PostgREST spec: SQLite skip breakdown
396
+
397
+ The 263 cases skipped on SQLite (out of 1,702) cluster into a small set of upstream Postgres features that have no SQLite analogue. Generated by `cd app && bun run test:spec:analyze:sqlite` (writes `.context/sqlite-analysis.json`).
398
+
399
+ | Category | Skipped | Why |
400
+ |----------|--------:|--------------------------------------------------------------------------------------|
401
+ | `feature_haskell_divergence_strict` | 46 | Strict Haskell PostgREST behaviour (header parsing, prefer modes) lite intentionally diverges from |
402
+ | `domain_type` | 37 | Postgres `CREATE DOMAIN` types; no SQLite equivalent |
403
+ | `pg_only_operators` | 36 | Postgres-specific operator syntax (`~`, `~*`, dictionary FTS, etc.) |
404
+ | `tx_preferences` | 36 | `Prefer: tx=commit/rollback` and pre-request GUCs; no SQLite txn boundary |
405
+ | `cache_fk_names` | 23 | FK disambiguation paths exercising specific PG catalog metadata |
406
+ | `pg_dictionary_fts` | 16 | Language-tagged full-text search dictionaries |
407
+ | `feature_error_codes` | 11 | PostgREST/PG-specific SQLSTATE shapes |
408
+ | `cache_pg_types` | 10 | JSON operator behaviour tied to PG type cache |
409
+ | `pg_extension_specific` | 10 | CORS / custom media types relying on PG extensions |
410
+ | `view_embedding` | 10 | View-FK resolution in PG-only metadata |
411
+ | `state_dependent` | 8 | Cases dependent on connection-pool/session state |
412
+ | `pg_role_set` | 4 | `SET LOCAL role` / JWT-driven role switching |
413
+ | `lite_anon_bearer_fallback` | 4 | Lite-specific JWT fallback handling |
414
+ | `computed_column` | 3 | PG `STORED`/`VIRTUAL` generated columns |
415
+ | `mutating_views` | 1 | Updatable view rules |
416
+ | `postgis` | 1 | PostGIS spatial types |
417
+ | `xml_io` | 1 | XML output |
418
+ | `mutation_embed_two_query_limit` | 2 | Embedded mutation w/ two-query plan |
419
+ | `computed_relations` | 1 | PG computed relationship overrides |
420
+ | `collation_locale` | 1 | Locale-sensitive ordering |
421
+ | `jsonb_dedup` | 2 | PG `jsonb` duplicate-key dedup semantics |
422
+
423
+ Run `bun run test:spec:analyze` (or `:sqlite-postgres`, `:pglite`, `:postgres`) to regenerate per backend.
424
+
425
+ ---
426
+
427
+ ## Auth API (GoTrue-compatible)
428
+
429
+ Backend implementation in `app/src/auth/`. GoTrue-compatible HTTP endpoints at `/auth/v1/*`.
430
+
431
+ ### ✅ Implemented
432
+
433
+ | Method | Endpoint | Notes |
434
+ |------------------------|----------------------------------------|-----------------------------------------------------------------------|
435
+ | `signUp()` | `POST /signup` | Email/password, optional metadata, email confirmation |
436
+ | `signInWithPassword()` | `POST /token?grant_type=password` | JWT + refresh token |
437
+ | `signInWithOtp()` | `POST /otp` | Magic link / OTP via email |
438
+ | `verifyOtp()` | `POST /verify` | Supports: signup, magiclink, recovery, email_change, reauthentication |
439
+ | `refreshSession()` | `POST /token?grant_type=refresh_token` | Token rotation, immediate reuse compatibility, session expiry checks |
440
+ | `signOut()` | `POST /logout` | Scopes: local, global, others |
441
+ | `getUser()` | `GET /user` | JWT-authenticated |
442
+ | `updateUser()` | `PUT /user` | Metadata, password, email change |
443
+ | `recover()` | `POST /recover` | Password reset email |
444
+ | `resend()` | `POST /resend` | Resend confirmation / email change |
445
+ | `reauthenticate()` | `GET /reauthenticate` | Request reauthentication nonce |
446
+
447
+ Supporting infrastructure:
448
+
449
+ - JWT signing/verification
450
+ - Password hashing (bcrypt-compatible)
451
+ - Refresh token rotation with revocation, reuse, and parent-chain tracking
452
+ - Session management (create, validate, refresh, expire, delete)
453
+ - Configurable email signup, confirmations, and secure/insecure email change
454
+ - Mailer integration (confirmation, recovery, magic link, email change)
455
+
456
+ ### 🔄 Planned
457
+
458
+ | Method | Notes |
459
+ |-----------------------------|---------------------------------------------------------------|
460
+ | `signInWithOAuth()` | Config schema exists, major providers (Google, GitHub, Apple) |
461
+ | `exchangeCodeForSession()` | PKCE flow for OAuth |
462
+ | `signInAnonymously()` | Create anonymous session |
463
+ | `linkIdentity()` | Link OAuth identity to existing user |
464
+ | `unlinkIdentity()` | Remove linked identity |
465
+ | `admin.createUser()` | Direct user creation (skip confirmation) |
466
+ | `admin.listUsers()` | Paginated user list |
467
+ | `admin.getUserById()` | Fetch user by ID |
468
+ | `admin.updateUserById()` | Direct user update |
469
+ | `admin.deleteUser()` | User deletion |
470
+ | `admin.inviteUserByEmail()` | Send invite |
471
+ | `admin.generateLink()` | Generate verification/reset links |
472
+ | `admin.signOut()` | Admin-initiated logout |
473
+ | `admin.mfa.listFactors()` | List user MFA factors |
474
+ | `admin.mfa.deleteFactor()` | Remove MFA factor |
475
+
476
+ ### ⚫ Not Planned
477
+
478
+ | Method | Reason |
479
+ |----------------------------------------|--------------------------------|
480
+ | `mfa.enroll()` | MFA deferred |
481
+ | `mfa.challenge()` | MFA deferred |
482
+ | `mfa.verify()` | MFA deferred |
483
+ | `mfa.challengeAndVerify()` | MFA deferred |
484
+ | `mfa.unenroll()` | MFA deferred |
485
+ | `mfa.listFactors()` | MFA deferred |
486
+ | `mfa.getAuthenticatorAssuranceLevel()` | MFA deferred |
487
+ | `mfa.webauthn.*` (5 methods) | WebAuthn deferred |
488
+ | `signInWithSSO()` | SAML/SSO deferred |
489
+ | `signInWithWeb3()` | Web3 deferred |
490
+ | `signInWithIdToken()` | OIDC token validation deferred |
491
+ | `oauth.getAuthorizationDetails()` | OAuth server deferred |
492
+ | `oauth.approveAuthorization()` | OAuth server deferred |
493
+ | `oauth.denyAuthorization()` | OAuth server deferred |
494
+ | `oauth.listGrants()` | OAuth server deferred |
495
+ | `oauth.revokeGrant()` | OAuth server deferred |
496
+ | `oauth.listClients()` | OAuth admin deferred |
497
+ | `oauth.createClient()` | OAuth admin deferred |
498
+ | `oauth.getClient()` | OAuth admin deferred |
499
+ | `oauth.updateClient()` | OAuth admin deferred |
500
+ | `oauth.deleteClient()` | OAuth admin deferred |
501
+ | `oauth.regenerateClientSecret()` | OAuth admin deferred |
502
+
503
+ ### ⚫ Client-Side Only (N/A)
504
+
505
+ These methods exist in `@supabase/supabase-js` but are client-side concerns, not backend endpoints:
506
+
507
+ | Method | Notes |
508
+ |---------------------------|------------------------------------------------------|
509
+ | `onAuthStateChange()` | Client event listener |
510
+ | `startAutoRefresh()` | Client timer |
511
+ | `stopAutoRefresh()` | Client timer |
512
+ | `getSession()` | Client storage read |
513
+ | `setSession()` | Client storage write |
514
+ | `initialize()` | Client initialization |
515
+ | `getClaims()` | Client JWT parsing |
516
+ | `isThrowOnErrorEnabled()` | Client error mode |
517
+ | `resetPasswordForEmail()` | Client-side wrapper for `recover()` |
518
+ | `getUserIdentities()` | Client-side wrapper for identity data in `getUser()` |
519
+
520
+ ### Summary: Auth API
521
+
522
+ | Status | Count |
523
+ |-------------------|-------|
524
+ | ✅ Implemented | 11 |
525
+ | 🔄 Planned | 15 |
526
+ | ⚫ Not Planned | 27 |
527
+ | ⚫ Client-side N/A | 10 |
528
+
529
+ ### Auth spec: SQLite skip breakdown
530
+
531
+ The 247 cases skipped against the supabase-spec Auth corpus break down by deferred feature. Generated by `cd app && bun run test:spec:auth:analyze:sqlite` (writes `.context/auth-analysis-sqlite.json`).
532
+
533
+ | Category | Skipped | Why |
534
+ |---------------------------------|--------:|--------------------------------------------------------------------|
535
+ | `admin_api` | 69 | `/admin/*` endpoints (createUser, listUsers, generateLink, etc.) |
536
+ | `mfa` | 44 | TOTP/WebAuthn enroll/challenge/verify |
537
+ | `non_runnable_spec_placeholder` | 29 | Upstream rows with `expected.status: null`; see `app/test/supabase-spec/auth/NON_RUNNABLE_PLACEHOLDERS.md` |
538
+ | `oauth` | 24 | OAuth2/PKCE provider flows |
539
+ | `phone_sms` | 23 | Phone signup / SMS OTP |
540
+ | `saml_sso` | 18 | SAML / SSO |
541
+ | `session_admin` | 12 | Admin session management |
542
+ | `anonymous` | 10 | `signInAnonymously()` and conversion flows |
543
+ | `identity_linking` | 9 | `linkIdentity()` / `unlinkIdentity()` |
544
+ | `notification_hooks` | 9 | Send-email/SMS hooks |
545
+
546
+ ---
547
+
548
+ ## Storage API
549
+
550
+ Backend implementation in `app/src/storage/`. HTTP endpoints at `/storage/v1/*`. Pluggable storage backends (filesystem, S3).
551
+
552
+ ### ✅ Implemented
553
+
554
+ | Endpoint | Method | Notes |
555
+ |----------------------------------------|----------|-----------------------------------------------|
556
+ | `POST /bucket` | Create | id, name, public, file_size_limit, mime types |
557
+ | `GET /bucket` | List | |
558
+ | `GET /bucket/:id` | Get | |
559
+ | `PUT /bucket/:id` | Update | public, file_size_limit, allowed_mime_types |
560
+ | `DELETE /bucket/:id` | Delete | Fails if non-empty |
561
+ | `POST /bucket/:id/empty` | Empty | |
562
+ | `POST /object/:bucketId/*` | Upload | Multipart form-data, upsert via `x-upsert` |
563
+ | `PUT /object/:bucketId/*` | Replace | |
564
+ | `GET /object/:bucketId/*` | Download | Authenticated |
565
+ | `GET /object/public/:bucketId/*` | Download | Public buckets, no auth |
566
+ | `HEAD /object/:bucketId/*` | Exists | |
567
+ | `DELETE /object/:bucketId` | Remove | Batch delete by prefixes |
568
+ | `POST /object/list/:bucketId` | List | prefix, limit, offset, sortBy, search |
569
+ | `POST /object/move` | Move | |
570
+ | `POST /object/copy` | Copy | |
571
+ | `GET /object/info/:bucketId/*` | Info | Object metadata |
572
+ | `POST /object/sign/:bucketId/*` | Sign | Create signed download URL (JWT) |
573
+ | `POST /object/upload/sign/:bucketId/*` | Sign | Create signed upload URL |
574
+ | `GET /object/sign/:bucketId/*` | Download | Via signed URL token |
575
+ | `PUT /object/upload/sign/:bucketId/*` | Upload | Via signed upload URL |
576
+
577
+ ### Storage Adapters
578
+
579
+ | Adapter | Status | Notes |
580
+ |--------------------|--------|--------------------------------------------|
581
+ | Filesystem | ✅ | Local file storage |
582
+ | S3 / S3-compatible | ✅ | MinIO, AWS S3, Cloudflare R2 via aws4fetch |
583
+ | Cloudflare R2 | ✅ | Via S3 adapter |
584
+
585
+ ### Transformation Adapters
586
+
587
+ | Adapter | Status | Notes |
588
+ |------------|--------|-----------------------------|
589
+ | Noop | ✅ | Passthrough (no transforms) |
590
+ | Sharp | ✅ | Resize, format conversion |
591
+ | Cloudflare | ✅ | URL-based image transforms |
592
+
593
+ ### 🔄 Not Yet Implemented
594
+
595
+ | Feature | Notes |
596
+ |--------------------------------|------------------------------------------------------------|
597
+ | `/status` health endpoint | Oracle returns 200 with no auth |
598
+ | Role-based access control | service_role-only for admin ops, anon/authenticated gating |
599
+ | RLS policies on storage tables | Per-user object access via row-level security |
600
+ | Bucket list query params | `?search=`, `?limit=`, `?offset=` on `GET /bucket` |
601
+ | S3-compatible protocol | `PUT/GET/DELETE` via S3 API paths (`/s3/`) |
602
+ | TUS resumable uploads | `POST/PATCH/HEAD` on `/upload/resumable` |
603
+ | Webhooks | ObjectCreated/ObjectRemoved events |
604
+
605
+ ### Summary: Storage API
606
+
607
+ | Status | Count |
608
+ |---------------------|------------------------------|
609
+ | ✅ Endpoints | 20 |
610
+ | ✅ Adapters | 3 storage + 3 transformation |
611
+ | 🔄 Missing features | 7 |
612
+
613
+ ### Spec Test Results
614
+
615
+ Tests run via supabase-spec JSON test cases against Postgres (pgserve). Run: `cd app && bun run test:phenotype:storage`
616
+
617
+ | Category | Pass | Fail | Notes |
618
+ |------------------|------|------|---------------------------------------------|
619
+ | bucket_crud | 36 | 2 | Fails: anon/authenticated access |
620
+ | object_upload | 19 | 3 | Fails: anon access, size limit, auth format |
621
+ | access_control | -- | 48 | Needs RLS + role-based access |
622
+ | object_read | -- | 28 | Cascade from setup/access issues |
623
+ | object_list | -- | 26 | Cascade from setup/access issues |
624
+ | signed_urls | -- | 24 | Cascade from setup/access issues |
625
+ | errors | -- | 27 | Auth error format (401 vs 400) |
626
+ | object_move_copy | -- | 21 | Cascade from setup/access issues |
627
+ | object_naming | -- | 15 | Cascade from setup/access issues |
628
+ | user_metadata | -- | 10 | Cascade from setup/access issues |
629
+ | file_types | -- | 8 | Cascade from setup/access issues |
630
+ | health | -- | 3 | Missing `/status` endpoint |
631
+ | object_delete | -- | 3 | Cascade from setup/access issues |
632
+
633
+ > Many failures cascade from a few root issues (missing role-based access, auth error format). Fixing role checking, `/status`, and error format would flip a large number of tests.
634
+
635
+ ---
636
+
637
+ ## CLI
638
+
639
+ Backend implementation in `app/src/cli/`. Aligned to upstream `supabase` CLI command shape (v2.98.2). Reference: [`internal/docs/cli/`](https://github.com/supabase-community/lite/tree/HEAD/internal/docs/cli/). Lite-only commands carry a `[lite]` tag in `--help` output.
640
+
641
+ Help groups match upstream: **Local Development** (commands you run from a project) and **Management APIs** (commands that manage remote resources).
642
+
643
+ ### Top-Level
644
+
645
+ | Command | Status | Notes |
646
+ |-----------|--------|--------------------------------------------------------------------|
647
+ | `init` | ✅ | Parity at verb level; flags differ from upstream |
648
+ | `start` | ✅ | In-process; no Docker stack flags (`-x`, `--ignore-health-check`) |
649
+ | `status` | 🧪 | `[experimental]` `[lite]`: shows linked project metadata |
650
+ | `login` | 🧪 | `[experimental]` Email/password against supalite cloud |
651
+ | `logout` | 🧪 | `[experimental]` Parity |
652
+ | `link` | 🧪 | `[experimental]` Parity at verb; no `--password`/`--skip-pooler` |
653
+ | `unlink` | 🧪 | `[experimental]` Parity |
654
+ | `dev` | ✅ | `[lite]`: schema-watching dev server; emits experimental warning |
655
+ | `repl` | ✅ | `[lite]`: Node-only interactive REPL |
656
+ | `debug` | ✅ | `[lite]`: runtime info dump |
657
+ | `signup` | 🧪 | `[experimental]` `[lite]`: register supalite cloud account |
658
+ | `whoami` | 🧪 | `[experimental]` `[lite]`: current authenticated user |
659
+ | `upgrade` | ✅ | `[lite]`: migrate supalite project to hosted/local Supabase |
660
+ | `bootstrap` | ⚫ | Not planned (starter-template scaffolding belongs to upstream CLI) |
661
+ | `stop` | 🔄 | Not registered yet |
662
+ | `services` | 🚫 | Not applicable; no Docker stack |
663
+ | `seed buckets` | 🔄 | Depends on storage support |
664
+ | `test db` / `test new` | 🚫 | Not applicable on SQLite (no pgTAP) |
665
+
666
+ ### `db` group
667
+
668
+ | Command | Status | Notes |
669
+ |---------------|--------|----------------------------------------------------------------------|
670
+ | `db diff` | ✅ | Re-export of `db schema --diff` handler; `--local` semantics only |
671
+ | `db query` | ✅ | Renamed from top-level `exec`; supports `--remote` and `--config` |
672
+ | `db schema` | ✅ | `[lite]`: moved from top-level; `--diff` and `--sql` modes |
673
+ | `db push` | 🔄 | Not registered; use `lite cloud deploy` |
674
+ | `db pull`, `db reset`, `db dump`, `db lint`, `db advisors` | 🔄 | Not registered |
675
+ | `db start` | 🚫 | Not applicable; in-process, no separate DB start |
676
+
677
+ ### `migration` group
678
+
679
+ | Command | Status | Notes |
680
+ |---------------------|--------|--------------------------------------------------------|
681
+ | `migration diff` | ✅ | `[lite]`: diff DB state against `schemas/*.sql` |
682
+ | `migration new`, `migration list`, `migration up`, `migration down`, `migration repair`, `migration squash`, `migration fetch` | 🔄 | Not registered |
683
+
684
+ ### `cloud` group `[lite]` `[experimental]`
685
+
686
+ | Command | Status | Notes |
687
+ |-----------------|--------|-----------------------------------------------------------------------------|
688
+ | `cloud deploy` | 🧪 | Pushes schema + config + seed to linked supalite cloud (renamed from `push`) |
689
+ | `cloud diff` | 🧪 | Diffs local against linked remote (renamed from `diff`) |
690
+
691
+ ### `config` group
692
+
693
+ Not registered. `config push` (upstream pushes `config.toml` to the linked project's API config) is folded into `cloud deploy` until it's worth splitting.
694
+
695
+ ### `projects` group `[experimental]`
696
+
697
+ | Command | Status | Notes |
698
+ |-------------------|--------|--------------------------------------------------------|
699
+ | `projects list` | 🧪 | List supalite cloud projects |
700
+ | `projects create` | 🧪 | Create project; flags differ (no `--org-id`/`--region`) |
701
+ | `projects api-keys` | 🔄 | Not registered |
702
+ | `projects delete` | 🔄 | Not registered |
703
+
704
+ ### Management API groups
705
+
706
+ All management-API groups are deferred per LITE-177. None registered as placeholders to keep `--help` output clean. Re-evaluate per group as supalite cloud exposes matching endpoints.
707
+
708
+ | Group | Status | Reason |
709
+ |-------|--------|--------|
710
+ | `orgs`, `secrets`, `branches`, `domains`, `network-bans`, `network-restrictions`, `ssl-enforcement`, `vanity-subdomains`, `encryption`, `sso` | 🔄 | Depends on supalite cloud surface area |
711
+ | `gen`, `functions`, `inspect`, `storage`, `telemetry` | 🔄 | Depends on respective service support |
712
+ | `backups`, `snippets`, `postgres-config`, `services` | 🚫 | Not applicable (SQLite / no dashboard / no Docker / no GUC) |
713
+
714
+ ### Environment & `.env`
715
+
716
+ Mirrors upstream behavior documented in [`internal/docs/cli/environment.md`](https://github.com/supabase-community/lite/blob/HEAD/internal/docs/cli/environment.md). Loader runs in `ProjectLocalApi.readConfig` before TOML parsing; `env(VAR)` substitution runs after parsing, before schema validation.
717
+
718
+ | Behavior | Status | Notes |
719
+ |------------------------------------------------------------------|--------|------------------------------------------------------------------|
720
+ | Dotenv directory walk (config dir → CWD) | ✅ | First-write-wins; deeper dir wins over parent |
721
+ | `SUPABASE_ENV` flavor (default `development`; `test` skips `.env.local`) | ✅ | Filename order: `.env.<flavor>.local` → `.env.local` → `.env.<flavor>` → `.env` |
722
+ | Process env never overwritten by files | ✅ | Shell/CI wins over every file |
723
+ | `env(VAR_NAME)` substitution in config | ✅ | Anchored match; missing → empty string |
724
+ | `secrets set --env-file` | ⚫ | Depends on `secrets` group (deferred) |
725
+ | `functions serve --env-file` / `supabase/functions/.env` | ⚫ | Depends on `functions` group (deferred) |
726
+
727
+ ### Summary: CLI
728
+
729
+ | Status | Count |
730
+ |-----------------------------|-------|
731
+ | ✅ Implemented | 22 |
732
+ | 🔄 Planned (not registered) | 17 |
733
+ | 🚫 Not Applicable | 7 |
734
+
735
+ > Counts include only commands tracked in this file (excludes management-API groups represented as a single row each).
736
+
737
+ ---
738
+
739
+ ## Other Services
740
+
741
+ | Service | Status | Notes |
742
+ |--------------------|--------|-------------------------------------------------------------------------------------------------------|
743
+ | **Storage** | 🔄 | Config schema defined (`app/src/config/storage.ts`). Buckets, file size limits, image transformation. |
744
+ | **Drivers** | ✅ | Minimal email, SMS, and cache driver interfaces. Configured via `options.drivers`, exposed at `app.drivers`. |
745
+ | **Realtime** | 🔄 | Config schema defined (`app/src/config/realtime.ts`). |
746
+ | **Edge Functions** | 🔄 | Config schema defined (`app/src/config/functions.ts`). Per-function JWT verification, entrypoints. |
747
+ | **Vite plugin** | ✅ | `@supabase/lite/vite` subpath export mounts supalite as middleware in a Vite dev server (`app/src/vite/`). |
748
+
749
+ ---
750
+
751
+ ## Upgrade to Supabase
752
+
753
+ See [UPGRADE.md](https://github.com/supabase-community/lite/blob/HEAD/UPGRADE.md) for the upgrade command contract, target behavior, known gaps, and test strategy.
754
+
755
+ | Capability | Status | Notes |
756
+ |----------------------------|--------|-------------------------------------------------------------------------------------------------|
757
+ | Self-service upgrade | ⚠️ | User-mode CLI flow creates a hosted Supabase project by default, or upgrades the current directory into a local Supabase CLI workdir with `--target local`; applies schema, migrates auth/data, syncs hosted config, and preserves hosted sessions via signing-key import with weak-secret assessment. Disposable local Supabase rehearsals are available as opt-in Docker integration coverage. |
758
+ | Auth schema migration | ⚠️ | Core `auth.*` divergences are documented; auth export has explicit local/Supabase generated-column rules. |
759
+ | User table data migration | ✅ | Emits FK-ordered INSERTs, deserializes SQLite shim-backed fields before Postgres literal generation, and resets serial/bigserial sequences after explicit migrated IDs. |
760
+ | SQLite shim health audit | ✅ | `lite upgrade --dry-run` scans shim-backed fields with affected counts, sample raw values, and sample row IDs; `--json` switches the audit output to structured JSON. |
761
+ | Storage/realtime migration | 🔄 | Upgrade command warns; migration support is deferred until those services land. |
762
+
763
+ ---
764
+
765
+ ## Testing
766
+
767
+ | Test Suite | Passing | Skipped | Failed | Assertions | Files |
768
+ |------------|-------------------|-------------|-----------------|-------------------|----------------|
769
+ | App | **2,221 passing** | 486 skipped | 0 failed | 17,186 assertions | 150 test files |
770
+ | App (vitest: node + D1 + DO + KV) | **34 passing** | 0 skipped | 0 failed | 80 assertions | 4 test files |
771
+ | Repo | **3,248 passing** | 540 skipped | 0 failed | 30,961 assertions | 155 test files |
772
+
773
+ Latest `cd app && bun test`, `cd app && bun run vitest`, and root `bun test --recursive` completed with zero failures.
774
+
775
+ PostgREST spec status (`cd app && bun run test:spec:status`):
776
+
777
+ | Backend | Total | Passing | Pass % | Skipped | Skip % | Failed |
778
+ |---------|------:|--------:|-------:|--------:|-------:|-------:|
779
+ | Postgres | 2,460 | **1,752** | 71.2% | 708 | 28.8% | 0 |
780
+ | PGlite | 2,460 | **1,746** | 71.0% | 714 | 29.0% | 0 |
781
+ | SQLite | 1,702 | **1,439** | 84.5% | 263 | 15.5% | 0 |
782
+ | SQLite-Postgres | 1,702 | **1,479** | 86.9% | 223 | 13.1% | 0 |
783
+
784
+ Auth supabase-spec status (`cd app && bun run test:spec:auth`):
785
+
786
+ | Backend | Total | Passing | Pass % | Skipped | Skip % | Failed |
787
+ |---------|------:|--------:|-------:|--------:|-------:|-------:|
788
+ | Postgres | 488 | **241** | 49.4% | 247 | 50.6% | 0 |
789
+ | PGlite | 488 | **241** | 49.4% | 247 | 50.6% | 0 |
790
+ | SQLite | 488 | **241** | 49.4% | 247 | 50.6% | 0 |
791
+ | SQLite-Postgres | 488 | **241** | 49.4% | 247 | 50.6% | 0 |
792
+
793
+ ### Methodology
794
+
795
+ Feature status tables above are validated by ported test suites run against both the **vendor implementation** and **this project**, ensuring claimed compatibility is real.
796
+
797
+ **PostgREST (Database API):** 501 test cases extracted from the upstream [PostgREST Haskell test suite](https://github.com/PostgREST/postgrest) into JSON specs (`packages/postgrest-test-suite/`). Verified by running against real PostgREST and PostgreSQL via Docker. All 501 pass against vendor. The same suite runs against lite's implementation on four backends (`postgres`, `pglite`, `sqlite`, `sqlite-postgres`), with skips documented per dialect for known incompatibilities (JSON operators, range types, full-text search locale differences). Run `bun run test:spec:status` in `app/` to regenerate the baseline table. `sqlite-postgres` exercises the deparser path end users hit with `driver: "sqlite-postgres"`.
798
+
799
+ **Auth (GoTrue):** 51 test cases across 12 spec files (`packages/gotrue-test-suite/`) covering all 11 implemented endpoints. Verified by running against the vendor GoTrue Docker image (v2.186.0) with PostgreSQL + Inbucket (email trap). The same suite runs against lite's auth implementation on both SQLite and PostgreSQL. The upstream `supabase-spec` Auth JSON cases also run against lite's SQLite auth implementation under `app/test/supabase-spec/auth/`; the baseline now unskips passing email/password/refresh/user, error-shape, health/settings/JWKS, response-shape, email side-effect, logout/reauthenticate DB-change, magic-link, email OTP/verify token, refresh rotation, session lifecycle, short JWT-expiry, non-phone config-variant, recovery password-change, and duplicate-signup/form-token response-shape cases. Remaining Auth skips are hard-scoped product areas or non-runnable upstream placeholder rows documented in `app/test/supabase-spec/auth/NON_RUNNABLE_PLACEHOLDERS.md`; there are no current addressable Auth skips. Run `bun run test:spec:auth:analyze` in `app/` to execute all Auth JSON cases and group current mismatch signatures.
800
+
801
+ Both test suites are reusable packages exposing `definePostgrestTests()` and `defineAuthTests()`. They accept either a URL (for vendor) or a fetch handler (for direct in-process testing).
802
+
803
+ ### Running Tests
804
+
805
+ ```bash
806
+ bun test # all app tests (bun:sqlite connection suite runs here)
807
+ bun test:coverage # all app tests + coverage report
808
+ bun test:postgrest # PostgREST suite against Postgres
809
+ bun test:gotrue # GoTrue suite against vendor Docker
810
+ bun run test:spec:auth # supabase-spec Auth JSON suite against SQLite
811
+ bun run test:spec:auth:analyze # grouped Auth supabase-spec mismatch report
812
+ bun run vitest # node:sqlite + D1 + DO connection suites
813
+ ```
814
+
815
+ **Connection contract suite** (`app/test/db/connection-suite/`): one
816
+ `suite.ts` defines the contract every `SqliteConnection` must honour
817
+ (bind normalization, CRUD, RLS-guarded select, transactions,
818
+ introspection), and runs against each backend via thin per-runner
819
+ entries. `bun:sqlite` uses `bun:test` (runs as part of `bun test`);
820
+ `node:sqlite`, `D1`, and `DurableObject` use `vitest`, with D1/DO routed
821
+ through `@cloudflare/vitest-pool-workers`. See the suite README for scope
822
+ differences per runtime.
823
+
824
+ Full GoTrue test suite:
825
+
826
+ ```bash
827
+ cd packages/gotrue-test-suite && bun test
828
+ ```
829
+
830
+ Full PostgREST test suite:
831
+
832
+ ```bash
833
+ cd packages/postgrest-test-suite && bun run test:postgres
834
+ cd packages/postgrest-test-suite && bun run test:pgserve
835
+ ```
836
+
837
+ ---
838
+
839
+ ## Documentation
840
+
841
+ Detailed API research docs are in `internal/docs/sdk/`:
842
+
843
+ - [`internal/docs/sdk/database/`](https://github.com/supabase-community/lite/blob/HEAD/internal/docs/sdk/database/README.md): 66 database methods with SQLite compatibility analysis
844
+ - [`internal/docs/sdk/auth/`](https://github.com/supabase-community/lite/blob/HEAD/internal/docs/sdk/auth/README.md): 68 auth methods with implementation complexity analysis