Merge remote-tracking branch 'origin/master' into poc/explorer-next

This commit is contained in:
Saxon Fletcher committed 2026-09-14 15:18:16 +10:00
commit 3fa0dab20e
882 files changed
+5929 -2904

No files matched your search

+187 -40
View File
@@ -2,73 +2,220 @@
name: edit-the-docs
description: >-
Restructure, reorder, and improve existing Supabase docs pages under
apps/docs — clarity, connective text, section grouping, and brevity.
Use when asked to edit, reorganize, restructure, tighten prose, or add
glue between sections on a page that already exists. Not for net-new
feature drafts (use write-the-docs) or PR triage/verification (use
review-the-docs).
apps/docs: clarity, connective text, section grouping, and brevity.
Use when asked to edit, reorganize, restructure, tighten prose, add glue
between sections, or split a page edit into stacked PRs. Not for net-new
feature drafts, which belong to write-the-docs, and not for PR triage or
verification, which belong to review-the-docs.
---
# Edit the docs
Improves **existing** Supabase docs pages: structure, order, connective text,
and clarity. Distinct from [`write-the-docs`](../write-the-docs/SKILL.md)
(draft net-new or product-grounded rewrites from intent + code) and
[`review-the-docs`](../review-the-docs/SKILL.md) (lint, build, PR triage).
Improves **existing** Supabase docs pages: structure, order, connective text, and clarity.
**Not this skill:** [`write-the-docs`](../write-the-docs/SKILL.md) drafts net-new content or product-grounded rewrites from intent and code. [`review-the-docs`](../review-the-docs/SKILL.md) covers lint, build, and PR triage.
**Output is one pull request, with one change type per commit.** A reviewer reads the style diff apart from the structure diff without holding several PRs in their head. Split into a stack of PRs only when the requester asks for one, or approves the split you offer because the diff turned out large. Phase 0 covers when to raise it, and [reference/stacked-prs.md](reference/stacked-prs.md) covers the mechanics.
## Core rules
1. **Read before you rewrite.** Open the target page and nearby pages of the same type. Name the reader's goal and the page type (explainer, guide, tutorial, troubleshooting) before moving sections.
2. **Improve structure and clarity; don't invent product truth.** Preserve behavior claims, UI labels, and positioning unless you verify a change against code or product intent. Accuracy gaps or missing net-new content belong with [`write-the-docs`](../write-the-docs/SKILL.md) / [`pm-the-docs`](../pm-the-docs/SKILL.md), not silent invention here.
1. **Read before you rewrite.** Open the target page and nearby pages of the same type. Name the reader's goal and the page type before moving sections.
2. **Don't invent product truth. Verify it, in its own PR.** Style and structure work preserves behavior claims, UI labels, and positioning as written. Correcting a claim is PR 3 work, and adding one belongs to the additions branches above it. Those PRs follow [`write-the-docs`](../write-the-docs/SKILL.md) grounding rules: read the code, separate shipped behavior from product intent, and flag what you inferred.
3. **Follow CONTRIBUTING.md and WORD_LIST.md** for voice, terminology, and formatting. See [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) and [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md).
4. **Prefer brevity.** Prefer broad strokes when mechanical detail doesn't help the reader's task. Cut redundancy; don't over-explain.
5. **Reuse sibling skills.** IA/architecture via [`ask-the-docs`](../ask-the-docs/SKILL.md); validation and self-review via [`review-the-docs`](../review-the-docs/SKILL.md). Shared pitfalls live in [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md) — apply them, don't duplicate them.
4. **Prefer brevity.** Use broad strokes when mechanical detail doesn't help the reader's task. Cut redundancy. Don't over-explain.
5. **Reuse sibling skills.** Get IA and architecture from [`ask-the-docs`](../ask-the-docs/SKILL.md). Get validation and self-review from [`review-the-docs`](../review-the-docs/SKILL.md). Apply the shared pitfalls in [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md) rather than duplicating them here.
6. **One change type per diff.** A diff that mixes reworded prose with moved sections is unreviewable, because the reader can't tell a move from a rewrite. Separate them by commit in a single PR, or by branch in a stack.
## Phase 1 — Diagnose
## Phase 0: Size and split
1. Identify the document type per CONTRIBUTING.md (explainer, tutorial, guide, reference, or troubleshooting).
1. Identify the document type per CONTRIBUTING.md. The types are explainer, tutorial, guide, reference, and troubleshooting.
2. State the reader's goal and prerequisites in one or two lines.
3. Note structural problems: mixed information types interrupting a procedure, missing intro navigation on a long page, weak transitions, redundancy, or over-explained mechanics.
4. Summarize the diagnosis to the requester before large moves when the restructure would change how the page is read.
4. Sort the diagnosis into the buckets below. **Drop any bucket that comes back empty, and say so.** Style, structure, and technical revision take one commit or branch each. Additions take as many as the content needs, so the edit has no fixed size. A style edit plus a structural edit is the common shape, because most pages that need restructuring are already correct. Two buckets is a complete result, not a truncated one.
5. Know where the edit ends. **The edit is only the buckets that have content.** Any bucket you drop is beyond the edit, and a later request for that change type is a new request. That includes one you raise yourself. Name it, keep the work in progress clean, and ask whether it belongs in this edit, in a separate ticket, or nowhere. Absorbing it into a bucket that's already open is what turns an edit into a rewrite.
6. Size the edit. **When it comes out large, offer a stack. Don't choose one.** One PR with each bucket as its own commit is the output unless the requester approves a split. Raise the question when both hold:
- The edit rewrites prose and moves sections, or it corrects a technical claim.
- It runs over roughly 150 changed lines.
## Phase 2 — Restructure
Say how large the diff is and propose the branches. Name the trade in the ask: a stack gives a reviewer clean per-change-type diffs, and it also means no PR page shows the whole edit, so reading it end to end costs them an extra command. Their reviewers pay that cost, so it's their call. **No answer means one PR.**
Apply the **Mixed information types**, **Navigation**, and **Cross-references and glue** guidance in [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) (Guides section), summarized here:
7. Summarize the diagnosis and the proposed split to the requester, and **wait for confirmation before creating any branch.** Name which buckets are empty and why. When nobody is available to confirm, record the diagnosis in the PR body and ship one PR.
1. 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.
2. 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.
3. 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.
4. Move and regroup first; preserve meaning. Don't silently rewrite facts while restructuring.
**When the diff outgrows the estimate mid-edit, stop and offer the split then.** A size call made at diagnosis can be wrong by the time the style pass lands. Say how large it got and ask. Splitting unasked is the failure here, and so is carrying on quietly because you already have an answer.
## Phase 3 — Edit for clarity
**The sections below are named for the stacked case.** In a single PR they're commits, in the same order and under the same rules.
## PR 1: Style
Inline changes only. Nothing in this PR moves a line from one place to another.
**Rewrite:**
- Use second person, present tense, short paragraphs, and ordered steps for sequential actions.
- Cut restated points and mechanical over-explanation.
- Apply [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md): timelessness, no internal planning context in shipped MDX, redundancy, single-item lists, admonition restatement.
- Search [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md) when introducing or revising technical terms and UI actions.
- Keep code samples executable in their stated context; mark intentionally omitted code. Prefer partials under `apps/docs/content/_partials/` over copied blocks.
- Put procedures in procedure format, per the Procedures section of CONTRIBUTING.md. Start each step with an imperative verb, keep one action or a closely related set per step, present 7 ± 2 steps per chunk, and group anything longer into named phases or smaller procedures.
- Apply the inline rules in CONTRIBUTING.md for admonitions, emphasis, links, lists, and the "Styling, formatting, and grammar" section.
- Keep code samples executable in their stated context, and mark intentionally omitted code. Prefer partials under `apps/docs/content/_partials/` over copied blocks.
- **Check the alt text on every image, and open the image to do it.** Alt text on an existing page usually names the topic rather than describing the picture, and a topic name is what the nearby heading already says. Describe what a reader who can't see it would need: the labeled parts, the relationships between them, and any values the diagram carries. This is a rewrite of existing text, so it belongs in this PR.
## Phase 4 — Validate
**Cut:**
Before handoff:
- Restated points and mechanical over-explanation.
- The shared pitfalls in [`common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md): timelessness, internal planning context in shipped MDX, redundancy, single-item lists, and admonition restatement.
- Terminology that doesn't match [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md). Crawl the list for terms already on the page, not only the ones you introduce. An existing page is where nonconforming terminology accumulates.
- [ ] Section groups follow information type; procedures aren't interrupted by long context
- [ ] Intro navigation present only when the page needs it; links resolve
## PR 2: Structure
Apply the **Mixed information types**, **Navigation**, and **Cross-references and glue** guidance in the Guides section of [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md).
**Work in this order, and settle the outline before you move a line.** A restructure invalidates every branch above it in the stack, so each revision costs a full restack, and a restack is where content gets dropped in conflict resolution. Reworking the shape twice costs far more than getting it right once.
### 1. Lock the headings other code links to
Grep the whole repo for `#<slug>` against every heading on the page, not just `apps/docs/content`. Studio renders Docs buttons that deep-link into guide anchors, and `apps/www` links into them too. Those are the matches that break a button in the product rather than a link between two pages.
Write the matched heading texts down. For the rest of this PR they are immutable. **Moving a section preserves its slug, and so does changing its level. Only renaming breaks it.** That is what makes an aggressive regroup safe.
### 2. Classify every substantial section
Each one is **procedural** (the reader performs actions), **contextual** (the reader needs to understand something before acting), or **reference** (the reader looks something up).
**Classify by what the reader is doing, not by what the section is about.** Subject matter is the trap: on a page about tables every section is "about tables", so grouping by topic produces one task-named bucket that quietly collects the background as well. A reader opens a section on schemas to understand something, not to do something, so it is context no matter how much it is about tables.
**A section serving two classes gets split, not filed under the larger half.** Give the new half a heading, keep the heading text of the half that stays, and cross-reference the two. One cross-reference costs less than a reader hunting for the half they need.
### 3. Write the target outline before touching the file
Produce the whole heading tree, with levels, and check it against the locked list from step 1. Put it in front of the requester along with the Phase 0 diagnosis. The outline is the artifact that gets revised, not the page.
Order the groups: a short conceptual opener when the page serves newcomers, then procedures, then context, then reference. The action path runs uninterrupted and the background sits after it.
A guide about database tables settled here:
```
## What is a table? <- short conceptual opener
## Creating and managing tables <- procedures
### Creating tables
### Securing your tables
### Loading data
### Joining tables with foreign keys
## How tables are organized <- context
### Primary keys
### Relationships between tables
### Schemas
## Reference
### Data types
```
"Joining tables with foreign keys" held both classes. The steps kept the heading and stayed in the procedures group; the idea of a relational database moved to "Relationships between tables" in the context group.
### 4. Move, then add the glue the new shape needs
Move and regroup, and preserve meaning. Don't silently rewrite facts while restructuring. A pure set of moves is what makes this PR reviewable, so call out in the PR body any deletion that isn't a move.
Then:
- Add a short introduction linking each major group and saying when to use it. Skip it when a short page is already easy to scan.
- Add a group introduction, a transition where the information type changes, and an outcome after a procedure. Don't link every adjacent section.
- Put sections covering the same topic under a shared heading.
### 5. When the page itself should split
When a topic outgrows the page, give it its own page rather than its own group. Navigation that overflows the sidebar is one signal. A section carrying its own subsections several levels deep, sharing nothing with the rest of the page but a single word, is another.
Update every navigation entry, repoint every inbound anchor, and cross-reference the new page. Confirm the nav-registration mechanism through [`ask-the-docs`](../ask-the-docs/SKILL.md) rather than assuming it.
### 6. Before you submit
Re-run the step 1 grep. Every locked heading text is still present, at whatever level it ended up.
**A move that only reads correctly once new content exists isn't a PR 2 move.** It belongs to the branch that adds the content. Leave the section where it is, and say in the PR body which move you deferred and what it is waiting on. Otherwise PR 2 stops standing on its own, and a stack merged partway leaves the page reading worse than before.
If nothing needs to move, PR 2 doesn't exist. A page can be well organized and still need a style pass. Drop the branch and say the structure held up.
## PR 3: Technical revision
Validate the truth of the content and correct what's wrong.
**Change a claim only when leaving it would produce a wrong outcome.** A reader following the page would hit an error, get a different result than the page promises, or decide on a fact that isn't true. That's the test.
**Leave it alone otherwise.** Don't open PR 3 for imprecise but harmless phrasing, a claim you'd have worded differently, an accurate detail that isn't the newest way to do it, or a stale-looking value you can't verify against code. The last one is a note to the author, not an edit.
**An external rule isn't a wrong outcome by itself.** A best-practices rule that a reader would never hit as a failure doesn't clear the gate, however high the rule's stated impact. Weigh what the reader experiences against the page, not how the rule is ranked.
**PR 3 corrects what's on the page. A missing safeguard is an absence, and absences are additions.** When the fix is to add something the page never had, it belongs above this branch, not in it. This is the line that keeps a verification pass from quietly becoming a rewrite.
When a claim does fail the test, verify before you change it, per Phase 1 of [`write-the-docs`](../write-the-docs/SKILL.md):
- Read the implementation. Prefer the diff of a linked `supabase/supabase` PR over a general codebase read.
- Where code and product intent disagree, code wins for behavior claims. Flag the mismatch.
- Flag anything you inferred in the PR description, not in the MDX.
**Run the snippets when the page has them.** Offer [`test-the-docs`](../test-the-docs/SKILL.md) before you start, and don't run it unasked. A snippet that fails in the sandbox is the most direct evidence a claim fails the wrong-outcome test, because the reader hits the same error. Attach the verification report to the PR body. If the author declines, record the artifacts as deferred and carry on with the code read. If the sandbox fails for an environmental reason, that's a deferral rather than a result — retry it before the branch merges.
**Run every fence in document order, not only one path.** The reader pastes top to bottom, so that order is the claim. Snippets that each work alone can still fail as a sequence, by re-creating an object an earlier one made or by depending on one no fence ever creates. Nothing in a code read surfaces that, and it's the failure a reader hits first.
Testing covers procedural content only. Claims that nothing executes, such as limits, defaults, and positioning, still need the code read above.
**A branch above can change the answer.** The test is applied to the page as it stands, so a claim that passes inspection here can become wrong once an additions branch contradicts it. That correction belongs to the branch that creates the conflict, not back down here. Say so when you leave the claim, so the later change reads as intended rather than as a missed finding.
**If every finding fails the test, PR 3 is empty.** Say what you checked and what you're deliberately leaving, then drop the branch. An empty PR 3 means verified and fine, not skipped. A technical concern raised later in the stack is then a new request, per the boundary rule in Phase 0.
## PR 4+: Additions, on request only
**Additions sit on top of the stack, so they stay out of the edit.** New content is a different job from editing what's already there. Keeping it on its own branches is what stops an edit from turning into a rewrite halfway through.
**Additions take as many branches as the content needs.** Split them by diff size so each branch stays reviewable, and name each branch for what it adds rather than for its position in the stack. One branch is right when the additions are one topic and a small diff.
**Don't scope these branches from the diagnosis.** Additions are empty by default. Don't propose them because the page looks thin.
**A tracked request is the request.** An assigned ticket or issue that asks for new content has already made the ask, so treat it as scoped and get on with it. The rule forbids inventing additions yourself. It doesn't ask you to wait for someone to repeat a request that's already written down.
**Route mid-edit requests up here instead.** When the author asks for new content while you're on an earlier branch, or when you spot a gap yourself, say it's additions material and keep the current branch clean. Then ask whether they want it in this stack, in a separate ticket, or not at all. Naming it is how you keep the conversation from reopening PR 1.
Once it's scoped:
- Crawl reader feedback for candidate gaps. Linear is an internal Supabase tool, preferred when available and not required for open-source contributors.
- Ground additions the same way as PR 3. Read the code before making a behavior claim, and flag what you inferred.
- **Run every new runnable snippet through [`test-the-docs`](../test-the-docs/SKILL.md) before it ships.** New content is where an untested snippet is likeliest to be wrong, because nothing has ever executed it.
- Strip internal business context before the draft ships: PRD intent, roadmap speculation, and ticket discussion. It belongs in the PR description, not in the MDX.
## Validate each PR
Run this per change type, before you submit the commit or branch that carries it, not once at the end:
- [ ] The diff contains only this change type
- [ ] Section groups follow information type, and procedures aren't interrupted by long context
- [ ] Intro navigation is present only when the page needs it, and links resolve
- [ ] Connective text is selective, not link spam
- [ ] Voice matches CONTRIBUTING.md / WORD_LIST.md
- [ ] Voice matches CONTRIBUTING.md and WORD_LIST.md
- [ ] Every image has alt text that describes the image, checked against the image itself
- [ ] No invented behavior or positioning
- [ ] Shared pitfalls checklist considered
Mechanics (anchors, lint, format): follow [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md). Before renaming or rewording headings, grep for `#<old-anchor-slug>` under `apps/docs/content` and update matches.
**Anchors.** PR 2 step 1 builds the locked-heading list and step 6 re-checks it. Any branch that renames or rewords a heading clears the same gate.
Then run [`review-the-docs`](../review-the-docs/SKILL.md) local self-review (`pnpm lint:mdx`, and `pnpm build:guides-markdown` when guides/explainers/tutorials changed).
**Frontmatter `title`.** It follows the same sentence-case rule as a heading. Renaming it moves a navigation label and a search entry, not just a line of prose, so it clears this same gate and lands in PR 2 rather than PR 1.
**Lint and format.** Follow [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md). Then run the [`review-the-docs`](../review-the-docs/SKILL.md) local self-review: `pnpm lint:mdx`, plus `pnpm build:guides-markdown` when a guide, explainer, or tutorial changed.
`build:guides-markdown` writes `apps/docs/public/markdown/manifest.json`, which the repo tracks and commits as `[]`. Discard that file before committing. It's a build artifact, not part of the edit.
## Additional resources
- Structure SoT: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) (mixed types, navigation, glue)
- Structure ops: [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md) — Guides: Mixed information types, Navigation, Cross-references and glue
- Pitfalls: [`write-the-docs/reference/common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md)
- Mechanics: [`write-the-docs/reference/drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md)
- Architecture/IA: [`ask-the-docs`](../ask-the-docs/SKILL.md)
**Stacking:**
- Mechanics and `gh stack` commands: [reference/stacked-prs.md](reference/stacked-prs.md)
- Bottom-up stack review: [`review-the-docs`](../review-the-docs/SKILL.md)
**Style and structure:**
- Mixed information types, navigation, and glue: the Guides section of [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md)
- Procedure format: the Procedures section of [`apps/docs/CONTRIBUTING.md`](../../../apps/docs/CONTRIBUTING.md)
- Terminology: [`apps/docs/WORD_LIST.md`](../../../apps/docs/WORD_LIST.md)
**Sibling skills:**
- Pitfalls and drafting mechanics: [`common-pitfalls.md`](../write-the-docs/reference/common-pitfalls.md), [`drafting-mechanics.md`](../write-the-docs/reference/drafting-mechanics.md)
- Runnable verification: [`test-the-docs`](../test-the-docs/SKILL.md)
- Architecture and IA: [`ask-the-docs`](../ask-the-docs/SKILL.md)
- Net-new drafts: [`write-the-docs`](../write-the-docs/SKILL.md)
- Review: [`review-the-docs`](../review-the-docs/SKILL.md)
@@ -0,0 +1,74 @@
# Stacked PRs for a page edit
Mechanics for shipping the [`edit-the-docs`](../SKILL.md) buckets as a stack. **A stack is the exception, and the requester approves it.** Phase 0 covers when to offer one. This file covers how to build and submit it once they agree.
## Branch names
One branch per change type, bottom to top:
| PR | Branch |
| --- | ---------------------------- |
| 1 | `docs/<page>-style` |
| 2 | `docs/<page>-structure` |
| 3 | `docs/<page>-technical` |
| 4+ | `docs/<page>-<what-it-adds>` |
The first three names are fixed, because there's one of each. **Additions get one branch per topic, named for the content it adds:** `docs/tables-rls` and `docs/tables-datatypes`, not `docs/tables-additions-1` and `-2`. Use `docs/<page>-additions` when a single branch carries all of them.
**Create only the branches whose buckets have content.** Two branches is the common shape once an edit clears the gate. `gh stack init` takes however many you pass it.
**Use a category prefix and a short second segment.** Don't prefix a branch with an author name, even when a tracker suggests that format.
**Get every name right before you submit.** Renaming a branch that already has an open PR closes the PR rather than retargeting it, and a closed PR whose head ref is gone can't be reopened. Recovering costs the PR number and its CI history.
## Build the stack with gh stack
Never chain `gh pr create --base <previous-branch>`. That produces correct base branches but no GitHub stack. There's no stack number and no stack UI, so reviewers see several unrelated-looking PRs instead of one series.
1. `gh stack init <bottom> <middle> <top>` adopts existing branches, bottom to top. This is local only and makes no remote change.
2. `gh stack view` confirms the structure and shows the mapped PR for each branch.
3. `gh stack submit --auto` pushes and registers the stack on GitHub. Use `--auto` in a non-interactive session, where the editor can't open. New PRs are created as drafts unless you pass `--open`.
**Check the titles after submitting.** `submit` can title a PR from its branch name rather than its commit subject. Fix any that came out wrong with `gh pr edit <pr> --title`.
**Safe to re-run on PRs that already exist.** `submit` reports each one "up to date" and reuses it, so PR numbers, descriptions, and creation timestamps survive.
**Draft state doesn't reliably survive.** `--open` marks existing PRs ready for review, not just new ones, and a resubmit has been observed taking drafts out of draft without it. Check the draft state of every PR after submitting, and set it back with `gh pr ready --undo` if it moved.
**Other commands.** `gh stack link <pr> <pr> <pr>` registers the GitHub stack without local tracking. `gh stack unstack` removes a stack. The extension is `github/gh-stack`.
## Reading the stack as a whole
No PR page shows the whole edit, so a reviewer who wants it in one view needs the command:
```bash
git diff master...<top-branch> -- <path>
```
`gh stack view` lists the branches in order, so it gives you the top one. Put the command in the bottom PR's body. Without it the reviewer reconstructs the edit branch by branch, and that cost is why Phase 0 defaults to a single PR.
## Restacking after a change low in the stack
`gh stack rebase` replays every branch above the one you changed. Where a lower branch moved content that an upper branch also edited, git raises a conflict whose two sides are "the new structure" and "the old content being re-added". Resolving toward the new structure is usually right, and it silently drops the upper branch's edit along with the stale copy.
**Assume that happened. Audit rather than read the diff.** Before pushing, grep each branch for a marker of every change it is supposed to carry:
```bash
git show <branch>:<path> | grep -c '<marker>'
```
One marker per change, checked against the count you expect. A restructure large enough to conflict is large enough that reading the diff will not catch a missing paragraph.
Restore anything missing as a new commit on the branch that owns it, then rebase again. Don't fold it into a neighboring branch to avoid a second rebase; that breaks the one-change-type-per-PR rule the stack exists for.
## Merge order
Merge bottom-up: `master`, then PR 1, then PR 2, then PR 3, then each additions branch in stack order. This is the model [`review-the-docs`](../../review-the-docs/SKILL.md) uses to review a stack, so the authoring and review sides share one vocabulary.
## PR bodies
Each body states which change type the PR carries and what it leaves to the PRs above it. That tells a reviewer the diff is narrow on purpose. Reworded prose isn't missing from the structure PR, it already landed below.
Carry forward anything you flagged while working: inferred claims from PR 3, gaps you named but didn't fill, and stale values you couldn't verify. Those belong in the description, not in the MDX.
For general PR-body mechanics, see [`create-pull-request`](https://github.com/supabase/docs-agent-skills/blob/main/.claude/skills/create-pull-request/SKILL.md).
+9
View File
@@ -8,9 +8,17 @@ on:
- 'apps/www/next.config.mjs'
- 'apps/www/next.config.js'
- 'apps/www/lib/**/*.js'
- 'apps/www/lib/**/*.mjs'
- 'apps/www/content/md/**'
- 'apps/www/scripts/**/*.mjs'
- 'apps/www/internals/**/*.mjs'
- 'apps/www/_blog/**'
- 'apps/www/_alternatives/**'
- 'apps/www/_customers/**'
- 'apps/www/public/.well-known/**'
# www catalog tests check that linked docs guides exist, so guide changes
# must trigger these tests and their sources must be included in checkout.
- 'apps/docs/content/guides/**'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
@@ -33,6 +41,7 @@ jobs:
persist-credentials: false
sparse-checkout: |
apps/www
apps/docs/content/guides
packages
supabase
patches
+1
View File
@@ -7,6 +7,7 @@ apps/**/out
.context/**
# prettier-plugin-sql-cst only supports sqlite syntax
**/supabase/migrations/*.sql
**/supabase/schemas/**/*.sql
apps/www/schema.sql
apps/www/public/images/*
# Generated by apps/www/scripts/generateStaticContent.mjs (GitHub discussion bodies)
+5 -5
View File
@@ -105,11 +105,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<tr>
<td>Flutter</td>
<td><a href="https://github.com/supabase/supabase-flutter" target="_blank" rel="noopener noreferrer">supabase-flutter</a></td>
<td><a href="https://github.com/supabase/postgrest-dart" target="_blank" rel="noopener noreferrer">postgrest-dart</a></td>
<td><a href="https://github.com/supabase/gotrue-dart" target="_blank" rel="noopener noreferrer">gotrue-dart</a></td>
<td><a href="https://github.com/supabase/realtime-dart" target="_blank" rel="noopener noreferrer">realtime-dart</a></td>
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/postgrest" target="_blank" rel="noopener noreferrer">postgrest</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_auth" target="_blank" rel="noopener noreferrer">supabase_auth</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_realtime" target="_blank" rel="noopener noreferrer">supabase_realtime</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_storage" target="_blank" rel="noopener noreferrer">supabase_storage</a></td>
<td><a href="https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_functions" target="_blank" rel="noopener noreferrer">supabase_functions</a></td>
</tr>
<tr>
<td>Swift</td>
+30 -10
View File
@@ -4,40 +4,60 @@ Design resources for building consistent user experiences at Supabase.
## Getting started
First, make a copy of _.env.local.example_ and name it _env.local_. Then install any required packages and start the development server:
From the repo root:
```bash
# Copy local env vars (sets NEXT_PUBLIC_BASE_PATH for asset URLs)
cp apps/design-system/.env.local.example apps/design-system/.env.local
# Move into the design-system app
cd apps/design-system
# Install dependencies
pnpm i
# Build the registry and Velite content, then start the dev servers
pnpm dev
```
The `dev` command generates `__registry__`, then runs the Next.js development server and Contentlayer together. That is the recommended workflow.
Or from `apps/design-system`:
```bash
# Copy local env vars (sets NEXT_PUBLIC_BASE_PATH for asset URLs)
cp .env.local.example .env.local
# Install dependencies
pnpm i
# Build the registry and Velite content, then start the dev servers
pnpm dev
```
The `dev` command builds the registry and Velite content, then runs the Next.js dev server and Velite watcher in parallel.
Open [http://localhost:3003/design-system](http://localhost:3003/design-system) in your browser to see the result.
Doc pages load compiled MDX from `.velite/codes/*.json` per document. Metadata lives in the smaller `allDocs.json` index (~367KB instead of ~27MB), so content edits only reload the changed doc's code.
### Alternative commands
You can also run the development server and content watcher separately. Generate the registry first, because `dev:next` and `dev:content` do not:
You can also run the development server and content watcher separately. Build the registry and content first, because `dev:next` and `dev:content` do not:
```bash
pnpm generate:registry
pnpm build:registry
pnpm build:content
# Run only the Next.js development server
pnpm dev:next
# Run only the content watcher (in a separate terminal shell)
# Run only the Velite content watcher (in a separate terminal shell)
pnpm dev:content
```
From the repo root, `pnpm dev:design-system` runs the same `dev` script, so it also generates `__registry__`. If you split the watchers from the root, generate first:
From the repo root, `pnpm dev:design-system` runs the same `dev` script. If you split the watchers from the root, build first:
```bash
pnpm --filter=design-system generate:registry
pnpm --filter=design-system build:registry
pnpm --filter=design-system build:content
pnpm --filter=design-system dev:next
pnpm --filter=design-system dev:content
```
Open [http://localhost:3003](http://localhost:3003) in your browser to see the result.
### Watching for MDX changes
The `dev` command watches MDX files and hot-reloads them. If you are running `pnpm dev:next` on its own, also run `pnpm dev:content` in another terminal.
@@ -64,5 +84,5 @@ Do not edit `__registry__`. `pnpm dev`, `pnpm typecheck`, and `pnpm build` gener
```bash
cd apps/design-system
pnpm generate:registry
pnpm build:registry
```
@@ -3,8 +3,10 @@ import { DocsPager, getBreadcrumbSegments } from '@/components/pager'
import { SourcePanel } from '@/components/source-panel'
import { DashboardTableOfContents } from '@/components/toc'
import { siteConfig } from '@/config/site'
import { getAllDocs, getDocBySlug, getDocMetaBySlug } from '@/lib/docs'
import { getTableOfContents } from '@/lib/toc'
import { absoluteUrl } from '@/lib/utils'
/* eslint-disable turbo/no-undeclared-env-vars */
import '@/styles/code-block-variables.css'
import '@/styles/mdx.css'
@@ -16,8 +18,6 @@ import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea, Separator } from 'ui'
import { allDocs } from '@/.velite'
interface DocPageProps {
params: Promise<{
slug: string[]
@@ -26,13 +26,7 @@ interface DocPageProps {
async function getDocFromParams({ params }: { params: { slug: string[] } }) {
const slug = params.slug?.join('/') || ''
const doc = allDocs.find((doc) => doc.slugAsParams === slug)
if (!doc) {
return null
}
return doc
return getDocMetaBySlug(slug)
}
export async function generateMetadata(props: DocPageProps): Promise<Metadata> {
@@ -71,14 +65,20 @@ export async function generateMetadata(props: DocPageProps): Promise<Metadata> {
}
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
if (process.env.NODE_ENV === 'development') {
return []
}
const allDocs = await getAllDocs()
return allDocs.map((doc) => ({
slug: doc.slugAsParams.split('/'),
slug: doc.slugAsParams ? doc.slugAsParams.split('/') : [],
}))
}
export default async function DocPage(props: DocPageProps) {
const params = await props.params
const doc = await getDocFromParams({ params })
const slug = params.slug?.join('/') || ''
const doc = await getDocBySlug(slug)
if (!doc) {
notFound()
@@ -125,7 +125,6 @@ export function ComponentPreview({
<Button
className="rounded-full"
onClick={() => setExpandState(!expand)}
variant="default"
icon={<Expand className="text-foreground-lighter" />}
>
{expand ? 'Collapse code' : 'Expand code'}
+44
View File
@@ -0,0 +1,44 @@
import 'server-only'
/* eslint-disable turbo/no-undeclared-env-vars */
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { connection } from 'next/server'
import type { Doc as DocMeta } from '@/.velite'
export type { DocMeta }
export type Doc = DocMeta & { code: string }
const CODE_DIR = path.join(process.cwd(), '.velite/codes')
async function loadDocCode(codeId: string): Promise<string> {
const raw = await readFile(path.join(CODE_DIR, `${codeId}.json`), 'utf8')
return JSON.parse(raw) as string
}
export async function getAllDocs(): Promise<DocMeta[]> {
if (process.env.NODE_ENV === 'development') {
await connection()
}
const { allDocs } = await import('@/.velite')
return allDocs
}
export async function getDocMetaBySlug(slug: string): Promise<DocMeta | null> {
const allDocs = await getAllDocs()
return allDocs.find((doc) => doc.slugAsParams === slug) ?? null
}
export async function getDocBySlug(slug: string): Promise<Doc | null> {
const doc = await getDocMetaBySlug(slug)
if (!doc) {
return null
}
const code = await loadDocCode(doc.codeId)
return { ...doc, code }
}
@@ -19,7 +19,6 @@ export default function AdmonitionButtonSplitDemo() {
<div className="flex w-full @lg:w-auto">
<Button
type="button"
variant="default"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Set up SMTP
@@ -28,7 +27,6 @@ export default function AdmonitionButtonSplitDemo() {
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
aria-label="More email template editing options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
@@ -10,7 +10,7 @@ export default function AdmonitionDemo() {
title="Set up custom SMTP"
description="You’re using the built-in email service. This service has rate limits and is not meant to be
used for production apps."
actions={<Button variant="default">Set up SMTP</Button>}
actions={<Button>Set up SMTP</Button>}
/>
<Admonition
type="destructive"
@@ -9,7 +9,7 @@ export default function AdmonitionDemo() {
title="OAuth Server is disabled"
description="Enable OAuth Server to make your project act as an identity provider for
third-party applications."
actions={<Button variant="default">OAuth Server Settings</Button>}
actions={<Button>OAuth Server Settings</Button>}
/>
)
}
@@ -8,7 +8,7 @@ export default function AdmonitionDemo() {
layout="responsive"
title="Disk management has moved"
description="Disk management is now handled alongside Project Compute on the Compute and Disk page."
actions={<Button variant="default">Go to Compute and Disk</Button>}
actions={<Button>Go to Compute and Disk</Button>}
/>
)
}
@@ -13,7 +13,6 @@ export default function ButtonSplitDropdownDemo() {
<div className="flex w-fit">
<Button
type="button"
variant="default"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Primary action
@@ -22,7 +21,6 @@ export default function ButtonSplitDropdownDemo() {
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
aria-label="More actions"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
@@ -57,7 +57,6 @@ export default function CalendarForm() {
<PopoverTrigger asChild>
<FormControl>
<Button
variant="default"
size="small"
className={cn(
'w-[240px] justify-start',
@@ -69,7 +69,6 @@ export default function ComboboxPopover() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="default"
size="small"
className="w-[150px] justify-start rounded-full"
icon={
@@ -58,7 +58,6 @@ export default function ComboBoxResponsive() {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="default"
size="small"
className="w-[150px] justify-start"
icon={!selectedStatus && <Plus className="text-foreground-muted" />}
@@ -24,9 +24,7 @@ export default function ConfirmationModalDemo() {
return (
<>
<Button variant="default" onClick={() => setVisible(!visible)}>
Show Confirmation Modal
</Button>
<Button onClick={() => setVisible(!visible)}>Show Confirmation Modal</Button>
<ConfirmationModal
visible={visible}
size="small"
@@ -133,5 +133,5 @@ export function InterstitialActionError({ error }: { error?: React.ReactNode })
}
export function SignOutButton() {
return <Button variant="default" icon={<LogOut />} className="px-2" aria-label="Sign out" />
return <Button icon={<LogOut />} className="px-2" aria-label="Sign out" />
}
@@ -18,9 +18,7 @@ export default function CopyConfirmations() {
</div>
</div>
<div className="flex gap-2 justify-end">
<Button variant="default" size="tiny">
Cancel
</Button>
<Button size="tiny">Cancel</Button>
<Button variant="danger" size="tiny">
Delete
</Button>
@@ -40,9 +38,7 @@ export default function CopyConfirmations() {
</div>
</div>
<div className="flex gap-2 justify-end">
<Button variant="default" size="tiny">
Cancel
</Button>
<Button size="tiny">Cancel</Button>
<Button variant="danger" size="tiny">
Delete project
</Button>
@@ -139,7 +139,7 @@ export const columns: ColumnDef<Payment>[] = [
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="px-1.5" icon={<MoreVertical />} />
<Button className="px-1.5" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="max-w-48">
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}>
@@ -200,7 +200,7 @@ export default function DataTableDemo() {
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
<Button className="ml-auto" size="tiny" iconRight={<ChevronDown />}>
Columns
</Button>
</DropdownMenuTrigger>
@@ -303,19 +303,13 @@ export default function DataTableDemo() {
</div>
<div className="space-x-2">
<Button
variant="default"
size="tiny"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="default"
size="tiny"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<Button size="tiny" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</Button>
</div>
@@ -17,7 +17,7 @@ export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="default">Edit profile</Button>
<Button>Edit profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]" centered={false}>
<DialogHeader>
@@ -19,7 +19,7 @@ export default function DialogCloseButton() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="default">Share</Button>
<Button>Share</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
@@ -43,9 +43,7 @@ export default function DialogCloseButton() {
</DialogSection>
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button variant="default" type="button">
Custom Close Button
</Button>
<Button type="button">Custom Close Button</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
@@ -17,7 +17,7 @@ export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="default">Show Dialog</Button>
<Button>Show Dialog</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
@@ -67,9 +67,7 @@ export default function DrawerDemo() {
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="default" size="small">
Open Drawer
</Button>
<Button size="small">Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-sm">
@@ -13,7 +13,7 @@ export default function EmptyStateMissingRoute() {
title="Unable to find bucket"
description={`${bucketId ? `The bucket “${bucketId}”` : 'This bucket'} doesn’t seem to exist.`}
>
<Button asChild variant="default" className="mt-2">
<Button asChild className="mt-2">
<Link
href="/"
onClick={(e) => {
@@ -29,7 +29,7 @@ export default function EmptyStatePresentationalIcon() {
title="Add a provider"
description="Use third-party authentication systems to access your project."
>
<Button size="tiny" variant="default" icon={<Plus size={14} />}>
<Button size="tiny" icon={<Plus size={14} />}>
Add provider
</Button>
</EmptyStatePresentational>
@@ -116,9 +116,7 @@ export default function FieldDemo() {
<Button variant="primary" type="submit">
Submit
</Button>
<Button type="button" variant="default">
Cancel
</Button>
<Button type="button">Cancel</Button>
</Field>
</FieldGroup>
</form>
@@ -48,9 +48,7 @@ export default function FieldResponsive() {
<Button variant="primary" type="submit">
Submit
</Button>
<Button type="button" variant="default">
Cancel
</Button>
<Button type="button">Cancel</Button>
</Field>
</FieldGroup>
</FieldSet>
@@ -25,9 +25,7 @@ function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) {
className="w-full"
/>
<div className="flex justify-end gap-2 py-3 px-4 border-t">
<Button variant="default" onClick={onCancel}>
Cancel
</Button>
<Button onClick={onCancel}>Cancel</Button>
<Button
variant="primary"
onClick={() =>
@@ -340,7 +340,6 @@ export default function FormPatternsPageLayout() {
</Button>
<div className="flex gap-2 items-center">
<Button
variant="default"
size="tiny"
icon={<Upload size={14} />}
onClick={() => uploadButtonRef.current?.click()}
@@ -349,7 +348,6 @@ export default function FormPatternsPageLayout() {
</Button>
{logoUrl && (
<Button
variant="default"
size="tiny"
icon={<Trash size={12} />}
onClick={() => {
@@ -454,7 +452,6 @@ export default function FormPatternsPageLayout() {
{file.name}
</span>
<Button
variant="default"
size="tiny"
icon={<Trash size={12} />}
onClick={() => {
@@ -760,24 +757,17 @@ export default function FormPatternsPageLayout() {
>
<div className="flex gap-2 items-center justify-end">
<Button
variant="default"
icon={<ExternalLink size={14} />}
onClick={() => console.log('Action performed')}
>
View documentation
</Button>
<Button variant="default" onClick={() => console.log('Reset action')}>
Reset API key
</Button>
<Button onClick={() => console.log('Reset action')}>Reset API key</Button>
</div>
</FormItemLayout>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button variant="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
{form.formState.isDirty && <Button onClick={() => form.reset()}>Cancel</Button>}
<Button variant="primary" type="submit" disabled={!form.formState.isDirty}>
Save changes
</Button>
@@ -338,7 +338,6 @@ export default function FormPatternsSidePanel() {
</Button>
{logoUrl && (
<Button
variant="default"
size="tiny"
icon={<Trash size={12} />}
onClick={() => {
@@ -445,7 +444,6 @@ export default function FormPatternsSidePanel() {
{file.name}
</span>
<Button
variant="default"
size="tiny"
icon={<Trash size={12} />}
onClick={() => {
@@ -772,15 +770,12 @@ export default function FormPatternsSidePanel() {
>
<div className="col-span-6 flex gap-2 items-center">
<Button
variant="default"
icon={<ExternalLink size={14} />}
onClick={() => console.log('Action performed')}
>
View documentation
</Button>
<Button variant="default" onClick={() => console.log('Reset action')}>
Reset API key
</Button>
<Button onClick={() => console.log('Reset action')}>Reset API key</Button>
</div>
</FormItemLayout>
</SheetSection>
@@ -788,7 +783,6 @@ export default function FormPatternsSidePanel() {
</Form>
<SheetFooter>
<Button
variant="default"
onClick={() => {
form.reset()
setOpen(false)
@@ -33,11 +33,7 @@ export default function InnerSideMenuEmpty() {
title="No functions found"
description="Create your first serverless function to get started."
illustration={<div className="text-4xl">🚀</div>}
actions={
<Button variant="default" onClick={() => setHasItems(true)}>
Create Function
</Button>
}
actions={<Button onClick={() => setHasItems(true)}>Create Function</Button>}
/>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
@@ -61,11 +57,7 @@ export default function InnerSideMenuEmpty() {
/>
</figure>
}
actions={
<Button variant="default" onClick={() => setHasItems(true)}>
Create Function
</Button>
}
actions={<Button onClick={() => setHasItems(true)}>Create Function</Button>}
/>
</InnerSideMenuCollapsibleContent>
</InnerSideMenuCollapsible>
@@ -11,10 +11,7 @@ export default function KeyboardShortcutDemo() {
<div className="flex w-full max-w-2xl flex-col gap-6">
<div className="flex flex-wrap gap-3">
<Button iconRight={<KeyboardShortcut keys={['Meta', 'S']} variant="inline" />}>Save</Button>
<Button
variant="default"
iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}
>
<Button iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}>
Run query
</Button>
</div>
@@ -5,10 +5,7 @@ export default function KeyboardShortcutInline() {
<div className="flex w-full max-w-xl flex-col gap-4">
<div className="flex flex-wrap gap-3">
<Button iconRight={<KeyboardShortcut keys={['Meta', 'S']} variant="inline" />}>Save</Button>
<Button
variant="default"
iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}
>
<Button iconRight={<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />}>
Apply
</Button>
</div>
@@ -16,18 +16,13 @@ export default function MultiSelectDemo() {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2">
<Button
size="tiny"
variant="default"
onClick={() => setLimit(limit - 1)}
disabled={limit < 1}
>
<Button size="tiny" onClick={() => setLimit(limit - 1)} disabled={limit < 1}>
<Minus size={12} />
</Button>
<span className="text-sm text-foreground/90 peer-checked:line-through font-semibold hover:cursor-pointer">
Limit: {limit}
</span>
<Button size="tiny" variant="default" onClick={() => setLimit(limit + 1)}>
<Button size="tiny" onClick={() => setLimit(limit + 1)}>
<Plus size={12} />
</Button>
</div>
@@ -27,9 +27,7 @@ export default function PageHeaderDemo() {
</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
<Button variant="default" size="small">
Secondary
</Button>
<Button size="small">Secondary</Button>
<Button variant="primary" size="small">
Deploy Function
</Button>
@@ -197,7 +197,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
layout="horizontal"
className="mb-4"
actions={
<Button variant="default" size="tiny" onClick={onNavigateToSmtp}>
<Button size="tiny" onClick={onNavigateToSmtp}>
Set up SMTP
</Button>
}
@@ -283,9 +283,7 @@ function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
})}
<CardFooter className="justify-end space-x-2">
{notificationsForm.formState.isDirty && (
<Button variant="default" onClick={() => notificationsForm.reset()}>
Cancel
</Button>
<Button onClick={() => notificationsForm.reset()}>Cancel</Button>
)}
<Button
variant="primary"
@@ -495,11 +493,7 @@ function SmtpPage() {
)}
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button variant="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
{form.formState.isDirty && <Button onClick={() => form.reset()}>Cancel</Button>}
<Button variant="primary" type="submit" disabled={!form.formState.isDirty}>
Save changes
</Button>
@@ -82,9 +82,7 @@ export default function PageLayoutDetail() {
<p className="text-sm">March 15, 2024</p>
</div>
<div className="pt-2">
<Button variant="default" size="small">
Change Plan
</Button>
<Button size="small">Change Plan</Button>
</div>
</div>
</CardContent>
@@ -114,9 +112,7 @@ export default function PageLayoutDetail() {
<p className="text-sm">$234.50</p>
</div>
<div className="pt-2">
<Button variant="default" size="small">
Configure Limits
</Button>
<Button size="small">Configure Limits</Button>
</div>
</div>
</CardContent>
@@ -146,9 +142,7 @@ export default function PageLayoutDetail() {
<p className="text-sm">12/2025</p>
</div>
<div className="pt-2">
<Button variant="default" size="small">
Update Payment Method
</Button>
<Button size="small">Update Payment Method</Button>
</div>
</div>
</CardContent>
@@ -234,9 +234,7 @@ export default function PageLayoutEdgeFunction() {
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button variant="default" size="tiny">
Test
</Button>
<Button size="tiny">Test</Button>
<Button variant="primary" size="tiny">
Deploy
</Button>
@@ -426,7 +424,7 @@ function OverviewPage() {
<PageSectionTitle>Errors since last deploy</PageSectionTitle>
</PageSectionSummary>
<PageSectionAside>
<Button variant="default" size="tiny" icon={<ExternalLink size={14} />}>
<Button size="tiny" icon={<ExternalLink size={14} />}>
View logs
</Button>
</PageSectionAside>
@@ -704,7 +702,7 @@ function CodePage() {
<h3 className="text-sm font-normal font-mono uppercase text-lighter tracking-wide">
Files
</h3>
<Button size="tiny" variant="default" icon={<Plus size={14} />} onClick={addNewFile}>
<Button size="tiny" icon={<Plus size={14} />} onClick={addNewFile}>
Add File
</Button>
</div>
@@ -20,9 +20,7 @@ export default function PageLayoutFullWidth() {
<PageBreadcrumbs
actions={
<PageBreadcrumbsActions>
<Button variant="default" size="tiny">
Docs
</Button>
<Button size="tiny">Docs</Button>
</PageBreadcrumbsActions>
}
>
@@ -118,12 +118,8 @@ export function PageLayoutLogsContent() {
/>
</div>
<div className="flex shrink-0 items-center gap-2">
<Button variant="default" size="tiny">
Live
</Button>
<Button variant="default" size="tiny">
Refresh
</Button>
<Button size="tiny">Live</Button>
<Button size="tiny">Refresh</Button>
</div>
</div>
@@ -161,9 +161,7 @@ export default function PageLayoutSettings() {
</CardContent>
<CardFooter className="justify-end space-x-2">
{refreshTokenForm.formState.isDirty && (
<Button variant="default" onClick={() => refreshTokenForm.reset()}>
Cancel
</Button>
<Button onClick={() => refreshTokenForm.reset()}>Cancel</Button>
)}
<Button
variant="primary"
@@ -266,9 +264,7 @@ export default function PageLayoutSettings() {
<CardFooter className="justify-end space-x-2">
{userSessionsForm.formState.isDirty && (
<Button variant="default" onClick={() => userSessionsForm.reset()}>
Cancel
</Button>
<Button onClick={() => userSessionsForm.reset()}>Cancel</Button>
)}
<Button
variant="primary"
@@ -21,9 +21,7 @@ export default function PageSectionDemo() {
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<Button variant="default" size="small">
Action
</Button>
<Button size="small">Action</Button>
</PageSectionAside>
</PageSectionMeta>
<PageSectionContent>
@@ -21,9 +21,7 @@ export default function PageSectionWithAside() {
</PageSectionDescription>
</PageSectionSummary>
<PageSectionAside>
<Button variant="default" size="small">
Secondary
</Button>
<Button size="small">Secondary</Button>
<Button variant="primary" size="small">
Primary Action
</Button>
@@ -168,9 +168,7 @@ export default function SheetConfirmOnCloseDemo() {
return (
<>
<Button variant="default" onClick={openSheet}>
Open endpoint sheet
</Button>
<Button onClick={openSheet}>Open endpoint sheet</Button>
<Sheet open={open} onOpenChange={handleOpenChange}>
<SheetContent className="flex flex-col gap-0">
@@ -210,9 +208,7 @@ export default function SheetConfirmOnCloseDemo() {
</SheetSection>
<Separator />
<SheetFooter>
<Button variant="default" onClick={confirmOnClose}>
Cancel
</Button>
<Button onClick={confirmOnClose}>Cancel</Button>
<Button variant="primary" onClick={saveChanges} disabled={!isDirty}>
Save changes
</Button>
@@ -4,7 +4,6 @@ import { Button } from 'ui'
export default function SonnerDemo() {
return (
<Button
variant="default"
onClick={() =>
toast('Event has been created', {
description: 'Sunday, December 03, 2023 at 9:00 AM',
@@ -7,11 +7,8 @@ export default function SonnerDemo() {
return (
<div className="flex gap-1">
<Button variant="default" onClick={() => toast('Event has been created')}>
Default
</Button>
<Button onClick={() => toast('Event has been created')}>Default</Button>
<Button
variant="default"
onClick={() =>
toast.message('Event has been created', {
description: 'Monday, January 3rd at 6:00pm',
@@ -21,7 +18,6 @@ export default function SonnerDemo() {
description
</Button>
<Button
variant="default"
onClick={() =>
toast.success('Event has been created', {
description: 'Sunday, December 03, 2023 at 9:00 AM',
@@ -30,13 +26,8 @@ export default function SonnerDemo() {
>
Success
</Button>
<Button variant="default" onClick={() => toast.success('Event has been created')}>
Show Toast
</Button>
<Button
variant="default"
onClick={() => toast.info('Be at the area 10 minutes before the event time')}
>
<Button onClick={() => toast.success('Event has been created')}>Show Toast</Button>
<Button onClick={() => toast.info('Be at the area 10 minutes before the event time')}>
Info
</Button>
<Button
@@ -61,11 +52,8 @@ export default function SonnerDemo() {
>
Action
</Button>
<Button variant="default" onClick={() => toast.loading('Event has been created')}>
Loading
</Button>
<Button onClick={() => toast.loading('Event has been created')}>Loading</Button>
<Button
variant="default"
onClick={() =>
toast.promise(promise, {
loading: 'Loading...',
@@ -80,12 +68,11 @@ export default function SonnerDemo() {
Promise
</Button>
<Button
variant="default"
onClick={() =>
toast(
<>
<div>A custom toast with default styling</div>
<Button variant="default">Hello world</Button>
<Button>Hello world</Button>
</>
)
}
@@ -60,7 +60,6 @@ export default function SonnerUpload() {
return (
<div className="flex flex-col gap-3">
<Button
variant="default"
onClick={async () => {
// random id
const toastId = Math.random()
@@ -54,13 +54,12 @@ export default function TableActions() {
<TableCell>{user.name}</TableCell>
<TableCell className="text-foreground-lighter">{user.email}</TableCell>
<TableCell className="flex items-center gap-x-2">
<Button variant="default" size="tiny" className="hit-area-2">
<Button size="tiny" className="hit-area-2">
Inspect
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
icon={<EllipsisVertical />}
aria-label="More actions"
className="w-7 hit-area-2"
@@ -84,13 +84,12 @@ export default function TableCrossLink() {
</Link>
</TableCell>
<TableCell className="flex items-center gap-x-2">
<Button variant="default" size="tiny" icon={<Edit2 />} className="hit-area-2">
<Button size="tiny" icon={<Edit2 />} className="hit-area-2">
Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
icon={<EllipsisVertical />}
aria-label="More actions"
className="w-7 hit-area-2"
@@ -103,7 +103,6 @@ export default function TableRowLinkActions() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="default"
icon={<EllipsisVertical />}
aria-label="More actions"
className="w-7 hit-area-2"
+28 -6
View File
@@ -1,3 +1,5 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { mkdir, rename, writeFile } from 'node:fs/promises'
import path from 'path'
import { getHighlighter, loadTheme } from '@shikijs/compat'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
@@ -10,6 +12,13 @@ import { defineConfig, s } from 'velite'
import { rehypeComponent } from './lib/rehype-component'
const CODE_OUTPUT_DIR = '.velite/codes'
function toCodeId(slugAsParams) {
if (!slugAsParams) return 'index'
return Buffer.from(slugAsParams, 'utf8').toString('base64url')
}
const LinksProperties = s.object({
doc: s.string().optional(),
api: s.string().optional(),
@@ -46,16 +55,29 @@ const docs = s
// real benefit for a dev-only content cache, and dominates build time.
code: s.mdx({ copyLinkedFiles: false, minify: false }),
})
.transform(({ path: flattenedPath, ...data }) => ({
...data,
slug: `/${flattenedPath}`,
slugAsParams: flattenedPath.split('/').slice(1).join('/'),
}))
.transform(async ({ path: flattenedPath, code, ...data }) => {
const slugAsParams = flattenedPath.split('/').slice(1).join('/')
const codeId = toCodeId(slugAsParams)
const codesDir = path.join(process.cwd(), CODE_OUTPUT_DIR)
await mkdir(codesDir, { recursive: true })
const codePath = path.join(codesDir, `${codeId}.json`)
const tmpPath = `${codePath}.tmp`
await writeFile(tmpPath, JSON.stringify(code), 'utf8')
await rename(tmpPath, codePath)
return {
...data,
slug: `/${flattenedPath}`,
slugAsParams,
codeId,
}
})
export default defineConfig({
root: './content',
output: {
clean: true,
clean: process.env.NODE_ENV === 'production',
},
collections: {
allDocs: {
+1 -1
View File
@@ -30,7 +30,7 @@ Ask your agent for a skill by name (`pm-the-docs`, `ask-the-docs`, `write-the-do
| [`pm-the-docs`](../../.agents/skills/pm-the-docs/SKILL.md) | Frame / Shape | Audience, product-stage, and cross-cutting scope calls (universe when you have Supabase org access, else OSS path) |
| [`ask-the-docs`](../../.agents/skills/ask-the-docs/SKILL.md) | Frame / Shape | `apps/docs` architecture, IA placement, and where content lives |
| [`write-the-docs`](../../.agents/skills/write-the-docs/SKILL.md) | Draft | Drafting net-new content grounded in the code |
| [`edit-the-docs`](../../.agents/skills/edit-the-docs/SKILL.md) | Edit | Restructure and improve existing pages |
| [`edit-the-docs`](../../.agents/skills/edit-the-docs/SKILL.md) | Edit | Restructure and improve existing pages, split by change type |
| [`test-the-docs`](../../.agents/skills/test-the-docs/SKILL.md) | Draft / Self-review | Execute docs snippets in a Docker-isolated local stack; verification report |
| [`review-the-docs`](../../.agents/skills/review-the-docs/SKILL.md) | Self-review / PR review | Checking a draft and PR triage/verification |
+1 -1
View File
@@ -7,7 +7,7 @@ import { Button } from 'ui'
const ErrorPage = ({ error }) => {
useEffect(() => {
Sentry.captureException(error)
Sentry.captureException(error, { tags: { globalErrorBoundary: true } })
}, [error])
return (
+1 -1
View File
@@ -6,7 +6,7 @@ import { useEffect } from 'react'
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error)
Sentry.captureException(error, { tags: { globalErrorBoundary: true } })
}, [error])
return (
+1 -1
View File
@@ -17,7 +17,7 @@ export default function NotFound() {
</p>
<div className="flex flex-wrap gap-4 pt-4">
<SearchButton />
<Button variant="default" size="small" className="p-4" asChild>
<Button size="small" className="p-4" asChild>
<Link href="/" className="no-underline">
Return to homepage
</Link>
@@ -106,7 +106,7 @@ function FeedbackModal({ visible, page, onCancel, onSubmit }: FeedbackModalProps
</Form>
<DialogFooter>
<div className="flex items-center justify-end gap-2">
<Button type="reset" variant="default" onClick={handleCancel} disabled={isSubmitting}>
<Button type="reset" onClick={handleCancel} disabled={isSubmitting}>
Cancel
</Button>
<Button
+3 -1
View File
@@ -109,10 +109,12 @@ function AiTools({ className }: { className?: string }) {
const GuidesSidebar = ({
className,
video,
videoTitle,
hideToc,
}: {
className?: string
video?: string
videoTitle?: string
hideToc?: boolean
}) => {
const pathname = usePathname()
@@ -125,7 +127,7 @@ const GuidesSidebar = ({
<div className="w-full relative border-l flex flex-col gap-6 lg:gap-8 px-2 h-fit">
{video && (
<div className="relative pl-5">
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} videoTitle={videoTitle} />
</div>
)}
{showFeedback && (
@@ -164,7 +164,7 @@ const GlobalMobileMenu = ({ open, setOpen }: Props) => {
</Button>
) : (
<>
<Button block size="medium" variant="default" asChild>
<Button block size="medium" asChild>
<Link href="https://supabase.com/dashboard/sign-in">Sign in</Link>
</Button>
<Button variant="primary" block size="medium" asChild>
+2
View File
@@ -1,3 +1,5 @@
'use client'
import React, { TableHTMLAttributes, useEffect, useRef, useState } from 'react'
import { cn } from 'ui'
@@ -551,11 +551,4 @@ Want to learn more about the awesome tech that is powering this?
- Read the pgvector Docs for [Embeddings and vector similarity](/docs/guides/database/extensions/pgvector)
- Watch Greg's video for a full breakdown:
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Yhtjd7yGGGA"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="Yhtjd7yGGGA" title="Vector search with Next.js and OpenAI" />
@@ -102,11 +102,4 @@ supabase secrets set --env-file ./supabase/.env.local
If you're interesting in learning how to use this to build your own ChatGPT, read [the blog post](/blog/chatgpt-supabase-docs) and check out the video:
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Yhtjd7yGGGA"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="Yhtjd7yGGGA" title="Building vector search with OpenAI embeddings" />
+1 -1
View File
@@ -10,7 +10,7 @@ sidebar_label: 'Google Colab'
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
<img src="/docs/img/ai/colab-badge.svg" alt="Open in Colab" />
</a>
Google Colab is a hosted Jupyter Notebook service. It provides free access to computing resources, including GPUs and TPUs, and is well-suited to machine learning, data science, and education. We can use Colab to manage collections using [Supabase Vecs](/docs/guides/ai/vecs-python-client).
@@ -17,7 +17,7 @@ Launch our [LlamaIndex](https://github.com/supabase/supabase/blob/master/example
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/llamaindex/llamaindex.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
<img src="/docs/img/ai/colab-badge.svg" alt="Open in Colab" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
@@ -22,7 +22,7 @@ Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/face_similarity.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
<img src="/docs/img/ai/colab-badge.svg" alt="Open in Colab" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
@@ -23,7 +23,7 @@ Launch our [`vector_hello_world`](https://github.com/supabase/supabase/blob/mast
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/vector_hello_world.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
<img src="/docs/img/ai/colab-badge.svg" alt="Open in Colab" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
@@ -23,7 +23,7 @@ Launch our [`semantic_text_deduplication`](https://github.com/supabase/supabase/
className="w-64"
href="https://colab.research.google.com/github/supabase/supabase/blob/master/examples/ai/semantic_text_deduplication.ipynb"
>
<img src="/docs/img/ai/colab-badge.svg" />
<img src="/docs/img/ai/colab-badge.svg" alt="Open in Colab" />
</a>
At the top of the notebook, you'll see a button `Copy to Drive`. Click this button to copy the notebook to your Google Drive.
@@ -565,14 +565,7 @@ Future<void> _nativeGoogleSignIn() async {
...
```
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/utMg6fVmX0U"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="utMg6fVmX0U" title="Google One Tap sign-in with Next.js" />
</TabPanel>
@@ -593,14 +586,7 @@ await supabase.auth.signInWithOAuth(
This call takes the user to Google's consent screen. Once the flow ends, the user's profile information is exchanged and validated with Supabase Auth before it redirects back to your Flutter application with an access and refresh token representing the user's session.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/utMg6fVmX0U"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="utMg6fVmX0U" title="Google sign-in with Flutter on web and desktop" />
</TabPanel>
@@ -755,14 +741,7 @@ Button(onClick = { authState.startFlow() }) {
}
```
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/P_jZMDmodG4"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="P_jZMDmodG4" title="Google sign-in with Kotlin Multiplatform" />
</TabPanel>
@@ -111,7 +111,7 @@ For convenience, you can also use the [Supabase client libraries](/docs/referenc
- [JavaScript](/docs/reference/javascript/introduction)
- [Flutter](/docs/reference/dart/introduction)
- [Swift](/docs/reference/swift)
- [Swift](/docs/reference/swift/introduction)
- [Python](/docs/reference/python/introduction)
- [C#](/docs/reference/csharp/introduction)
- [Kotlin](/docs/reference/kotlin/introduction)
@@ -9,14 +9,7 @@ The `http` extension allows you to call RESTful endpoints within Postgres.
## Quick demo
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/rARgrELRCwY"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="rARgrELRCwY" title="Calling an external API with the http extension" />
## Overview
@@ -10,14 +10,7 @@ These functions live inside your database, and they can be [used with the API](.
## Quick demo
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/MJZCCpCYEqk"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="MJZCCpCYEqk" title="Creating and calling Postgres functions" />
## Getting started
@@ -657,33 +650,12 @@ select advanced_example();
### Create Database Functions
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/MJZCCpCYEqk"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="MJZCCpCYEqk" title="Creating a Postgres function in the dashboard" />
### Call Database Functions using JavaScript
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/I6nnp9AINJk"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="I6nnp9AINJk" title="Calling a Postgres function from JavaScript" />
### Using Database Functions to call an external API
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/rARgrELRCwY"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="rARgrELRCwY" title="Calling an external API from a Postgres function" />
@@ -109,7 +109,7 @@ Used by the Auth middleware to connect to the database and run migration. Access
### `supabase_storage_admin`
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `storage` schema.
Used by the Storage middleware to connect to the database and run migration. Access is scoped to the `storage` schema.
### `supabase_etl_admin`
@@ -125,7 +125,7 @@ This role:
### `dashboard_user`
For running commands via the Supabase UI.
The Supabase Dashboard doesn't connect as this role. Queries you run in the Dashboard execute as `postgres` and include a `-- source: dashboard` comment, which you can use to [find them in the Postgres logs](/docs/guides/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj).
### `supabase_admin`
+1 -8
View File
@@ -423,14 +423,7 @@ For example if you had the following situations:
>
<TabPanel id="dashboard" label="Dashboard">
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/TKwF3IGij5c"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="TKwF3IGij5c" title="Joining tables with foreign keys in the dashboard" />
</TabPanel>
<TabPanel id="sql" label="SQL">
+1 -9
View File
@@ -163,15 +163,7 @@ updated_at | 2022-12-14 02:51:13.938396+00
## Deep dive
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/QHLPNDrdN2w"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
<YouTube id="QHLPNDrdN2w" title="Storing secrets with Supabase Vault" />
As we mentioned, Vault stores secrets in an authenticated encrypted form. There are some details around that you may be curious about. What does authenticated mean? Where is the encryption key stored? This section explains those details.
@@ -16,14 +16,7 @@ Database Webhooks are very similar to triggers, and that's because Database Webh
This video demonstrates how you can create a new customer in Stripe each time a row is inserted into a `profiles` table:
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/codAs9-NeHM"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="codAs9-NeHM" title="Sending Database Webhooks on table changes" />
## Creating a webhook
@@ -69,6 +69,12 @@ supabase db reset
# Navigate to Branches > Your Branch > View Logs
```
### Permission denied errors on a new branch
If the Data API returns `42501` permission denied errors on a branch for tables or functions that work on your base project, the branch is missing default privileges on the `public` schema. New branches are created without them, and only your migrations can grant them back.
Check whether your initial migration contains `alter default privileges ... grant` statements for the `public` schema. If it doesn't, see [Default privileges on branches](/docs/guides/deployment/branching/working-with-branches#default-privileges-on-branches) for how to add them, or [grant access explicitly](/docs/guides/api/securing-your-api#grant-access-explicitly) in a migration.
### Migration order problems
Migrations must run in the correct order. Common issues:
@@ -183,6 +183,105 @@ Migrations are run in sequential order. Each migration builds upon the previous
The preview branch inherits the migration history of your base project, so it only applies migrations that haven't been run yet. This can create an issue when rolling back migrations.
### Default privileges on branches
New branches are secure by default. They are created without [default privileges](/docs/guides/api/securing-your-api#default-privileges) on the `public` schema, regardless of the setting on your base project. New tables, functions, and sequences on a branch require explicit grants before `anon`, `authenticated`, or `service_role` can reach them through the Data API.
Your migrations control whether a branch re-enables these privileges. If your base project has default privileges enabled and your migration history was initialized by Supabase Branching or the Supabase CLI, the initial migration already contains the `alter default privileges` statements that grant access on `public`. Running it on a new branch restores the same access your base project has, so no changes are needed.
Two cases require manual intervention:
- You manage migrations outside Supabase and want to [keep default privileges enabled](#keep-default-privileges-enabled) on branches.
- You use Supabase managed migrations and want to [revoke default privileges](#revoke-default-privileges) on your base project and branches.
#### Keep default privileges enabled
If your migration history was not initialized by Supabase Branching or the Supabase CLI, your initial migration doesn't grant default privileges, so new branches start without them. To restore the same access your base project has:
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Turn on default privileges on your base project" fullWidth>
Open the [Data API settings](/dashboard/project/_/integrations/data_api/settings) in the Supabase Dashboard and turn on **Default privileges for new entities**.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Add grants to your initial migration" fullWidth>
Insert the following statements at the start of your initial migration file. Subsequent migrations then inherit these privileges, so the final database state is unchanged.
```sql
alter default privileges for role postgres in schema public grant usage, select, update on sequences to anon, authenticated, service_role;
alter default privileges for role postgres in schema public grant execute on functions to anon, authenticated, service_role;
alter default privileges for role postgres in schema public grant select, insert, update, delete on tables to anon, authenticated, service_role;
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Repair the migration history of your base project" fullWidth>
Mark the updated migration as applied so it isn't rerun on your base project. See [Diagnosing and fixing sync errors](/docs/guides/deployment/database-migrations#diagnosing-and-fixing-sync-errors).
```bash
supabase migration repair --status applied <migration-timestamp>
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
#### Revoke default privileges
For improved security, we recommend not exposing the `public` schema automatically on your base project either. If your initial migration was generated by Supabase Branching or the Supabase CLI, it re-grants default privileges when it runs on a branch. To revoke them on your base project and branches:
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="Turn off default privileges on your base project" fullWidth>
Open the [Data API settings](/dashboard/project/_/integrations/data_api/settings) in the Supabase Dashboard and turn off **Default privileges for new entities**.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="Add a new migration that revokes the grants" fullWidth>
Create a new migration file with the Supabase CLI. Don't edit the initial migration, because that affects subsequent migrations in your history.
```bash
supabase migration new revoke_default_privileges
```
Add the following statements to the generated file:
```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, public;
alter default privileges for role postgres in schema public revoke usage, select, update on sequences from anon, authenticated, service_role;
```
</StepHikeCompact.Details>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={3}>
<StepHikeCompact.Details title="Commit and push" fullWidth>
Commit the new migration file and push it to your Git repository. The migration runs on your base project when merged and on every new branch, so both start without default privileges.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
### Using ORM or custom seed scripts
If you want to use your own ORM for managing migrations and seed scripts, you will need to run them in GitHub Actions after the preview branch is ready. The branch credentials can be fetched using the following example GHA workflow.
@@ -5,14 +5,7 @@ description: 'Building a Slash Command Discord Bot with Edge Functions.'
video: 'https://www.youtube.com/v/J24Bvo_m7DM'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/J24Bvo_m7DM"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="J24Bvo_m7DM" title="Building a Discord bot with Edge Functions" />
## Create an application on Discord Developer portal
@@ -5,14 +5,7 @@ description: 'Deploying Edge Functions with GitHub Actions.'
video: 'https://www.youtube.com/v/l2KlzGrhB6w'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/l2KlzGrhB6w"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="l2KlzGrhB6w" title="Deploying Edge Functions with GitHub Actions" />
Use the Supabase CLI together with GitHub Actions to automatically deploy our Supabase Edge Functions. [View on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/github-action-deploy).
@@ -5,14 +5,7 @@ description: 'Generate Open Graph images with Deno and Supabase Edge Functions.'
video: 'https://www.youtube.com/v/jZgyOJGWayQ'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/jZgyOJGWayQ"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="jZgyOJGWayQ" title="Generating OG images with Edge Functions" />
Generate Open Graph images with Deno and Supabase Edge Functions. [View on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/opengraph).
@@ -286,14 +286,7 @@ Push notifications are an important part of any mobile app. They allow you to se
1. In your `notifications` table, insert a new row.
1. Watch the magic happen 🪄
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/CiSv9E6ZKVc"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="CiSv9E6ZKVc" title="Sending push notifications from Edge Functions" />
</TabPanel>
</Tabs>
@@ -3,14 +3,7 @@ title: 'Rate Limiting Edge Functions'
description: 'Rate Limiting Edge Functions with Upstash Redis.'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/o4ooiE-SdUg"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="o4ooiE-SdUg" title="Rate limiting Edge Functions with Upstash Redis" />
[Redis](https://redis.io/about/) is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine. It is optimized for atomic operations like incrementing a value, for example for a view counter or rate limiting. We can even rate limit based on the user ID from Supabase Auth!
@@ -3,14 +3,7 @@ title: 'Taking Screenshots with Puppeteer'
description: 'Take screenshots in Edge Functions with Puppeteer and Browserless.io.'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Q1nfnQggR4c"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="Q1nfnQggR4c" title="Taking screenshots with Puppeteer and Edge Functions" />
[Puppeteer](https://pptr.dev/) is a handy tool to programmatically take screenshots and generate PDFs. However, trying to do so in Edge Functions can be challenging due to the size restrictions. Luckily there is a [serverless browser offering available](https://www.browserless.io/) that we can connect to via WebSockets.
@@ -3,14 +3,7 @@ title: 'Handling Stripe Webhooks'
description: 'Handling signed Stripe Webhooks with Edge Functions.'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/6OMVWiiycLs"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="6OMVWiiycLs" title="Handling signed Stripe webhooks with Edge Functions" />
Handling signed Stripe Webhooks with Edge Functions. [View on GitHub](https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/stripe-webhooks/index.ts).
@@ -5,13 +5,6 @@ description: 'Building a Telegram Bot with Edge Functions.'
video: 'https://www.youtube.com/v/AWfE3a9J_uo'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/AWfE3a9J_uo"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="AWfE3a9J_uo" title="Building a Telegram bot with Edge Functions" />
Handle Telegram Bot Webhooks with the [grammY framework](https://grammy.dev/). grammY is an open source Telegram Bot Framework which makes it easy to handle and respond to incoming messages. [View on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/telegram-bot).
@@ -3,14 +3,7 @@ title: 'Upstash Redis'
description: 'Build an Edge Functions Counter with Upstash Redis.'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/OPg3_oPZCh0"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="OPg3_oPZCh0" title="Using Upstash Redis with Edge Functions" />
A Redis counter example that stores a [hash](https://redis.io/commands/hincrby/) of function invocation count per region. Find the code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/upstash-redis-counter).
@@ -4,14 +4,7 @@ title: 'Type-Safe SQL with Kysely'
description: 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/zd9a_Lk3jAc"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="zd9a_Lk3jAc" title="Type-safe SQL in Edge Functions with Kysely" />
Supabase Edge Functions can [connect directly to your Postgres database](/docs/guides/functions/connect-to-postgres) to execute SQL queries. [Kysely](https://github.com/kysely-org/kysely#kysely) is a type-safe and autocompletion-friendly typescript SQL query builder.
@@ -4,14 +4,7 @@ title: 'Scheduling Edge Functions'
description: 'Schedule Edge Functions with pg_cron.'
---
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/-U6DJcjVvGo"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="-U6DJcjVvGo" title="Scheduling Edge Functions with pg_cron" />
The hosted Supabase Platform supports the [`pg_cron` extension](/docs/guides/database/extensions/pg_cron), a recurring job scheduler in Postgres.
@@ -23,14 +23,7 @@ This page is a focused tutorial on migrations. If you want to move an existing p
Database changes are managed through "migrations." Database migrations are a common way of tracking changes to your database over time.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/Kx5nHBmIxyQ"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="Kx5nHBmIxyQ" title="Managing database migrations with the Supabase CLI" />
For this guide, we'll create a table called `employees` and see how we can make changes to it.
@@ -13,7 +13,7 @@ For example, the schema path `metadata.request.cf.country` is queried as `log_at
<Tabs scrollable size="small" type="underlined" defaultActiveId="edge_logs" queryGroup="source">
{logConstants.schemas.map((schema) => (
<TabPanel id={schema.reference} key={schema.reference} label={schema.name}>
<table>
<Table>
<thead>
<tr>
<th className="font-bold">Schema path</th>
@@ -36,7 +36,7 @@ For example, the schema path `metadata.request.cf.country` is queried as `log_at
</tr>
))}
</tbody>
</table>
</Table>
</TabPanel>
))}
</Tabs>
@@ -12,14 +12,7 @@ Alternatively, use the [Heroku to Supabase migration tool](https://migrate.supab
## Quick demo
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/xsRhPMphtZ4"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="xsRhPMphtZ4" title="Migrating a Heroku Postgres database to Supabase" />
## Retrieve your Heroku database credentials [#retrieve-heroku-credentials]
@@ -7,11 +7,4 @@ sidebar_label: 'Videos'
The Postgres Changes extension listens for database changes and sends them to clients which enables you to receive database changes in real-time.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/gboTC2lcgzw?si=WBfCrZyqi9zDWS5n"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="gboTC2lcgzw" title="Listening to Postgres changes with Flutter" />
@@ -9,11 +9,4 @@ Use Supabase Presence to display the currently online users on your Flutter appl
Displaying the list of currently online users is a common feature for real-time collaborative applications. Supabase Presence makes it easy to track users joining and leaving the session so that you can make a collaborative app.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/B2NZvZ2uLNs?si=2JmxGOFuwwUGaTxr"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="B2NZvZ2uLNs" title="Using Realtime Presence with Flutter" />
@@ -8,11 +8,4 @@ sidebar_label: 'Videos'
In this guide, we explore the best ways to receive real-time Postgres changes with your Next.js application.
We'll show both client and server side updates, and explore which option is best.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/YR-xP6PPXXA"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="YR-xP6PPXXA" title="Building a Realtime app with Next.js" />
@@ -93,13 +93,7 @@ const changes = supabase
Postgres Changes require minimal setup, but have some [limitations](/docs/guides/realtime/postgres-changes#limitations) as your application scales. We recommend using Broadcast for most use cases.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/2rUjcmgZDwQ"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="2rUjcmgZDwQ" title="Subscribing to Postgres changes with Realtime" />
### Enable Postgres Changes
@@ -609,14 +609,7 @@ Some suggested systems include:
## Demo
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/FqiQKRKsfZE"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="FqiQKRKsfZE" title="Self-hosting Supabase on a DigitalOcean droplet" />
1. The VPS instance is a DigitalOcean droplet. (For server requirements refer to [System requirements](#system-requirements))
2. To access Studio, use the IPv4 IP address of your Droplet.
@@ -314,11 +314,4 @@ create policy "Public Access"
{/* Finish with a video. This also appears in the Sidebar via the "tocVideo" metadata */}
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/J9mTPY8rIXE"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="J9mTPY8rIXE" title="Adding security rules to Supabase Storage" />
@@ -102,14 +102,7 @@ create policy "Avatar images are publicly accessible." on storage.objects
{/* Finish with a video. This also appears in the Sidebar via the "tocVideo" metadata */}
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/4ERX__Y908k"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="4ERX__Y908k" title="Storage access control policy examples" />
## Bypassing access controls
@@ -801,11 +801,4 @@ IMGPROXY_URL=yourinternalimgproxyurl.internal.com
{/* Finish with a video. This also appears in the Sidebar via the "tocVideo" metadata */}
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/dLqSmxX3r7I"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<YouTube id="dLqSmxX3r7I" title="Configuring the Storage image transformation API" />
Loaded 100 of 882 files, more files were not shown because too many files have changed in this diff. Show more