# Limitations (quick reference)

Token-efficient cheat sheet for agents and humans. Read this before authoring schema or client code. Detailed semantics, status tables, and workarounds live in [STATUS.md](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md).

Anchors below point to the corresponding STATUS.md section. If a limitation here is fixed, delete the bullet (do not strike-through). If you ship a feature change in STATUS.md, update this file in the same commit (see [AGENTS.md](https://github.com/supabase-community/lite/blob/HEAD/AGENTS.md)).

## SQL / DDL (SQLite path)

- `DEFAULT auth.uid()` (and `auth.role()`, `auth.email()`, `auth.jwt()`) on columns → not supported. Drop the default, pass `user_id` from the client, rely on RLS `WITH CHECK`. See [Column Defaults](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#column-defaults).
- Subquery `WITH CHECK` on `INSERT` (`user_id IN (SELECT …)`, `EXISTS (…)`) → throws. Denormalise the owning column. See [RLS known limitations](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#row-level-security-rls).
- Scalar functions outside the allow-list in `DEFAULT` or `CHECK` (`trim`, `btrim`, `length`, `lower`, `upper`, …) → `Function call "<name>" not supported`. Use literals or move the check to the app layer. See [Column Defaults](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#column-defaults) and [CHECK constraint functions](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#check-constraint-functions).
- `nextval` / `currval`, `clock_timestamp`, `txid_current`, user-defined functions → not supported. See [Column Defaults](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#column-defaults).
- `FORCE ROW LEVEL SECURITY` / `NO FORCE` → accepted and ignored (no table-owner exemption to toggle). See [RLS known limitations](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#row-level-security-rls).
- PL/pgSQL `DECLARE`, `IF`, `LOOP`, `RAISE`, variables → not supported in trigger bodies. See [PL/pgSQL Trigger Functions](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#plpgsql-trigger-functions).

## supabase-js (SQLite path)

- `rpc()` → not supported. Use a regular HTTP endpoint for custom logic. See [Control & Specialized](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#control--specialized).
- Embedded-table dotted-path filters (`eq('rel.col', v)`) → not supported. Filter on a FK column or restructure the query. See [Embedded filters](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#embedded-filters).
- `contains` / `containedBy` / `overlaps` → partial. Arrays of scalars and shallow objects work; arrays of objects and nested objects do not. See [Array & JSON Filters](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#array--json-filters).
- `textSearch` (fts/plfts/phfts/wfts) → not implemented on SQLite. See [Full-Text Search](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#full-text-search).
- `regexMatch` / `regexIMatch` → not implemented on SQLite. See [Regex](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#regex).
- Range operators (`rangeGt`, …) and quantified comparisons (`eq(any)`, …) → not implemented on SQLite. See [Range Operators](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#range-operators) and [Quantified Comparison Operators](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#quantified-comparison-operators).
- `schema()` → SQLite is single-schema. See [Control & Specialized](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#control--specialized).

## Auth (shipped with caveats)

- `double_confirm_changes = true` (secure email change) is spec-compatible but **not** GoTrue's full two-mailbox flow: it finalizes from the current-email confirmation only, so it does not require the new mailbox to also confirm. It still prevents a session thief from changing the email using only a mailbox they control. See [Auth email delivery & templates](https://github.com/supabase-community/lite/blob/HEAD/docs/src/content/docs/auth/email.mdx).
- OAuth / social sign-in (`signInWithOAuth`, `exchangeCodeForSession`) only implements `github` and `google`. Enabling any other configured provider (including `apple`) returns "provider ... is not yet implemented". Automatic account linking on a verified-email match works; manual `linkIdentity()`/`unlinkIdentity()` do not. On the D1 backend, multi-statement Auth transaction spans (OAuth callback/token writes, email-change and other OTP verification) run best-effort without a wrapping transaction (D1 has no callback transaction API; single-statement guards still prevent code/state reuse) — all other backends are fully transactional. See [Auth API: Implemented](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#auth-api-gotrue-compatible).
- Legacy JWT-as-apikey (`ANON_KEY`/`SERVICE_ROLE_KEY` HS256) → not supported. Use the opaque `sb_publishable_*`/`sb_secret_*` keys instead. See [API Keys](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#api-keys).
- API key enforcement is opt-in: with no `auth.publishable_key`/`auth.secret_key` configured, `/rest/v1` and `/auth/v1` accept any/no `apikey` (unchanged old behavior). See [API Keys](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#api-keys).
- A key passed only via `Authorization` (no `apikey` header/query param) → 401. Use `apikey` header or `?apikey=` query param. See [API Keys](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#api-keys).
- No local mailbox UI: `[auth.email.smtp] enabled = true` sends real mail via `SmtpEmailDriver` (Nodemailer; e.g. to Mailpit/Inbucket at `localhost:1025`), but there is no built-in mailbox web UI to browse those messages — use the SMTP server's own UI. `[inbucket]` config is still parsed but not acted on: no local Inbucket-compatible service is started. The default `ConsoleEmailDriver` prints emails (To/Subject/text) to the console instead. `SmtpEmailDriver` requires Node or Bun; it is not supported on Cloudflare Workers or in the browser. See [Auth email delivery & templates](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#auth-api-gotrue-compatible).

## Auth (planned, not yet shipped)

- Other OAuth providers (Apple and the rest of the 18-provider config surface), anonymous sign-in, manual identity linking, admin API, MFA → planned. See [Auth API: Planned](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#-planned).

## Runtime / dev

- `vite preview` mounts the API and runs boot migrations, but does **not** watch schemas and never enables admin mode (it simulates production). `vite build` and standalone production servers do not mount the API at all. See [Vite plugin scope](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#vite-plugin-scope).
- Do not run `lite dev` (or `lite start`) alongside the Vite plugin — both bind the API and collide. See [When to use what](https://github.com/supabase-community/lite/blob/HEAD/README.md#when-to-use-what).
- `lite start` runs the imperative (migrations) workflow. Develop a declarative project (`supabase/schemas/*.sql`) with `lite dev` or the Vite plugin, and ship it by generating a migration: `lite db diff -f <name>` then `lite db reset`. `lite start` works on a declarative project while the cache written by the last `lite dev` run is valid; without a valid cache it is always refused, with no exceptions — it never re-derives RLS from schema files it did not apply, and never substitutes migration-only metadata for them. `lite db diff -f <name>` followed by `lite db reset` is the transition that makes the migration history authoritative, and after it `lite start` boots again. `lite migration up` is not that transition: it is non-destructive, so the generated migration re-creates objects the declarative apply already created and fails against the live development database. `lite db reset` is the related gotcha: it is destructive and replays migrations only, so anything `schemas/*.sql` describes that you never captured with `lite db diff -f` — tables and RLS policies alike — is simply not in the reset database. See [RLS](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#row-level-security-rls).
- `supabase/.temp/.deparse-cache.json` is safe to delete: it is regenerated on the next apply/translate, and a boot that cannot rebuild it fails closed (refuses to serve), never open. Only `sqlite-postgres` uses it. See [RLS](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#row-level-security-rls).
- **Admin mode is on by default locally.** `lite dev`, `lite start`, and the Vite dev server serve *keyless* `/rest/v1` (+ `/storage/v1` on the CLI) requests as `service_role`, so those requests bypass RLS. Credentialed requests are unaffected. Disable with `--no-admin` / `supalite({ admin: false })`. See [API Keys](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#api-keys).
- Admin mode never elevates `/auth/v1`, cross-origin requests, requests from a non-loopback socket, or requests for a non-loopback hostname (DNS rebinding) — so it is not a way to reach a dev server from another machine or from a hostile page. Embedders get the hostname check only, so set `options.server.admin` on a loopback-bound server or not at all.
- The Vite plugin mounts `/rest/v1` but not `/storage/v1`, so admin mode covers storage on the CLI only unless you add the prefix. See [Vite plugin scope](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#vite-plugin-scope).

## Postgres backends (pglite, postgres)

Most SQLite-only limitations above do not apply. `rpc()`, ranges, regex, quantified comparisons, full-text search, and native `DEFAULT auth.uid()` all work on the Postgres path. See per-section status tables in [STATUS.md](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md).

- Custom domain types (data representations): read/`RETURNING` output formats via the domain's `CAST(… AS json)`, and mutating JSON values **into** a domain column (epoch→`timestamptz`, base64→`bytea`, decimal-string→`numeric`) is converted on the Postgres path. Remaining gap: domain formatting through cross-relation embeds. See [Translated Field Types](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#translated-field-types).
- Postgres sessions run in **UTC** by default (so `timestamptz` rendering is deterministic regardless of the server's host timezone). Override via the `postgresOptions.connection.TimeZone` connection option. See [Translated Field Types](https://github.com/supabase-community/lite/blob/HEAD/STATUS.md#translated-field-types).
- Embedding **through views** resolves for views whose FK column is a plain projection of a base-table column — including **materialized views** and **multi-level recursive view-of-view** chains (the column mapping is composed across each hop to the underlying base table). Views whose FK column is not a plain projection (CTE, GROUP BY, subselect-in-FROM, or any JOIN view) are still not traced. View-FK resolution matches relations by **bare name**: if two exposed schemas expose a relation with the same name, an embed through a view may resolve against the wrong one — keep colliding names out of co-exposed schemas, or embed via an explicit FK/constraint-name hint.

## Anti-patterns

Common ways code goes wrong against supalite. The fix for each is the corresponding bullet above.

- Don't put `DEFAULT auth.uid()` on a column. Drop the default; pass `user_id` from the client; let RLS `WITH CHECK` enforce ownership.
- Don't call `rpc()` on the SQLite path. Run a regular HTTP endpoint, or switch the driver to `pglite` / `postgres` in `config.toml`.
- Don't use embedded dotted-path filters (`.eq('rel.col', v)`) on SQLite. Filter the FK column on the parent, or fetch matching ids first.
- Don't run `lite dev` or `lite start` next to the Vite plugin — port collision.
- Don't `lite db reset` then `lite start` on a declarative project and expect your schema to be there. Reset is destructive and replays migrations only, adopting that state (RLS included) as the authoritative one — run `lite db diff -f <name>` first so the declarative schema exists as a migration.
- Don't test RLS with a keyless request while admin mode is on — it runs as `service_role` and sees everything. Send `apikey: $PUBLISHABLE_KEY` (for `anon`), plus `Authorization: Bearer $USER_JWT` for `authenticated`, or start with `--no-admin`.
- Don't send only `Authorization: Bearer $USER_JWT` and expect `authenticated` RLS. With keys configured that's a 401 — the `apikey` is required as well.
- Don't rely on `vite preview` for a production-like surface beyond the API mount: it skips schema watching and admin mode, but it is still a dev tool. Use `lite start` or a real backend for non-dev environments.
- Don't reach for `trim` / `length` / `lower` inside `CHECK` constraints on SQLite — use literal/operator comparisons.
