Files
supabase/apps/docs
Miranda Limonczenko ef3cfad9ed docs(database): add access control to the tables guide (#50024)
Closes DOCS-1314

Brings the Eval to green.

Part 4 of a 5-PR stack on
`apps/docs/content/guides/database/tables.mdx`. Builds on #50023.

## Problem

The page taught table creation and never said to protect a table. Across
the original 573 lines, "row level security" appeared once, and that
mention was about `security_invoker` on views. There was no `enable row
level security`, no policy, and no `auth.uid()` anywhere.

The gap was uneven between the two paths the page offers. The Table
Editor enables row level security by default and warns that a table
without it is publicly writable and readable. The SQL path on the same
page produced an unprotected table and said nothing about it.

Two further gaps followed from that one:

- **Every example was a single access class.** `movies`, `categories`,
`actors`, `performances`, and `private.salaries` are all the same shape.
The page had no pattern for what most applications actually look like: a
shared table everyone reads sitting beside a per-person table only its
owner reads. The shared one is the one that gets skipped.
- **Nothing told the reader to check the result.** No verify, no
confirm, and no expected output anywhere on the page.

## Solution

Adds a "Securing your tables" section between creating a table and
loading data:

- Enabling row level security and writing a first policy, with the
consequence stated: a table with row level security and no policy
returns no rows to anyone.
- A worked example with two access classes, `movies` and `watchlists`.
- A verification step. Two queries against `pg_tables` and `pg_policies`
confirm that every table exists, has row level security enabled, and has
at least one policy.

Policy examples follow the idioms in the Row Level Security guide,
including the wrapped `(select auth.uid())` form. The guide is
cross-referenced rather than restated.

## Verification (`test-the-docs`)

All 22 SQL fences on the page were run in document order against a local
stack, the way a reader pasting top to bottom would.

| Snippet / step | Class | Result | Notes |
| --- | --- | --- | --- |
| `create table movies` (Creating tables) | runnable-local | pass | |
| `create table movies` ×2 (Primary keys) | illustrative-only | skipped
| Re-shows the same table to explain `identity`; not a continuation |
| `alter table movies enable row level security` | runnable-local | pass
| |
| `create policy "Anyone can read movies"` | runnable-local | pass | |
| `create table watchlists` + 2 policies | runnable-local | pass | |
| Verification query, `pg_tables` | runnable-local | pass | Lists both
tables with `rowsecurity` true |
| Verification query, `pg_policies` | runnable-local | pass | Lists all
three policies |
| `insert into movies` (Basic data loading) | runnable-local | pass | |
| `create table categories` + foreign key | runnable-local | pass | |
| `create table actors` / `performances` | runnable-local | pass |
Failed before this stack; see below |
| `create schema private` | runnable-local | pass | |
| `create table private.salaries` | runnable-local | pass | Failed
before this stack; see below |
| Views section, 9 fences | illustrative-only | skipped | Depend on
`students`, `courses`, and `grades`, which the page shows as rendered
tables and never creates |

**Tier A path:** `movies` → enable RLS → policy → `watchlists` +
policies → both verification queries → `insert into movies` →
`categories` + FK → `actors`/`performances` → `private` schema →
`private.salaries`. Runs clean end to end.

**Tier B, RLS behavior.** Every access claim in "Securing your tables"
was exercised with two real users:

| Check | Expected | Observed |
| --- | --- | --- |
| User A inserts into their own watchlist | succeeds | succeeds, A sees
1 row |
| User B reads A's rows | 0 rows | 0 rows |
| `anon` reads `movies` | rows returned | 2 rows |
| `anon` reads `watchlists` | 0 rows | 0 rows |
| User B inserts a row owned by A | rejected | `new row violates
row-level security policy for table "watchlists"` |

**Environment:** Compose sandbox (`sandbox/run.sh up-stack`), DinD +
`supabase start`, Postgres 17. Fences ran in-container only, never on
the host.

**Note on the sandbox.** `supabase start` inside the nested daemon hit
repeated `toomanyrequests: Rate exceeded` from ECR Public. The CLI
retries and the stack does come up, but expect a slow first run.

## What this PR leaves to the one above it

Data type guidance is #50025.

## Manual testing

Preview:
https://docs-git-docs-tables-rls-supabase.vercel.app/docs/guides/database/tables#securing-your-tables

1. Open the preview at that anchor. The three subsections render, and
the numbered steps show their embedded SQL blocks.
2. In a local project, run the `movies` and `watchlists` snippets, then
the two verification queries. Both tables appear with `rowsecurity` true
and at least one policy each.
3. As a signed-out client, select from `movies` and from `watchlists`.
`movies` returns rows; `watchlists` returns none.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Expanded the database tables guide with instructions to secure tables
before adding rows.
- Added guidance on enabling row-level security and creating policies
for shared and per-person tables.
- Clarified that policies control row access, while revoked table grants
can cause permission errors.
- Documented owner-scoped access using authenticated user IDs and ways
to verify table protection.
- Explained that read-only policies reject inserts through the Data API
and provided alternatives.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-14 18:49:11 -07:00
..
2026-07-01 12:59:00 +02:00

Reference Docs

Supabase Reference Docs

Maintainers

If you are a maintainer of any tools in the Supabase ecosystem, you can use this site to provide documentation for the tools & libraries that you maintain.

DocSpec

We use documentation specifications which can be used to generate human-readable docs.

  • OpenAPI: for documenting API endpoints.
  • SDKSpec (custom to Supabase): for SDKs and client libraries.
  • ConfigSpec (custom to Supabase): for configuration options.
  • CLISpec (custom to Supabase): for CLI commands and usage.

The benefit of using custom specifications is that we can generate many other types from a strict schema (eg, HTML and manpages). It also means that we can switch to any documentation system we want. On this site we use Next.js, but on Supabase's official website, we use a custom React site and expose only a subset of the available API for each tool.

Contributing

To contribute to docs, see the developers' guide and contributing guide.