diff --git a/.claude/skills/docs-content/SKILL.md b/.claude/skills/docs-content/SKILL.md new file mode 100644 index 0000000000000..a24486efb6410 --- /dev/null +++ b/.claude/skills/docs-content/SKILL.md @@ -0,0 +1,97 @@ +--- +name: docs-content +description: Write, edit, organize, and review Supabase content anywhere in apps/docs — guides, explainers, tutorials, troubleshooting entries, reference docs, and partials. Use for MDX/TOML authoring, frontmatter, navigation, terminology, links, code samples, content listings, and docs validation. +--- + +# Supabase docs authoring + +## Sources of truth + +Before changing docs content: + +1. Read `apps/docs/CONTRIBUTING.md` for content types, structure, components, and + style. +2. Read `apps/docs/WORD_LIST.md` for preferred terminology, spelling, and + capitalization. +3. Inspect nearby content of the same type and the relevant navigation section + before deciding on file placement or structure. Guides, explainers, and + tutorials live under `apps/docs/content/guides`. Troubleshooting entries live + under `apps/docs/content/troubleshooting` and use TOML frontmatter — follow + `_template.mdx` in that directory rather than a guide's YAML frontmatter. + Reference docs are generated from `apps/docs/spec` and library source, so + look for the spec file or repo definition instead of editing rendered output + directly. + +When guidance conflicts, follow `apps/docs/CONTRIBUTING.md`. Match literal code, +API names, UI labels, and third-party product names even when they differ from the +word list. + +## Writing workflow + +1. Identify the document type: explainer, tutorial, guide, or reference, per + `apps/docs/CONTRIBUTING.md`. A guide is a concise procedure for a targeted + task; a tutorial covers a larger goal and includes more explanatory context; + an explainer is conceptual and prose-based; reference content is factual, + like a dictionary entry. Troubleshooting entries follow their own TOML + structure rather than these four types. +2. Define the reader's goal and prerequisites before drafting. +3. Classify substantial sections as contextual, procedural, or reference content. + In a mixed page, group sections by information type so that context doesn't + interrupt the procedural path. +4. For a long or mixed page, add a short introduction that links to its major + section groups and tells readers when to use each one. Skip this navigation + when a short page is already easy to scan. +5. Connect contextual sections to their corresponding procedures when useful. + Add introductions to section groups, transitions between information types, + and outcomes after procedures. Don't link every adjacent section. +6. Use second person, present tense, short paragraphs, and ordered steps for + sequential actions. +7. Search `apps/docs/WORD_LIST.md` when introducing or reviewing technical terms, + UI actions, abbreviations, and potentially ambiguous language. +8. Keep code samples executable in their stated context and consistent with + repository formatting. Clearly mark intentionally omitted code. Use lowercase + SQL keywords. +9. Reuse repeated content through `apps/docs/content/_partials` instead of copying + it. +10. Add new guide, explainer, and tutorial pages to + `apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts`. + File placement alone doesn't add a page to navigation. Troubleshooting + entries are indexed automatically and don't need a navigation entry. +11. Use `/docs/...` paths for pages in Supabase docs and site-root paths such as + `/dashboard` for pages outside docs. Use descriptive link text and sparse + admonitions with the appropriate severity. + +## Validation + +From `apps/docs`, run: + +```bash +pnpm lint:mdx +pnpm build:guides-markdown +``` + +`pnpm lint:mdx` covers all content under `apps/docs/content`, including +troubleshooting entries. `pnpm build:guides-markdown` only applies to guides, +explainers, and tutorials. + +From the repository root, run `pnpm format` to apply Prettier to any changed +MDX (and other) files. This enforces repo-wide formatting rules, including +lowercase SQL keyword casing in code samples. + +Run broader type checking or tests when the change affects MDX components, +content listings, navigation code, or generated output. + +For a mixed page, verify that context and procedures are grouped, introductory +navigation links resolve to the intended sections, related context and procedures +are cross-referenced where useful, and transitions make the reading path clear. + +Treat lint replacements as suggestions when context matters. Rewrite the sentence +instead of applying a replacement that changes its technical meaning. + +Anchor IDs are generated from heading text at render time, and nothing in CI +checks that `#anchor` links still resolve. Before renaming, removing, or +substantially rewording a heading, run +`grep -rn "#" apps/docs/content` to find in-page and +cross-file links that target it, and update every match. If a heading needs a +stable anchor independent of its wording, pin it with a custom anchor, for +example `## Some heading [#some-heading]`. diff --git a/apps/docs/CONTRIBUTING.md b/apps/docs/CONTRIBUTING.md index 6a3027b177056..fac90e1aa4706 100644 --- a/apps/docs/CONTRIBUTING.md +++ b/apps/docs/CONTRIBUTING.md @@ -8,13 +8,14 @@ Here are some general guidelines on writing docs for Supabase. ## General principles -Docs should be helpful, quick to read, and easy to understand. We have an audience of global readers who speak different native languages. +Write helpful, concise, and understandable documentation. We have a global audience whose members speak different native languages. To make docs as clear as possible: - Write for the user. Think about what task they want to complete by reading your doc. Tell them what, and only what, they need to know. -- Write like you talk. Use words and sentences that sound natural when speaking. Cut unnecessary words. Read your writing out loud to help you choose the clearest and simplest phrases. -- Each paragraph should have one topic only. Start a new paragraph whenever you change the topic. Don't worry about paragraphs being too short. +- Write like you talk. Conversational English is easier for a global audience to understand and localize. Many readers who use English as an additional language learn conversational rather than academic English. Use words and sentences that sound natural when speaking. Cut unnecessary words. Read your writing out loud to help you choose the clearest and simplest phrases. +- Prefer short, direct sentences. Express one relationship at a time, and avoid unnecessary compound structures. This makes each sentence easier to understand, localize, and interpret consistently. +- Cover one topic in each paragraph. Start a new paragraph whenever you change the topic. Don't worry about paragraphs being too short. - Avoid using idioms and colloquialisms, such as `piece of cake`. These phrases are often specific to a region or culture. - Refer to the reader as `you`. Don't use `we` to refer to the reader. Use `we` only to refer to the Supabase team. @@ -31,7 +32,7 @@ Explainers help the reader to learn a topic. They are conceptual and mostly pros - Some examples of _when_ to use it - A high-level explanation of _how_ it works -They shouldn't include: +Explainers don't include: - Instructions on how to use it @@ -39,7 +40,7 @@ They shouldn't include: Tutorials are goal-oriented. They help a reader to finish a large, complex goal, such as setting up a web app that uses multiple Supabase features. -Tutorials mix prose explanations with procedures (lists of steps for the reader to follow). They provide context for why certain instructions are given. +Tutorials mix prose explanations with procedures. Procedures are lists of steps for the reader to follow. Tutorials provide context for why certain instructions are given. For inspiration, see [an example of a tutorial](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs). @@ -47,22 +48,35 @@ For inspiration, see [an example of a tutorial](https://supabase.com/docs/guides Guides are also goal-oriented, but they focus on shorter, more targeted tasks. For example, a guide might explain how to set up user login for an app. -Guides contain mostly procedures. Think of an instruction manual for building a desk: it's a list of concise steps that the user can go through quickly. +Guides contain mostly procedures: concise steps that readers can follow in sequence. -For inspiration, see [an example of a guide](https://supabase.com/docs/guides/auth/auth-email). +Begin each guide with a sentence that declares its intent, such as `This guide explains how to set up email login.` This helps readers and agents confirm that the guide matches their goal and expected outcome. + +Keep procedures focused on what the reader must do. Move substantial background or conceptual explanations into a separate section or an explainer. Cross-reference the authoritative explanation instead of repeating it in the procedure. This keeps the action path scannable, gives readers optional depth, and maintains one source of truth. + +- Recommended: `This guide explains how to enable Row Level Security. To learn how Row Level Security controls access, see [Row Level Security](...).` +- Not recommended: Begin with several paragraphs about how Row Level Security works before stating what the guide helps the reader do. + +**Mixed information types:** When a guide contains substantial context or reference material, group sections by information type. Keep contextual and reference sections separate from the procedure group so that background information doesn't interrupt the action path. + +**Navigation:** Begin a long guide with a short outline of its major section groups. Link to each group and state when a reader should use it. Don't add section navigation to a short guide when the headings are already easy to scan. + +**Cross-references and glue:** Connect contextual sections to their corresponding procedures when the relationship helps readers navigate. Add a brief introduction to each section group, a transition when the information type changes, and an outcome after a procedure. Add links selectively rather than linking every adjacent section. + +For inspiration, see [an example of a guide](/docs/guides/auth/auth-email-passwordless). ### Reference References are factual and to the point. Think of dictionary entries. -They should include: +References include: - Function parameters - Return types - Code samples -- Warnings for critical errors (for example, missteps that can cause data loss) +- Warnings about critical errors, such as missteps that can cause data loss -They shouldn't include: +References don't include: - Explanations of the context for a feature - Examples of use cases @@ -72,7 +86,7 @@ They shouldn't include: Most docs pages are contained in the `apps/docs/content` directory. Some docs sections are federated from other repositories, for example [`pg_graphql`](https://github.com/supabase/pg_graphql/tree/master/docs). Reference docs are generated from spec files in the `spec` directory. -You can usually identify a federated or reference doc because it uses a Next.js dynamic route (for example, `[[...slug]].tsx`). Look for the spec file import or the repo definition to find the content location. +You can usually identify a federated or reference doc because it uses a Next.js dynamic route. For example, it might use `[[...slug]].tsx`. Look for the spec file import or the repo definition to find the content location. Example spec file import: @@ -94,12 +108,12 @@ Check the sections for [guide structure](#guide-structure) and [reference struct ## Guide structure -The Supabase docs use [MDX](https://mdxjs.com/). Guides are written in unstructured prose as MDX documents. +The Supabase docs use [MDX](https://mdxjs.com/). Guides are MDX documents that combine concise prose with structured procedures. Adding a new guide requires: - YAML frontmatter -- A navigation entry (in a separate file) +- A navigation entry in a separate file Frontmatter looks like this. `title` is mandatory. There are also optional properties that you can use to control the page display, including `subtitle`, `tocVideo`, and `hideToc`. @@ -112,7 +126,7 @@ hideToc: true The navigation is defined in [`NavigationMenu.constants.ts`](https://github.com/supabase/supabase/blob/master/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts). -Add an entry with the `name`, `url`, and (optional) `icon` for your page. +Add an entry with the `name`, `url`, and optional `icon` for your page. ## Reference structure @@ -120,13 +134,13 @@ Reference docs are produced from the reference specs and library source code. A ### Common spec file -Each type of library (for example, language SDK or CLI) has a common spec file. For example, see the [spec file for the language SDKs](https://github.com/supabase/supabase/blob/master/apps/docs/spec/common-client-libs-sections.json). This file contains definitions for the common SDK functions: +Each type of library, such as a language SDK or CLI, has a common spec file. For example, see the [spec file for the language SDKs](https://github.com/supabase/supabase/blob/master/apps/docs/spec/common-client-libs-sections.json). This file contains definitions for the common SDK functions: -- **id** - Identifies the function -- **title** - Human-readable title -- **slug** - URL slug -- **product** - Supabase product that owns the function. For example, database operations are owned by `database`, and auth functions are owned by`auth` -- **type** - `function` for a structured function definition or `markdown` for a prose explainer section. +- `id`: Identifies the function +- `title`: Provides the human-readable title +- `slug`: Provides the URL slug +- `product`: Identifies the Supabase product that owns the function. For example, database operations are owned by `database`, and Auth operations are owned by `auth`. +- `type`: Uses `function` for a structured function definition or `markdown` for a prose explainer section To add a new function, manually add an entry to this common file. @@ -140,10 +154,10 @@ Each function contains a description, code examples, and optional notes. The par If you're a library maintainer, follow these steps when updating function parameters or return values: -1. Get your changes merged to `master` in your library -2. This will kick off an action that automatically updates the spec file in the library's `gh-pages` branch -3. Run `make` in `/spec` of the `supabase/supabase` repo. This will regenerate all of the `tsdoc` files that the docs site uses -4. You should now see the changes you've made in the docs site locally +1. Merge your changes into the library's `master` branch. +2. Wait for the action to update the specification in the `gh-pages` branch. +3. Run `make` from `apps/docs/spec` in the `supabase/supabase` repository. +4. Verify the changes on your local documentation site. ## Content reuse @@ -153,23 +167,32 @@ To use a partial, import it into your MDX file. You can also set up a partial to ## Components and elements -Docs include normal Markdown elements such as lists, and custom components such as admonitions (callouts). +Docs include normal Markdown elements such as lists and custom components such as admonitions, also known as callouts. Here are some guidelines for using elements: ### Admonitions -Admonitions (or callouts) draw reader attention to an important point or an aside. They highlight important information, but get less effective if they're overused. +Admonitions draw reader attention to an important point or an aside. They highlight important information, but get less effective if they're overused. -Use admonitions sparingly. Don't stack them on top of each other. +Use an admonition when a reader might otherwise miss information that affects the outcome of their task, or when you want to separate helpful but optional guidance from the main flow. Don't use an admonition for information that belongs in the main explanation or procedure. + +Use admonitions sparingly. Don't stack them on top of each other or use them as decoration. + +Begin every admonition with its impact and purpose: the "so what." Use the first sentence to tell the reader why the information matters, such as what could happen, what changes, or what benefit they gain. Add background or instructions after the impact is clear. + +For example: + +- Recommended: `Deleting this project permanently removes its database and backups. Export any data that you want to keep before you continue.` +- Not recommended: `Before you continue, there are a few things that you should know about project deletion.` Choose the appropriate `type` for your admonition: -- `danger` to warn the user about any missteps that could cause data loss or data leaks -- `deprecation` to notify the user about features that are (or will soon be) deprecated -- `caution` to warn about anything that could cause a bug or serious user inconvenience -- `tip` to point out helpful but optional actions -- `note` for anything else +- `danger`: Warn about actions or conditions that could cause data loss, expose sensitive data, or create another severe and difficult-to-reverse outcome. State the consequence first, and then explain how to avoid it. +- `deprecation`: Identify a deprecated feature or behavior. State how the change affects the reader, and then provide the supported alternative or migration path. +- `caution`: Warn about behavior that could cause bugs, failed operations, unexpected results, or serious inconvenience but doesn't rise to the severity of `danger`. +- `tip`: Share an optional shortcut, optimization, or best practice that helps the reader complete the task more effectively. The main procedure must still work without it. +- `note`: Highlight an important prerequisite, constraint, or clarification that doesn't represent a risk. If the information is essential to completing a step, include it in the procedure instead. ``` @@ -189,13 +212,13 @@ Keep code lines short to avoid scrolling. For example, you can split long shell - **JavaScript/TypeScript** - The `supabase` repo uses Prettier, which also formats JS/TS in code blocks. Your PR is blocked from merging if the Prettier check fails. Ensure that your code blocks are formatted by running `npm run format`, or by setting up auto-formatting in your IDE. + The `supabase` repository uses Prettier, which also formats JS/TS in code blocks. Your PR is blocked from merging if the Prettier check fails. From the repository root, run `pnpm format`, or set up automatic formatting in your IDE. - **SQL** Prefer lowercase for SQL. For example, `select * from table` rather than `SELECT * FROM table`. -Optionally specify a filename for the codeblock by including it after the opening backticks and language specifier: +Optionally specify a filename for the code block by including it after the opening backticks and language specifier: ````md ```ts environment.ts @@ -211,6 +234,16 @@ Optionally highlight lines by using `mark=${lineNumber}`. ``` ```` +### Emphasis + +Use **bold**, _italics_, and `code` formatting for distinct purposes. Don't use them interchangeably or to add visual emphasis alone. + +- **Bold**: Mark UI labels the reader interacts with, such as buttons, menu items, and field names. For example, `Click **Save**.` Also use bold for a term the reader must not miss, such as `**Never** commit your service role key.` +- _Italics_: Introduce a new term the first time you define it, or reference a title, such as a book or a third-party product name written in italics by convention. Use italics sparingly. Don't use italics for UI labels or for general emphasis. +- `Code`: Mark anything the reader types or copies verbatim, or anything the system reads literally. This includes filenames, paths, commands, flags, environment variables, function and parameter names, configuration keys, and literal values. For example, `` Set `SUPABASE_URL` in your `.env` file. `` + +If a phrase fits more than one category, pick the most specific one. A command name is `code`, not **bold**, even though the reader also interacts with it. + ### Content listings Overview and index pages use a single `` component for curated link sections such as "Get started", "Next steps", "Examples", or "Resources". Refer to [`storage.data.ts`](data/content-listings/storage.data.ts) and [`storage.mdx`](content/guides/storage.mdx) for a full example. @@ -218,7 +251,7 @@ Overview and index pages use a single `` component f **Prompt to add content listings:** ```text -Add a content listing block for [TOPIC] / [SECTION] (for example, Storage / Examples). +Add a content listing block for [TOPIC] / [SECTION]. For example, use Storage / Examples. Follow CONTRIBUTING § Content listings in apps/docs. Copy structure from `storageGetStarted` in apps/docs/data/content-listings/storage.data.ts. Pick a globally-unique kebab-case id like `[topic]-[section]`. @@ -227,11 +260,11 @@ Run `pnpm test:local lib/content-listings.test.ts` from apps/docs. **Manually add content listings:** -1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups (e.g. `storage-get-started`, not just `get-started`) — it is used both as the lookup key and as the telemetry `listingId`. +1. Add or update a `ContentListingGroup` export in [`data/content-listings/[topic].data.ts`](data/content-listings/). The `id` field must be globally unique across all listing groups. For example, use `storage-get-started` rather than `get-started`. The ID is both the lookup key and the telemetry `listingId`. 2. Place the component inline in guide MDX, for example ``. Use a partial only when the block is reused or gated with `$Show` at the partial level. 3. Run `pnpm test:local lib/content-listings.test.ts` from `apps/docs`. -Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets): `cl-data` (data export with namespaced id) and `cl-inline` (MDX component). +Code snippets for manually adding content listings are available in [`.vscode/content-listing.code-snippets`](../../.vscode/content-listing.code-snippets). Use `cl-data` for a data export with a namespaced ID. Use `cl-inline` for an MDX component. ### Footnotes @@ -240,7 +273,7 @@ Don't use footnotes. ### Graphs -Render diagrams (flowcharts, sequence diagrams, entity-relationship diagrams, etc.) by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light/dark mode automatically. +Render diagrams, including flowcharts, sequence diagrams, and entity-relationship diagrams, by writing a fenced code block with `mermaid` as the language. The MDX renderer routes these blocks through the shared `Mermaid` component, so theming follows light and dark mode automatically. For the full list of supported diagram types and their syntax, see the [official Mermaid diagram reference](https://mermaid.js.org/intro/syntax-reference.html). @@ -259,7 +292,7 @@ sequenceDiagram ``` ```` -Flowchart (`flowchart` accepts a direction like `LR`, `TD`, etc.): +The `flowchart` keyword accepts a direction such as `LR` or `TD`: ````mdx ```mermaid @@ -273,9 +306,9 @@ flowchart LR A few tips: -- Use the standard Mermaid diagram keywords (`sequenceDiagram`, `flowchart`, `erDiagram`, etc.) on the first line of the block. +- Use a standard Mermaid diagram keyword, such as `sequenceDiagram`, `flowchart`, or `erDiagram`, on the first line of the block. - Keep diagrams focused on a single flow or concept. If a diagram gets too dense, split it into multiple smaller diagrams. -- Wrap node labels that contain special characters (`*`, `/`, spaces, punctuation) in double quotes, for example `A["content/**/*.md"]`. +- Wrap node labels that contain special characters in double quotes. Special characters include `*`, `/`, spaces, and punctuation. For example, use `A["content/**/*.md"]`. - Don't hardcode colors. The component themes the diagram automatically so it matches both light and dark mode. - Use diagrams to support the prose, not replace it. Explain the key takeaway in text near the diagram. @@ -283,17 +316,34 @@ A few tips: Images are uploaded in the `apps/docs/public/img` folder. -For vector illustrations, use `svg`. For screenshots and non-vector graphics, use `png`. (These are automatically converted to `webp` for supported browsers.) +For vector illustrations, use `.svg` files. For screenshots and non-vector graphics, use `.png` files. Supported browsers receive `.webp` versions automatically. Redact any sensitive information, such as API keys. ### Links -Link text should be descriptive. The reader should understand where the link goes from reading the link text alone. This is important for accessibility. For example, don't use `here` as link text. +Use descriptive link text that tells the reader where the link goes. This is important for accessibility. For example, don't use `here` as link text. + +Keep link text concise. Use the shortest part of the link that is descriptive enough. For example, `see the [reference section](/link)` rather than `[see the reference section](/link)`. + +Don't include the `https://supabase.com` origin when linking to pages on `supabase.com`. Use a `/docs/...` path for a page in Supabase docs, such as `[getting started](/docs/guides/getting-started)`. Use a site-root path for a page outside docs, such as `[open the Supabase Dashboard](/dashboard)`. + +### Procedures + +Use a procedure when a human or agent must perform actions to reach an outcome. The procedural format makes that expectation explicit. -But link text shouldn't be too long. Use the shortest part of the link that is descriptive enough. For example, `see the [reference section](/link)` rather than `[see the reference section](/link)`. +Write sequential actions as an ordered list. Begin each step with an imperative verb, and include one action or a closely related set of actions per step. Give the reader enough context to know where to act. -Use relative links when linking within the `supabase.com` domain. For example, `[link to another page in Supabase docs](/docs/guides/getting-started)`. +Apply the [Information Mapping chunking principle](https://informationmapping.com/blogs/news/writing-for-the-web-the-magical-number-seven-plus-or-minus-two) to procedures. Present 7 ± 2 related steps at a time. This gives readers a manageable chunk of five to nine actions. Aim for the lower end of the range when the task is complex or unfamiliar. + +If a procedure has more than nine steps, group related steps into named phases or smaller procedures. If one step contains multiple distinct actions, split it into separate steps. Don't add steps to reach a minimum. The range is a guideline for organizing information, not a required procedure length. + +An apparent one-step procedure can become two steps when there is a real orientation action. For example: + +1. Open a terminal in your project directory. +2. Run `supabase start`. + +The first step establishes the operating context for both readers and agents. Don't add a redundant orientation step to a genuinely atomic instruction. For example, write `Click **Save**.` instead of adding `Locate the **Save** button` as a separate step. ### Lists @@ -319,7 +369,7 @@ Don't nest lists more than two deep. Use tabs to provide alternative instructions for different platforms or languages. -The `queryGroup` param is optional. It lets you link directly to a tab by using the query group as a query param in the URL, for example: `https://supabase.com/docs/my-page?packagemanager=ts` +The optional `queryGroup` prop lets you link directly to a tab. For this example, use `/docs/my-page?packagemanager=npm`. ``` + +### Request information + +Use the Postgres `current_setting()` function to access request information: + +```sql +-- Get all headers sent in the request +select current_setting('request.headers', true)::json; + +-- Get one header with a JSON arrow operator +select current_setting('request.headers', true)::json->>'user-agent'; + +-- Get cookies +select current_setting('request.cookies', true)::json; +``` + +| `current_setting()` | Example | Description | +| ------------------- | ----------------------------------------------- | ------------------------------------ | +| `request.method` | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` | Request's method | +| `request.path` | `table` | Table's path | +| `request.path` | `view` | View's path | +| `request.path` | `rpc/function` | Function's path | +| `request.headers` | `{ "User-Agent": "...", ... }` | JSON object of the request's headers | +| `request.cookies` | `{ "cookieA": "...", "cookieB": "..." }` | JSON object of the request's cookies | +| `request.jwt` | `{ "sub": "a7194ea3-...", ... }` | JSON object of the JWT payload | + +To access the client's IP address, look up the `X-Forwarded-For` header in the `request.headers` setting: + +```sql +select split_part( + current_setting('request.headers', true)::json->>'x-forwarded-for', + ',', 1); -- takes the client IP before the first comma +``` + +See [Pre-request](https://postgrest.org/en/stable/references/transactions.html#pre-request) in the PostgREST documentation and [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) in the MDN documentation. + +For complete implementations that use this request information, see [Pre-request examples](#pre-request-examples). + +### Error responses + +A pre-request function can raise an exception to stop a request. This example returns an HTTP 402 Payment Required response with a `hint` and an `X-Powered-By` header: + +```sql +raise sqlstate 'PGRST' using + message = json_build_object( + 'code', '123', + 'message', 'Payment Required', + 'details', 'Quota exceeded', + 'hint', 'Upgrade your plan')::text, + detail = json_build_object( + 'status', 402, + 'headers', json_build_object( + 'X-Powered-By', 'Nerd Rage'))::text; +``` + +The exception produces this HTTP response: + +```http +HTTP/1.1 402 Payment Required +Content-Type: application/json; charset=utf-8 +X-Powered-By: Nerd Rage + +{ + "message": "Payment Required", + "details": "Quota exceeded", + "hint": "Upgrade your plan", + "code": "123" +} +``` + +Use JSON functions and operators to build dynamic responses from exceptions. Include the `status_text` key in the `detail` clause when you use a custom HTTP status code such as 419. See [JSON Functions and Operators](https://www.postgresql.org/docs/current/functions-json.html) in the Postgres documentation. + +For PostgREST 11 or earlier, use the legacy syntax for raising errors. [Check your PostgREST version](/dashboard/project/_/settings/infrastructure) in the Dashboard. See [Raise errors with HTTP status codes](https://postgrest.org/en/stable/references/errors.html#raise-errors-with-http-status-codes) in the PostgREST documentation. + +## Configure Data API security + +This section groups the procedures for configuring each security control. Apply the procedures that match your architecture. + +### Grant access explicitly A table isn't reachable through the Data API unless you have granted a role privileges on it. Grant the minimum privileges each role needs. For example: @@ -18,7 +144,7 @@ A table isn't reachable through the Data API unless you have granted a role priv -- Read-only access for anonymous clients grant select on table public.your_table to anon; --- Full access for signed-in users (still subject to RLS) +-- Full access for signed-in users; RLS still applies grant select, insert, update, delete on table public.your_table to authenticated; -- Full access for server-side code using the service role @@ -38,47 +164,34 @@ If a required grant is missing, PostgREST returns a `42501` error with a hint th } ``` -See [the Database API 42501 errors troubleshooting guide](/docs/guides/troubleshooting/database-api-42501-errors) for the full troubleshooting flow. - - +See [Database API 42501 errors](/docs/guides/troubleshooting/database-api-42501-errors) for the full troubleshooting flow. -Bundle grants with your RLS setup in the same migration. They belong together: `grant` controls role access, `enable row level security` and policies control row access. +**Migration:** Bundle grants with your RLS setup in the same migration. The `grant` command controls role access. The `enable row level security` command and policies control row access. - - -## Default privileges for new tables and functions +### Revoke default privileges -By default on existing projects, tables and functions you create in `public` are automatically granted `SELECT`, `INSERT`, `UPDATE`, `DELETE` (or `EXECUTE` for functions) to `anon`, `authenticated`, and `service_role`. That means a new table is reachable through the Data API the moment it lands, even if you forgot to enable RLS or did not intend to expose it. +Revoke automatic grants when you want new objects in `public` to remain inaccessible until you grant access: -Supabase is moving the platform default to **revoke** these automatic grants, so that exposure becomes opt-in, read more about the change in [this changelog entry](https://github.com/orgs/supabase/discussions/45329). +1. Open the [SQL Editor](/dashboard/project/_/sql/new). +2. Run the following statements: -To opt an existing project in today, open the [SQL Editor](/dashboard/project/_/sql/new) and run: + ```sql + alter default privileges for role postgres in schema public + revoke select, insert, update, delete on tables from anon, authenticated, service_role; -```sql -alter default privileges for role postgres in schema public - revoke select, insert, update, delete on tables from anon, authenticated, service_role; + alter default privileges for role postgres in schema public + revoke execute on functions from anon, authenticated, service_role; -alter default privileges for role postgres in schema public - revoke execute on functions from anon, authenticated, service_role; + alter default privileges for role postgres in schema public + revoke usage, select on sequences from anon, authenticated, service_role; -alter default privileges for role postgres in schema public - revoke usage, select on sequences from anon, authenticated, service_role; + alter default privileges for role postgres in schema public + revoke execute on functions from public; + ``` -alter default privileges for role postgres in schema public - revoke execute on functions from public; -``` - - - -These default privileges pose no direct security risk. They appear in [pg_default_acl](https://www.postgresql.org/docs/current/catalog-pg-default-acl.html), granted by [supabase_admin](/docs/guides/database/postgres/roles#supabaseadmin) to `anon`, `authenticated`, and `service_role`. The default privileges are intentional and part of Supabase's standard permission model. The `supabase_admin` role is an internal management role that can't authenticate through the Data API. - - +New tables, functions, and sequences now require explicit grants before Data API roles can access them. -## Use a dedicated API schema - -If you want an extra boundary around your Data API, lock down the `public` schema and expose a dedicated schema, such as `api`, instead. You can control access with grants in any schema, but this can make the surface easier to reason about: objects in `api` represent your Data API, while internal tables and helper functions stay in schemas that are not exposed. See [Using Custom Schemas](/docs/guides/api/using-custom-schemas) for setup steps. - -## Disable the Data API +### Disable the Data API If your app never uses Supabase client libraries, REST, or GraphQL data endpoints, turn the Data API off: @@ -87,19 +200,17 @@ If your app never uses Supabase client libraries, REST, or GraphQL data endpoint With the Data API disabled, none of the auto-generated REST endpoints respond, regardless of grants or RLS. -## Add RLS policies - -Enable Row Level Security (RLS) on all tables and views you have exposed via the Data API. You can then write RLS policies to grant users access to specific database rows based on their authentication token. - -For functions, RLS does not apply. Instead, control access by granting `EXECUTE` privileges only to the roles that should be able to call the function, and review any `SECURITY DEFINER` functions carefully. +### Enable RLS policies -Always enable Row Level Security on tables and views you expose via the Data API to protect your data. For functions, restrict access by granting `EXECUTE` only to appropriate roles. +Tables and views exposed through the Data API without RLS can be accessed by any role with matching grants. Enable RLS or add equivalent controls to prevent unauthorized access. RLS doesn't apply to functions, so grant `EXECUTE` only to the roles that need to call them. Review every `SECURITY DEFINER` function carefully. -Any table created through the Supabase Dashboard will have RLS enabled by default. If you created the tables via the SQL editor or via another way, enable RLS like so: +Enable RLS on every table and view exposed through the Data API. You can then write policies that grant users access to specific rows based on their authentication token. + +Tables created through the Supabase Dashboard have RLS enabled by default. Enable RLS explicitly for tables created in the SQL Editor or through another tool: -With RLS enabled, you can create Policies that allow or disallow users to access and update data. We provide a detailed guide for creating Row Level Security Policies in our [Authorization documentation](/docs/guides/database/postgres/row-level-security). - - - -Any granted table **without RLS enabled** can be accessed by roles with matching Data API grants (for example, `anon`). Always make sure RLS is enabled, or that you've got other controls in place to avoid unauthorized access to your project's data. - - - -## Enforce additional rules on each request - -Using Row Level Security policies may not always be adequate or sufficient to protect APIs. - -Here are some common situations where additional protections are necessary: - -- Enforcing per-IP or per-user rate limits. -- Checking custom or additional API keys before allowing further access. -- Rejecting requests after exceeding a quota or requiring payment. -- Disallowing direct access to certain tables, views, or functions in exposed schemas. - -You can build these cases in your application by creating a Postgres function that will read information from the request and perform additional checks, such as counting the number of requests received or checking that an API key is already registered in your database before serving the response. - -Define a function like so: - -```sql -create function public.check_request() - returns void - language plpgsql - security definer - as $$ -begin - -- your logic here -end; -$$; -``` - -And register it to run on every Data API request using: - -```sql -alter role authenticator - set pgrst.db_pre_request = 'public.check_request'; -``` - -This configures the `public.check_request` function to run on every Data API request. To have the changes take effect, you should run: - -```sql -notify pgrst, 'reload config'; -``` - -<$Partial path="db_pre_request_warning.mdx" /> - -Inside the function you can perform any additional checks on the request headers or JWT and raise an exception to prevent the request from completing. For example, this exception raises an HTTP 402 Payment Required response with a `hint` and additional `X-Powered-By` header: - -```sql -raise sqlstate 'PGRST' using - message = json_build_object( - 'code', '123', - 'message', 'Payment Required', - 'details', 'Quota exceeded', - 'hint', 'Upgrade your plan')::text, - detail = json_build_object( - 'status', 402, - 'headers', json_build_object( - 'X-Powered-By', 'Nerd Rage'))::text; -``` - -When raised within the `public.check_request` function, the resulting HTTP response will look like: - -```http -HTTP/1.1 402 Payment Required -Content-Type: application/json; charset=utf-8 -X-Powered-By: Nerd Rage - -{ - "message": "Payment Required", - "details": "Quota exceeded", - "hint": "Upgrade your plan", - "code": "123" -} -``` - -Use the [JSON operator functions](https://www.postgresql.org/docs/current/functions-json.html) to build rich and dynamic responses from exceptions. +With RLS enabled, create policies that control which data users can access and update. See [Row Level Security](/docs/guides/database/postgres/row-level-security). -If you use a custom HTTP status code like 419, you can supply the `status_text` key in the `detail` clause of the exception to describe the HTTP status. +### Configure a pre-request function -If you're using PostgREST version 11 or lower ([find out your PostgREST version](/dashboard/project/_/settings/infrastructure)) a different and less powerful [syntax](https://postgrest.org/en/stable/references/errors.html#raise-errors-with-http-status-codes) needs to be used. +Create and register a Postgres function to run checks before each Data API request: -### Accessing request information +Before adding the check logic, review [Request information](#request-information) and [Error responses](#error-responses). -Like with RLS policies, you can access information about the request by using the `current_setting()` Postgres function. Here are some examples on how this works: +1. Create a pre-request function: -```sql --- To get all the headers sent in the request -SELECT current_setting('request.headers', true)::json; + ```sql + create function public.check_request() + returns void + language plpgsql + security definer + as $$ + begin + -- your logic here + end; + $$; + ``` --- To get a single header, you can use JSON arrow operators -SELECT current_setting('request.headers', true)::json->>'user-agent'; +2. Register the function to run on every Data API request: --- Access Cookies -SELECT current_setting('request.cookies', true)::json; -``` + ```sql + alter role authenticator + set pgrst.db_pre_request = 'public.check_request'; + ``` -| `current_setting()` | Example | Description | -| ------------------- | ----------------------------------------------- | ------------------------------------ | -| `request.method` | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` | Request's method | -| `request.path` | `table` | Table's path | -| `request.path` | `view` | View's path | -| `request.path` | `rpc/function` | Functions's path | -| `request.headers` | `{ "User-Agent": "...", ... }` | JSON object of the request's headers | -| `request.cookies` | `{ "cookieA": "...", "cookieB": "..." }` | JSON object of the request's cookies | -| `request.jwt` | `{ "sub": "a7194ea3-...", ... }` | JSON object of the JWT payload | +3. Reload the PostgREST configuration: -To access the IP address of the client look up the [X-Forwarded-For header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) in the `request.headers` setting. For example: + ```sql + notify pgrst, 'reload config'; + ``` -```sql -SELECT split_part( - current_setting('request.headers', true)::json->>'x-forwarded-for', - ',', 1); -- takes the client IP before the first comma (,) -``` +The function now runs before every Data API request. Add the checks that match your security requirements. -Read more about [PostgREST's pre-request function](https://postgrest.org/en/stable/references/transactions.html#pre-request). +### Pre-request examples -### Examples +Use these examples after you configure the pre-request function. Each example replaces the placeholder logic with a complete request check. -You can only rate-limit `POST`, `PUT`, `PATCH` and `DELETE` requests. This is because `GET` and `HEAD` requests run in read-only mode, and will be served by [Read Replicas](/docs/guides/platform/read-replicas) which do not support writing to the database. +You can only rate-limit `POST`, `PUT`, `PATCH`, and `DELETE` requests. `GET` and `HEAD` requests run in read-only mode. They can be served by [Read Replicas](/docs/guides/platform/read-replicas), which don't support writing to the database. -Outline: +**Outcome:** -- A new row is added to a `private.rate_limits` table each time a modifying action is done to the database containing the IP address and the timestamp of the action. -- If there are over 100 requests from the same IP address in the last 5 minutes, the request is rejected with an HTTP 420 code. +- The `private.rate_limits` table records the IP address and timestamp of each write request. +- The function rejects requests with an HTTP 420 response when an IP address makes more than 100 write requests in 5 minutes. -Create the table: +**Create the table:** ```sql create table private.rate_limits ( @@ -275,9 +304,9 @@ create table private.rate_limits ( create index rate_limits_ip_request_at_idx on private.rate_limits (ip, request_at desc); ``` -The `private` schema is used as it cannot be accessed over the API! +The `private` schema prevents Data API access to the rate-limit records. -Create the `public.check_request` function: +**Create the request check:** Create the `public.check_request` function: ```sql create function public.check_request() @@ -317,7 +346,7 @@ end; $$; ``` -Finally, configure the `public.check_request()` function to run on every Data API request: +**Register the request check:** Configure the `public.check_request()` function to run on every Data API request: ```sql alter role authenticator @@ -326,32 +355,26 @@ alter role authenticator notify pgrst, 'reload config'; ``` -<$Partial path="db_pre_request_warning.mdx" /> - -To clear old entries in the `private.rate_limits` table, set up a [pg_cron](/docs/guides/database/extensions/pg_cron) job to clean them up. +**Clean up old records:** Set up a [`pg_cron`](/docs/guides/database/extensions/pg_cron) job to delete old entries from `private.rate_limits`. -Some applications can benefit from using additional API keys managed by the application **in addition to the [Supabase API keys](/docs/guides/getting-started/api-keys)**. This is commonly necessary in cases like: +Use application-managed API keys when you need another access check. This approach applies to applications that: -- Applications that use the Data API without RLS policies. -- Applications that do not use [Supabase Auth](/auth) or any other authentication system and rely on the `anon` role. +- Use the Data API without RLS policies. +- Don't use [Supabase Auth](/auth) or another authentication system and rely on the `anon` role. - +**Required Supabase key:** The `apikey` header is mandatory and not configurable. If you use another API key, distribute both the publishable key and your application's custom key. See [API keys](/docs/guides/getting-started/api-keys). -Using the `apikey` header with the [Supabase API keys](/docs/guides/getting-started/api-keys) is mandatory and not configurable. If you use additional API keys, you have to distribute both the `publishable` API key and your application's custom API key. - - - -Outline: +**Outcome:** - Your application requires the presence of the `x-app-api-key` header when the `anon` role is used to prevent abuse of your API. - These API keys are stored in the `private.anon_api_keys` table, and are distributed independently. - Each request using the `anon` role will be blocked with HTTP 403 if the `x-app-api-key` header is not registered in the table. -Set up the table: +**Create the table:** ```sql create table private.anon_api_keys ( @@ -360,7 +383,7 @@ create table private.anon_api_keys ( ); ``` -Create the `public.check_request` function: +**Create the request check:** Create the `public.check_request` function: ```sql create function public.check_request() @@ -399,7 +422,7 @@ end; $$; ``` -Finally, configure the `public.check_request()` function to run on every Data API request: +**Register the request check:** Configure the `public.check_request()` function to run on every Data API request: ```sql alter role authenticator @@ -408,8 +431,6 @@ alter role authenticator notify pgrst, 'reload config'; ``` -<$Partial path="db_pre_request_warning.mdx" /> -