docs: add Personal Access Tokens guide with generated permission tables (#49732)

Add a guide that compares classic and scoped personal access tokens,
explains how account roles constrain token permissions, and walks
through creating and testing a project-scoped token. Include generated
tables mapping permissions to Management API endpoints and MCP tools,
and link the guide from docs navigation and Studio token sheets.

Move the scoped-token permission catalog from Studio into shared-data.
Studio and docs generation now share permission names, categories,
descriptions, risk metadata, modes, scopes, and display order.

Generate the tables from the shared catalog, OpenAPI
x-fga-permissions, and the downloaded MCP permission map. Exclude
Workers permissions until the feature is live.

Run regeneration through the docs Makefile, verify checked-in output in
CI, and refresh it in the weekly Management API workflow. Add Dashboard
and Docs ownership plus contributor guidance so permission changes stay
synchronized.
This commit is contained in:
Wen Bo Xie authored and GitHub committed 2026-09-01 12:30:56 +00:00
1 parent b278b1ec8a
commit 2681a21f5c
24 files changed
+1474 -634

No files matched your search

+1 -1
View File
@@ -87,7 +87,7 @@ at hand — they cite each other where context matters.
| [`reference/llm-agent-parity.md`](./reference/llm-agent-parity.md) | HTML↔markdown fidelity (e.g. AI prompts), search caveat, agent onboarding guides, in-flux wiring. |
| [`reference/federated-docs.md`](./reference/federated-docs.md) | How docs pulls markdown from external repos at build time. Routes, `pageMap`, remark/rehype plugins, link transforms, known failure modes. |
| [`reference/ci-and-lint.md`](./reference/ci-and-lint.md) | GitHub Actions on every PR — `docs_lint`, `Docs Tests`, typecheck, prettier, Vercel preview gate. Where to add a check before creating a new one. |
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI download → Redocly bundle → codegen → `ApiEndpointSection`; why not to swap in Scalar/Redoc. |
| [`reference/management-api-reference.md`](./reference/management-api-reference.md) | Management API OpenAPI → reference generation, including scoped PAT permission tables; why not to swap in Scalar/Redoc. |
| [`reference/gotchas.md`](./reference/gotchas.md) | Specific traps to watch for. One-liner per item. |
## How to use during a chat
@@ -14,7 +14,7 @@ PR opened / updated
│
├── docs_lint (MDX/content linting; required)
├── docs_lint_comment_external (posts results as PR comments for external PRs)
├── Docs Tests (pnpm test:docs on .ts* file changes)
├── Docs Tests (pnpm test:docs on relevant docs code/spec changes)
├── TypeScript & Lint (tsc + eslint)
├── Prettier (format check)
├── reviewdog (inline annotations)
@@ -36,15 +36,16 @@ check before merging.** Backed by `supa-mdx-lint`.
### 2. Docs Tests (`docs-tests.yml`)
Triggered on PRs to master when files matching `apps/docs/**/*.ts*` or
`apps/docs/spec/**/*.json` change. Runs on a Blacksmith 4-vCPU Ubuntu runner
with concurrency controls to cancel stale builds.
Triggered on relevant docs code/spec changes, including the generated scoped
PAT partials and their shared permission catalog. Runs on a Blacksmith 4-vCPU
Ubuntu runner with concurrency controls to cancel stale builds.
- Sparse checkout of `apps/docs`, `examples`, `packages`, `supabase`, and
`patches`.
- Install pnpm (pinned hash).
- Set up Node.js from `.nvmrc`.
- `pnpm install --frozen-lockfile`.
- Regenerate the scoped PAT partials and fail if the committed output drifts.
- Run `pnpm run test:docs` (with dummy GitHub OAuth env vars to prevent local
Supabase startup errors).
@@ -92,6 +92,15 @@ the custom cycle-safe resolve remains required.
| `apps/docs/features/docs/Reference.sections.tsx` | `ApiEndpointSection` UI |
| `apps/docs/internals/generate-reference-markdown.ts` | agent markdown export |
## Scoped personal access token permission tables
The "Personal Access Tokens" guide's permission and MCP tables are generated
from the Management API specs, MCP permission map, and the same shared catalog
Studio uses. Regenerate `content/_partials/access-control/scoped_pat_*.mdx` with
`make -C apps/docs/spec generate.partials.access-control`. Docs Tests runs this
on relevant pull requests and fails if the partials drift; the weekly Management
API update also runs it after refreshing the live inputs.
## Related
- [`build-pipeline.md`](./build-pipeline.md) — where `codegen:references`
+1 -1
View File
@@ -41,7 +41,7 @@ pnpm api:codegen # platform Management API types → packages/api-ty
Every PR must pass typecheck + lint (one workflow), Prettier, and a typos check. Other checks are path-filtered: Studio unit tests/build and the lint ratchet (ESLint warning count must not increase) run on `apps/studio/**` changes; app-specific test suites run on their own paths.
Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`.
Never hand-edit generated files: `packages/api-types/types/**`, `**/routeTree.gen.ts`, `**/__generated__/**`, `apps/docs/features/docs/generated/**`, `apps/www/.generated/**`, `supabase/functions/common/database-types.ts`, `apps/docs/content/_partials/access-control/scoped_pat_*.mdx` (run `make -C apps/docs/spec generate.partials.access-control`).
## Conventions
+1
View File
@@ -1,6 +1,7 @@
/packages/ui/ @supabase/design
/packages/shared-data/pricing.ts @supabase/billing
/packages/shared-data/plans.ts @supabase/billing
/packages/shared-data/scoped-access-token-permissions.ts @supabase/Dashboard @supabase/docs
/packages/common/telemetry-constants.ts @supabase/growth-eng
/packages/dev-tools/ @supabase/growth-eng
# /packages/pg-meta @supabase/postgres @avallete
+23 -10
View File
@@ -3,7 +3,7 @@ name: Update Mgmt Api Docs
on:
schedule:
# Run at 00:00 UTC every Monday
- cron: "0 0 * * 1"
- cron: '0 0 * * 1'
workflow_dispatch:
permissions:
@@ -19,10 +19,13 @@ jobs:
with:
persist-credentials: false
ref: ${{ github.ref }}
# The PAT tables generator imports the shared catalog and its base tsconfig.
sparse-checkout: |
apps/docs
patches
packages/generator
packages/shared-data
packages/tsconfig
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
name: Install pnpm
@@ -32,15 +35,15 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "pnpm"
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Change to apps/docs/spec directory and run make command
- name: Refresh Management API docs
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
run: make download.api.v1 download.mcp-tools-permissions dereference.api.v1 generate.sections.api.v1 generate.partials.access-control format
- name: Generate token
id: app-token
@@ -55,8 +58,18 @@ jobs:
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
with:
token: ${{ steps.app-token.outputs.token }}
commit-message: "feat: update mgmt api docs"
title: "feat: update mgmt api docs"
body: "This PR updates mgmt api docs automatically."
branch: "gha/auto-update-mgmt-api-docs"
base: "master"
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: |
This PR updates Management API docs automatically.
This regenerates:
- Management API specs and sections
- Personal Access Tokens permission-to-endpoint table
- Personal Access Tokens MCP tool permissions table
Sources include the live Management API specs, MCP permission map,
and Studio's shared permission catalog.
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
+12
View File
@@ -6,8 +6,13 @@ on:
paths:
- 'apps/docs/**/*.ts*'
- 'apps/docs/spec/**/*.json'
- 'apps/docs/spec/Makefile'
- 'apps/docs/spec/sections/generateAccessControlPartials.mts'
- 'apps/docs/content/_partials/access-control/**'
- 'apps/docs/.env.development'
- 'apps/docs/package.json'
- 'packages/shared-data/package.json'
- 'packages/shared-data/scoped-access-token-permissions.ts'
- 'e2e/docs/local-smoke/**'
- 'e2e/docs/playwright.local-smoke.config.ts'
- 'e2e/docs/package.json'
@@ -52,6 +57,13 @@ jobs:
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Check access-control partials
run: |
make -C apps/docs/spec generate.partials.access-control
git diff --exit-code -- \
apps/docs/content/_partials/access-control/scoped_pat_permissions.mdx \
apps/docs/content/_partials/access-control/scoped_pat_mcp_tools.mdx
- name: Download JS reference TypeDoc dumps
# The source dumps under apps/docs/spec/reference/<lib>/<ver>/*.json are
# gitignored — `make download.tsdoc.v2` re-fetches them from
@@ -2728,6 +2728,11 @@ export const platform: NavMenuConstant = {
name: 'Access Control',
url: '/guides/platform/access-control' as `/${string}`,
},
{
name: 'Personal Access Tokens',
url: '/guides/platform/personal-access-tokens' as `/${string}`,
enabled: fullPlatformEnabled,
},
{
name: 'Multi-factor Authentication',
url: '/guides/platform/multi-factor-authentication',
@@ -0,0 +1,37 @@
{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}
| MCP tool | Required permission |
| --------------------------- | ----------------------------------------------------------------------------- |
| `apply_migration` | **Migrations** (Read-write) |
| `confirm_cost` | None (always available) |
| `create_branch` | **Development Branches** (Read-write) or **Production Branches** (Read-write) |
| `create_project` | **Organization Projects** (Read-write) |
| `delete_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
| `deploy_edge_function` | **Edge Functions** (Read-write) |
| `execute_sql` | **Database** (Read) |
| `generate_typescript_types` | **Database** (Read) |
| `get_advisors` | **Advisors** (Read) |
| `get_cost` | **Organization Settings** (Read) and **Projects (account-wide)** (Read) |
| `get_edge_function` | **Edge Functions** (Read) |
| `get_logs` | **Logs** (Read) |
| `get_organization` | **Organization Settings** (Read) |
| `get_project` | **Project Settings** (Read) |
| `get_project_url` | **Project Settings** (Read) |
| `get_publishable_keys` | **API Keys** (Read) |
| `get_storage_config` | **Storage Config** (Read) |
| `list_branches` | **Development Branches** (Read) or **Production Branches** (Read) |
| `list_edge_functions` | **Edge Functions** (Read) |
| `list_extensions` | **Database** (Read) |
| `list_migrations` | **Migrations** (Read) |
| `list_organizations` | **Organizations** (Read) |
| `list_projects` | **Projects (account-wide)** (Read) |
| `list_storage_buckets` | **Storage** (Read) |
| `list_tables` | **Database** (Read) |
| `merge_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
| `pause_project` | **Project Settings** (Read-write) |
| `query_logs` | **Logs** (Read) |
| `rebase_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
| `reset_branch` | **Production Branches** (Read-write) or **Development Branches** (Read-write) |
| `restore_project` | **Project Settings** (Read-write) |
| `search_docs` | None (always available) |
| `update_storage_config` | **Storage Config** (Read-write) |
@@ -0,0 +1,236 @@
{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}
| Permission | Access required | Management API endpoint |
| -------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Project** | | |
| Project Settings | Read | [Get JIT access config](/docs/reference/api/v1-get-jit-access-config) |
| | | [Get postgres upgrade eligibility](/docs/reference/api/v1-get-postgres-upgrade-eligibility)[^1] |
| | | [Get postgres upgrade status](/docs/reference/api/v1-get-postgres-upgrade-status)[^1] |
| | | [Get project](/docs/reference/api/v1-get-project) |
| | | [Get services health](/docs/reference/api/v1-get-services-health) |
| | | [List available restore versions](/docs/reference/api/v1-list-available-restore-versions) |
| | | [List private link associations](/docs/reference/api/v2-list-private-link-associations) |
| | | [Preview a project transfer](/docs/reference/api/v2-preview-a-project-transfer) |
| | Read-write | [Cancel a project restoration](/docs/reference/api/v1-cancel-a-project-restoration) |
| | | [Create private link association](/docs/reference/api/v2-create-private-link-association) |
| | | [Delete a project](/docs/reference/api/v1-delete-a-project) |
| | | [Delete private link association](/docs/reference/api/v2-delete-private-link-association) |
| | | [Delete private link association for database](/docs/reference/api/v2-delete-private-link-association-for-database) |
| | | [Get pgsodium config](/docs/reference/api/v1-get-pgsodium-config) |
| | | [OAuth authorize project claim](/docs/reference/api/v1-oauth-authorize-project-claim)[^2] |
| | | [Pause a project](/docs/reference/api/v1-pause-a-project) |
| | | [Restart a project](/docs/reference/api/v1-restart-a-project) |
| | | [Restore a project](/docs/reference/api/v1-restore-a-project) |
| | | [Update a project](/docs/reference/api/v1-update-a-project) |
| | | [Update auth service config](/docs/reference/api/v1-update-auth-service-config)[^3] |
| | | [Update JIT access config](/docs/reference/api/v1-update-jit-access-config) |
| | | [Update pgsodium config](/docs/reference/api/v1-update-pgsodium-config) |
| | | [Upgrade postgres version](/docs/reference/api/v1-upgrade-postgres-version)[^4] |
| Action Runs | Read | [Count action runs](/docs/reference/api/v1-count-action-runs) |
| | | [Get action run](/docs/reference/api/v1-get-action-run) |
| | | [Get action run logs](/docs/reference/api/v1-get-action-run-logs) |
| | | [List action runs](/docs/reference/api/v1-list-action-runs) |
| | Read-write | [Update action run status](/docs/reference/api/v1-update-action-run-status) |
| Advisors | Read | [Get performance advisors](/docs/reference/api/v1-get-performance-advisors) |
| | | [Get security advisors](/docs/reference/api/v1-get-security-advisors) |
| Analytics Config | Read | [List log drains](/docs/reference/api/v2-list-log-drains) |
| | Read-write | [Create log drain](/docs/reference/api/v2-create-log-drain) |
| | | [Delete log drain](/docs/reference/api/v2-delete-log-drain) |
| | | [Update log drain](/docs/reference/api/v2-update-log-drain) |
| Logs | Read | [Get project logs](/docs/reference/api/v1-get-project-logs) |
| | | [Get project logs all](/docs/reference/api/v1-get-project-logs-all) |
| | | [Scrape project metrics](/docs/reference/api/v1-scrape-project-metrics) |
| Usage Analytics | Read | [Get project function combined stats](/docs/reference/api/v1-get-project-function-combined-stats) |
| | | [Get project usage API count](/docs/reference/api/v1-get-project-usage-api-count) |
| | | [Get project usage request count](/docs/reference/api/v1-get-project-usage-request-count) |
| Platform Webhooks | Read | [Get delivery](/docs/reference/api/v2-projects-ref-webhooks-deliveries-id-get) |
| | | [Get endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-get) |
| | | [List deliveries](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-deliveries-get) |
| | | [List endpoints](/docs/reference/api/v2-projects-ref-webhooks-endpoints-get) |
| | Read-write | [Create endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-post) |
| | | [Delete all endpoints](/docs/reference/api/v2-projects-ref-webhooks-endpoints-delete) |
| | | [Delete endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-delete) |
| | | [Retry delivery](/docs/reference/api/v2-projects-ref-webhooks-deliveries-id-retry-post) |
| | | [Send test event](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-test-post) |
| | | [Update endpoint](/docs/reference/api/v2-projects-ref-webhooks-endpoints-id-patch) |
| **Database** | | |
| Backups | Read | [Get backup schedule](/docs/reference/api/v1-get-backup-schedule) |
| | | [List all backups](/docs/reference/api/v1-list-all-backups) |
| | Read-write | [Restore PITR backup](/docs/reference/api/v1-restore-pitr-backup) |
| | | [Update backup schedule](/docs/reference/api/v1-update-backup-schedule) |
| Database | Read | [Generate typescript types](/docs/reference/api/v1-generate-typescript-types) |
| | | [Get database metadata](/docs/reference/api/v1-get-database-metadata) |
| | | [Get database openapi](/docs/reference/api/v1-get-database-openapi) |
| | | [Get postgres upgrade eligibility](/docs/reference/api/v1-get-postgres-upgrade-eligibility)[^1] |
| | | [Get postgres upgrade status](/docs/reference/api/v1-get-postgres-upgrade-status)[^1] |
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | | [Get project PgBouncer config](/docs/reference/api/v1-get-project-pgbouncer-config) |
| | | [Read only query](/docs/reference/api/v1-read-only-query) |
| | | [Run a query](/docs/reference/api/v1-run-a-query) |
| | Read-write | [Create login role](/docs/reference/api/v1-create-login-role) |
| | | [Delete login roles](/docs/reference/api/v1-delete-login-roles) |
| | | [Run a query](/docs/reference/api/v1-run-a-query) |
| | | [Upgrade postgres version](/docs/reference/api/v1-upgrade-postgres-version)[^4] |
| Database Config | Read | [Get postgres config](/docs/reference/api/v1-get-postgres-config) |
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | Read-write | [Update database password](/docs/reference/api/v1-update-database-password) |
| | | [Update postgres config](/docs/reference/api/v1-update-postgres-config) |
| Database JIT | Read | [Authorize JIT access](/docs/reference/api/v1-authorize-jit-access) |
| | | [Get JIT access](/docs/reference/api/v1-get-jit-access) |
| | Read-write | [Delete invite external JIT access](/docs/reference/api/v1-delete-invite-external-jit-access) |
| | | [Delete JIT access](/docs/reference/api/v1-delete-jit-access) |
| | | [Invite external JIT access](/docs/reference/api/v1-invite-external-jit-access) |
| | | [List JIT access](/docs/reference/api/v1-list-jit-access) |
| | | [Update JIT access](/docs/reference/api/v1-update-jit-access) |
| Network Bans | Read | [List all network bans](/docs/reference/api/v1-list-all-network-bans) |
| | | [List all network bans enriched](/docs/reference/api/v1-list-all-network-bans-enriched) |
| | Read-write | [Delete network bans](/docs/reference/api/v1-delete-network-bans) |
| Network Restrictions | Read | [Get network restrictions](/docs/reference/api/v1-get-network-restrictions) |
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | Read-write | [Patch network restrictions](/docs/reference/api/v1-patch-network-restrictions) |
| | | [Update network restrictions](/docs/reference/api/v1-update-network-restrictions) |
| Migrations | Read | [Get a migration](/docs/reference/api/v1-get-a-migration) |
| | | [List migration history](/docs/reference/api/v1-list-migration-history) |
| | Read-write | [Apply a migration](/docs/reference/api/v1-apply-a-migration) |
| | | [Patch a migration](/docs/reference/api/v1-patch-a-migration) |
| | | [Rollback migrations](/docs/reference/api/v1-rollback-migrations) |
| | | [Upsert a migration](/docs/reference/api/v1-upsert-a-migration) |
| Connection Pooling | Read | [Get pooler config](/docs/reference/api/v1-get-pooler-config) |
| | Read-write | [Update pooler config](/docs/reference/api/v1-update-pooler-config) |
| Read-only Mode | Read | [Get read-only mode status](/docs/reference/api/v1-get-readonly-mode-status) |
| | Read-write | [Disable read-only mode temporarily](/docs/reference/api/v1-disable-readonly-mode-temporarily) |
| SSL Enforcement | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | | [Get SSL enforcement config](/docs/reference/api/v1-get-ssl-enforcement-config) |
| | Read-write | [Update SSL enforcement config](/docs/reference/api/v1-update-ssl-enforcement-config) |
| Database Webhooks | Read-write | [Enable database webhook](/docs/reference/api/v1-enable-database-webhook) |
| **Application services** | | |
| API Keys | Read | [Get project API key](/docs/reference/api/v1-get-project-api-key) |
| | | [Get project API keys](/docs/reference/api/v1-get-project-api-keys) |
| | | [Get project legacy API keys](/docs/reference/api/v1-get-project-legacy-api-keys) |
| | Read-write | [Create project API key](/docs/reference/api/v1-create-project-api-key) |
| | | [Delete project API key](/docs/reference/api/v1-delete-project-api-key) |
| | | [Update project API key](/docs/reference/api/v1-update-project-api-key) |
| | | [Update project legacy API keys](/docs/reference/api/v1-update-project-legacy-api-keys) |
| Auth Config | Read | [Get a SSO provider](/docs/reference/api/v1-get-a-sso-provider) |
| | | [Get auth service config](/docs/reference/api/v1-get-auth-service-config) |
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | | [Get project TPA integration](/docs/reference/api/v1-get-project-tpa-integration) |
| | | [List all SSO provider](/docs/reference/api/v1-list-all-sso-provider) |
| | | [List project TPA integrations](/docs/reference/api/v1-list-project-tpa-integrations) |
| | Read-write | [Create a SSO provider](/docs/reference/api/v1-create-a-sso-provider) |
| | | [Create project TPA integration](/docs/reference/api/v1-create-project-tpa-integration) |
| | | [Delete a SSO provider](/docs/reference/api/v1-delete-a-sso-provider) |
| | | [Delete project TPA integration](/docs/reference/api/v1-delete-project-tpa-integration) |
| | | [Update a SSO provider](/docs/reference/api/v1-update-a-sso-provider) |
| | | [Update auth service config](/docs/reference/api/v1-update-auth-service-config)[^3] |
| Auth Signing Keys | Read | [Get legacy signing key](/docs/reference/api/v1-get-legacy-signing-key) |
| | | [Get project signing key](/docs/reference/api/v1-get-project-signing-key) |
| | | [Get project signing keys](/docs/reference/api/v1-get-project-signing-keys) |
| | Read-write | [Create legacy signing key](/docs/reference/api/v1-create-legacy-signing-key) |
| | | [Create project signing key](/docs/reference/api/v1-create-project-signing-key) |
| | | [Remove project signing key](/docs/reference/api/v1-remove-project-signing-key) |
| | | [Update project signing key](/docs/reference/api/v1-update-project-signing-key) |
| Data API Config | Read | [Get PostgREST service config](/docs/reference/api/v1-get-postgrest-service-config) |
| | | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | Read-write | [Update PostgREST service config](/docs/reference/api/v1-update-postgrest-service-config) |
| Edge Functions | Read | [Get a function](/docs/reference/api/v1-get-a-function) |
| | | [Get a function body](/docs/reference/api/v1-get-a-function-body) |
| | | [List all functions](/docs/reference/api/v1-list-all-functions) |
| | Read-write | [Bulk update functions](/docs/reference/api/v1-bulk-update-functions) |
| | | [Create a function](/docs/reference/api/v1-create-a-function) |
| | | [Delete a function](/docs/reference/api/v1-delete-a-function) |
| | | [Deploy a function](/docs/reference/api/v1-deploy-a-function) |
| | | [Update a function](/docs/reference/api/v1-update-a-function) |
| Edge Function Secrets | Read | [List all secrets](/docs/reference/api/v1-list-all-secrets) |
| | Read-write | [Bulk create secrets](/docs/reference/api/v1-bulk-create-secrets) |
| | | [Bulk delete secrets](/docs/reference/api/v1-bulk-delete-secrets) |
| Realtime Config | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | | [Get realtime config](/docs/reference/api/v1-get-realtime-config) |
| | Read-write | [Shutdown realtime](/docs/reference/api/v1-shutdown-realtime) |
| | | [Update realtime config](/docs/reference/api/v1-update-realtime-config) |
| Storage | Read | [List all buckets](/docs/reference/api/v1-list-all-buckets) |
| Storage Config | Read | [Get project config](/docs/reference/api/v2-get-project-config)[^5] |
| | | [Get storage config](/docs/reference/api/v1-get-storage-config) |
| | Read-write | [Update storage config](/docs/reference/api/v1-update-storage-config) |
| **Infrastructure and delivery** | | |
| Development Branches | Read | [Get a branch](/docs/reference/api/v1-get-a-branch) |
| | | [Get a branch config](/docs/reference/api/v1-get-a-branch-config) |
| | | [List all branches](/docs/reference/api/v1-list-all-branches) |
| | Read-write | [Create a branch](/docs/reference/api/v1-create-a-branch) |
| | | [Delete a branch](/docs/reference/api/v1-delete-a-branch) |
| | | [Diff a branch](/docs/reference/api/v1-diff-a-branch) |
| | | [Merge a branch](/docs/reference/api/v1-merge-a-branch) |
| | | [Push a branch](/docs/reference/api/v1-push-a-branch) |
| | | [Reset a branch](/docs/reference/api/v1-reset-a-branch) |
| | | [Restore a branch](/docs/reference/api/v1-restore-a-branch) |
| | | [Update a branch config](/docs/reference/api/v1-update-a-branch-config) |
| Production Branches | Read | [Get a branch](/docs/reference/api/v1-get-a-branch) |
| | | [Get a branch config](/docs/reference/api/v1-get-a-branch-config) |
| | | [List all branches](/docs/reference/api/v1-list-all-branches) |
| | Read-write | [Create a branch](/docs/reference/api/v1-create-a-branch) |
| | | [Delete a branch](/docs/reference/api/v1-delete-a-branch) |
| | | [Diff a branch](/docs/reference/api/v1-diff-a-branch) |
| | | [Disable preview branching](/docs/reference/api/v1-disable-preview-branching) |
| | | [Merge a branch](/docs/reference/api/v1-merge-a-branch) |
| | | [Push a branch](/docs/reference/api/v1-push-a-branch) |
| | | [Reset a branch](/docs/reference/api/v1-reset-a-branch) |
| | | [Restore a branch](/docs/reference/api/v1-restore-a-branch) |
| | | [Update a branch config](/docs/reference/api/v1-update-a-branch-config) |
| Custom Domains | Read | [Get hostname config](/docs/reference/api/v1-get-hostname-config) |
| | Read-write | [Activate custom hostname](/docs/reference/api/v1-activate-custom-hostname) |
| | | Delete hostname config |
| | | [Update hostname config](/docs/reference/api/v1-update-hostname-config) |
| | | [Verify DNS config](/docs/reference/api/v1-verify-dns-config) |
| Add-ons | Read | [List project add-ons](/docs/reference/api/v1-list-project-addons) |
| | Read-write | [Apply project add-on](/docs/reference/api/v1-apply-project-addon) |
| | | [Remove project add-on](/docs/reference/api/v1-remove-project-addon) |
| Disk Config | Read | [Get database disk](/docs/reference/api/v1-get-database-disk) |
| | | [Get disk utilization](/docs/reference/api/v1-get-disk-utilization) |
| | | [Get project disk auto-scaling config](/docs/reference/api/v1-get-project-disk-autoscale-config) |
| | Read-write | [Modify database disk](/docs/reference/api/v1-modify-database-disk) |
| Read Replicas | Read-write | [Remove a read replica](/docs/reference/api/v1-remove-a-read-replica) |
| | | [Setup a read replica](/docs/reference/api/v1-setup-a-read-replica) |
| Vanity Subdomain | Read | [Get vanity subdomain config](/docs/reference/api/v1-get-vanity-subdomain-config) |
| | Read-write | [Activate vanity subdomain config](/docs/reference/api/v1-activate-vanity-subdomain-config) |
| | | [Check vanity subdomain availability](/docs/reference/api/v1-check-vanity-subdomain-availability) |
| | | [Deactivate vanity subdomain config](/docs/reference/api/v1-deactivate-vanity-subdomain-config) |
| **Account and organization** | | |
| Organizations | Read | [List all organizations](/docs/reference/api/v1-list-all-organizations) |
| | Read-write | [Create an organization](/docs/reference/api/v1-create-an-organization) |
| Projects (account-wide) | Read | [List all projects](/docs/reference/api/v1-list-all-projects) |
| SQL Snippets (account-wide) | Read | [Get a snippet](/docs/reference/api/v1-get-a-snippet) |
| | | [List all snippets](/docs/reference/api/v1-list-all-snippets) |
| Organization Settings | Read | [Get an organization](/docs/reference/api/v1-get-an-organization) |
| | | [Get organization entitlements](/docs/reference/api/v1-get-organization-entitlements) |
| | Read-write | [Assign organization member role](/docs/reference/api/v2-assign-organization-member-role) |
| | | [OAuth authorize project claim](/docs/reference/api/v1-oauth-authorize-project-claim)[^2] |
| | | [Transfer a project](/docs/reference/api/v2-transfer-a-project) |
| Organization Members | Read | [List organization members](/docs/reference/api/v1-list-organization-members) |
| | | [List organization members](/docs/reference/api/v2-list-organization-members) |
| | | [List organization roles](/docs/reference/api/v2-list-organization-roles) |
| | Read-write | [Create organization invitations](/docs/reference/api/v2-create-organization-invitations) |
| | | [Delete organization invitations](/docs/reference/api/v2-delete-organization-invitations) |
| Organization Projects | Read | [Get all projects for organization](/docs/reference/api/v1-get-all-projects-for-organization) |
| | | [List organization GitHub connections](/docs/reference/api/v2-list-organization-github-connections) |
| | | [List organization projects](/docs/reference/api/v2-list-organization-projects) |
| | Read-write | [Create a project](/docs/reference/api/v1-create-a-project) |
| Platform Webhooks (organization) | Read | [Get delivery](/docs/reference/api/v2-organizations-slug-webhooks-deliveries-id-get) |
| | | [Get endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-get) |
| | | [List deliveries](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-deliveries-get) |
| | | [List endpoints](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-get) |
| | Read-write | [Create endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-post) |
| | | [Delete all endpoints](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-delete) |
| | | [Delete endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-delete) |
| | | [Retry delivery](/docs/reference/api/v2-organizations-slug-webhooks-deliveries-id-retry-post) |
| | | [Send test event](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-test-post) |
| | | [Update endpoint](/docs/reference/api/v2-organizations-slug-webhooks-endpoints-id-patch) |
[^1]: Requires **Project Settings** (Read) and **Database** (Read).
[^2]: Requires **Organization Settings** (Read-write) and **Project Settings** (Read-write).
[^3]: Requires **Auth Config** (Read-write) and **Project Settings** (Read-write).
[^4]: Requires **Project Settings** (Read-write) and **Database** (Read-write).
[^5]: Requires **Database Config** (Read), **Database** (Read), **SSL Enforcement** (Read), **Network Restrictions** (Read), **Auth Config** (Read), **Data API Config** (Read), **Realtime Config** (Read), and **Storage Config** (Read).
@@ -0,0 +1,61 @@
---
title: 'Personal Access Tokens'
description: 'Scope personal access tokens to specific organizations, projects, and permissions'
---
<Admonition type="caution">
Scoped personal access tokens are in **public alpha** and rolling out gradually. If you don't see the option to choose permissions when creating a token, your account doesn't have access yet. File a [support ticket](https://supabase.help) to get early access.
</Admonition>
Personal access tokens (PATs) authenticate you to the [Management API](/docs/reference/api/introduction) and the tools built on it, like the Supabase CLI and the [MCP server](/docs/guides/ai-tools/mcp). They come in two flavors:
- **Classic tokens** carry your account's full access. That means every permission, on every organization and every project you belong to today, and on every one you create or join in the future. A classic token created a year ago can touch a project you created today.
- **Scoped tokens** carry only the organizations, projects, and permissions you choose. For example: read one project's database and view its logs, with no access to billing or organization settings.
<Admonition type="note">
We recommend scoped tokens for everything, especially AI agents, automation scripts, and CI environments. If a token leaks, the blast radius stays small.
</Admonition>
To use scoped personal access tokens you need a Supabase account with a role on the organization or project you want the token to reach. A scoped personal access token's permissions only ever narrow what your account can already do. They never grant more. If your role doesn't include a permission (see [Access Control](/docs/guides/platform/access-control)), granting that permission to a token has no effect: the token still can't do it.
You create scoped tokens the same way as classic tokens, from your [access tokens](/dashboard/account/tokens) settings. Choose which permissions to grant during creation instead of leaving the token with full access.
## Create and use a scoped personal access token
This example creates a token that can only read one project's settings, then calls an endpoint outside that scope. Both calls are reads available to every organization role, including Read-Only, so anyone can run it.
1. Go to your [access tokens](/dashboard/account/tokens) settings and generate a new token.
2. While creating it, scope the token to one project and grant only the **Project Settings** permission with **Read** access.
3. Copy the token (scoped personal access tokens start with `sbp_fc`) and use it against the Management API, replacing `your-project-ref` with the project's ref:
```bash
export SUPABASE_ACCESS_TOKEN="sbp_fc..."
# Allowed: Project Settings Read unlocks this endpoint
curl "https://api.supabase.com/v1/projects/your-project-ref" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
# Returns the project's details
# Denied: this endpoint needs the Database permission, which the token lacks
curl -i "https://api.supabase.com/v1/projects/your-project-ref/types/typescript" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"
# Returns HTTP 403
```
The tables below list which permission unlocks which endpoints, and which permission each [MCP tool](#mcp-tools) requires, so you can grant exactly what a workflow needs.
## Permission scopes
Each permission controls read or read-write access to one resource, for example **Database**, **Edge Functions**, or **Production Branches**. The permissions and names below match those shown when creating a scoped personal access token. Permissions that currently unlock neither a public Management API endpoint nor an MCP tool are omitted. A few endpoints need more than one permission. The footnotes call these out.
<$Partial path="access-control/scoped_pat_permissions.mdx" />
## MCP tools
A scoped personal access token used to authenticate the [MCP server](/docs/guides/ai-tools/mcp) can only call the tools its granted permissions unlock. See [Available tools](/docs/guides/ai-tools/mcp#available-tools) for what each tool does.
<$Partial path="access-control/scoped_pat_mcp_tools.mdx" />
+18 -3
View File
@@ -1,7 +1,7 @@
REPO_DIR=$(shell pwd)
GENERATOR_DIR=../../../packages/generator
.PHONY: run download download.api.v1 download.storage.v1 download.tsdoc.v2 download.server.v1 transform dereference.api.v1 dereference.auth.v1 dereference.storage.v0 generate generate.sections.api.v1 format
.PHONY: run download download.api.v1 download.mcp-tools-permissions download.storage.v1 download.tsdoc.v2 download.server.v1 transform dereference.api.v1 dereference.auth.v1 dereference.storage.v0 generate generate.sections.api.v1 generate.partials.access-control format
run: download transform generate format
@@ -11,12 +11,17 @@ run: download transform generate format
###############################################################################
# comment out download.auth.v1 temporarily, we're manually creating the file
# download: download.api.v1 download.auth.v1 download.storage.v1 download.tsdoc.v2
download: download.api.v1 download.storage.v1 download.tsdoc.v2 download.server.v1
download: download.api.v1 download.mcp-tools-permissions download.storage.v1 download.tsdoc.v2 download.server.v1
download.api.v1:
curl -sS https://api.supabase.com/api/v1-json > $(REPO_DIR)/api_v1_openapi.json
curl -sS https://api.supabase.com/api/v2-json > $(REPO_DIR)/api_v2_openapi.json
# Download tool permissions from the public Management API projection of its enforcement rules.
download.mcp-tools-permissions:
curl -sSf https://api.supabase.com/platform/mcp-tools-permissions > $(REPO_DIR)/mcp_tools_permissions.json
npx prettier --write $(REPO_DIR)/mcp_tools_permissions.json
# This flow needs to be updated, so we'l comment out for the moment
# Manual flow for now:
# — get swagger.json (https://supabase.github.io/gotrue/swagger.json)
@@ -81,7 +86,7 @@ dereference.analytics.v0:
###############################################################################
# Generate sections from OpenAPI 3.0
###############################################################################
generate: generate.sections.api.v1
generate: generate.sections.api.v1 generate.partials.access-control
generate.sections.api.v1:
npx tsx $(REPO_DIR)/sections/generateMgmtApiSections.cts \
@@ -89,6 +94,16 @@ generate.sections.api.v1:
$(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \
$(REPO_DIR)/common-api-sections.json
# Generate PAT guide tables from Management API inputs and Studio's shared permission catalog.
generate.partials.access-control:
npx tsx $(REPO_DIR)/sections/generateAccessControlPartials.mts \
$(REPO_DIR)/transforms/api_v1_openapi_deparsed.json \
$(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \
$(REPO_DIR)/mcp_tools_permissions.json \
$(REPO_DIR)/../content/_partials/access-control/scoped_pat_permissions.mdx \
$(REPO_DIR)/../content/_partials/access-control/scoped_pat_mcp_tools.mdx
npx prettier --write $(REPO_DIR)/../content/_partials/access-control/*.mdx
###############################################################################
# Validate OpenAPI 3.0
###############################################################################
+35
View File
@@ -0,0 +1,35 @@
{
"list_organizations": [["organizations_read"]],
"get_organization": [["organization_admin_read"]],
"list_projects": [["projects_read"]],
"get_project": [["project_admin_read"]],
"create_project": [["organization_projects_create"]],
"pause_project": [["project_admin_write"]],
"restore_project": [["project_admin_write"]],
"list_branches": [["branching_development_read"], ["branching_production_read"]],
"create_branch": [["branching_development_create"], ["branching_production_create"]],
"delete_branch": [["branching_production_delete"], ["branching_development_delete"]],
"merge_branch": [["branching_production_write"], ["branching_development_write"]],
"reset_branch": [["branching_production_write"], ["branching_development_write"]],
"rebase_branch": [["branching_production_write"], ["branching_development_write"]],
"execute_sql": [["database_read"]],
"list_tables": [["database_read"]],
"list_extensions": [["database_read"]],
"list_migrations": [["database_migrations_read"]],
"apply_migration": [["database_migrations_write"]],
"get_logs": [["analytics_logs_read"]],
"query_logs": [["analytics_logs_read"]],
"get_advisors": [["advisors_read"]],
"generate_typescript_types": [["database_read"]],
"get_project_url": [["project_admin_read"]],
"get_publishable_keys": [["api_gateway_keys_read"]],
"list_edge_functions": [["edge_functions_read"]],
"get_edge_function": [["edge_functions_read"]],
"deploy_edge_function": [["edge_functions_write"]],
"get_storage_config": [["storage_config_read"]],
"update_storage_config": [["storage_config_write"]],
"list_storage_buckets": [["storage_read"]],
"get_cost": [["organization_admin_read", "projects_read"]],
"confirm_cost": [[]],
"search_docs": [[]]
}
@@ -0,0 +1,334 @@
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
const require = createRequire(import.meta.url)
const { PERMISSION_CATALOG_BY_CATEGORY, PERMISSION_MODE_LABEL } =
require('shared-data/scoped-access-token-permissions') as typeof import('shared-data/scoped-access-token-permissions')
type ScopeGroupAlternatives = string[][]
type McpMap = Record<string, ScopeGroupAlternatives>
type Operation = {
operationId?: string
summary?: string
'x-fga-permissions'?: ScopeGroupAlternatives
'x-internal'?: boolean
}
type Endpoint = {
operationId: string
label: string
groups: ScopeGroupAlternatives
}
type PermissionRow = {
resource: string
access: string
category: string
scopes: string[]
}
const GENERATED_NOTICE =
'{/* Generated by `make -C apps/docs/spec generate.partials.access-control`. Do not hand-edit; see supabase/platform#37175 and apps/docs/spec/Makefile. */}\n'
const WORD_FIXES: Record<string, string> = {
api: 'API',
sso: 'SSO',
tpa: 'TPA',
pitr: 'PITR',
dns: 'DNS',
jit: 'JIT',
ssl: 'SSL',
oauth: 'OAuth',
github: 'GitHub',
postgrest: 'PostgREST',
pgbouncer: 'PgBouncer',
readonly: 'read-only',
addon: 'add-on',
addons: 'add-ons',
autoscale: 'auto-scaling',
}
const readJson = (filePath: string) => JSON.parse(fs.readFileSync(filePath, 'utf8'))
function endpointLabel(operationId: string) {
const words = operationId
.replace(/^v\d+-?/, '')
.split('-')
.filter(Boolean)
.map((word) => WORD_FIXES[word] ?? word)
.join(' ')
return words.charAt(0).toUpperCase() + words.slice(1)
}
const permissionRows: PermissionRow[] = PERMISSION_CATALOG_BY_CATEGORY.flatMap((category) =>
category.entries.flatMap((entry) => [
...(entry.readScopes.length > 0
? [
{
resource: entry.name,
access: PERMISSION_MODE_LABEL.read,
category: category.name,
scopes: entry.readScopes,
},
]
: []),
...(entry.writeScopes.length > 0
? [
{
resource: entry.name,
access: PERMISSION_MODE_LABEL.readwrite,
category: category.name,
scopes: entry.writeScopes,
},
]
: []),
])
)
const rowByScope = new Map(permissionRows.flatMap((row) => row.scopes.map((scope) => [scope, row])))
// Workers permissions are present in the API spec but are not live for scoped PATs yet.
const EXCLUDED_SCOPES = new Set(['workers_read', 'workers_write'])
// The public v2 webhook operations currently omit x-fga-permissions from the OpenAPI projection.
// Keep this fallback narrow so the generated table can still link those endpoints, and fail below
// if any other public operation has not been classified for the scoped-PAT table.
const WEBHOOK_PERMISSION_SCOPES = [
{
routePrefix: '/v2/projects/{ref}/webhooks/',
read: 'platform_webhooks_projects_read',
write: 'platform_webhooks_projects_write',
},
{
routePrefix: '/v2/organizations/{slug}/webhooks/',
read: 'platform_webhooks_organization_read',
write: 'platform_webhooks_organization_write',
},
]
// These public operations sit outside the scoped-PAT permission table.
const OPERATIONS_OUTSIDE_SCOPED_PAT_TABLE = new Set([
'v1-accept-invite-external-jit-access',
'v1-authorize-user',
'v1-exchange-oauth-token',
'v1-get-available-regions',
'v1-get-profile',
'v1-revoke-token',
])
function webhookPermissionGroups(
route: string,
method: string
): ScopeGroupAlternatives | undefined {
const scopes = WEBHOOK_PERMISSION_SCOPES.find(({ routePrefix }) => route.startsWith(routePrefix))
if (!scopes) return undefined
const access = ['get', 'head'].includes(method)
? 'read'
: ['post', 'put', 'patch', 'delete'].includes(method)
? 'write'
: undefined
if (!access) return undefined
return [[scopes[access]]]
}
function knownGroups(groups: ScopeGroupAlternatives, missing: Set<string>) {
return groups.filter((group) => {
if (group.some((scope) => EXCLUDED_SCOPES.has(scope))) return false
const unknown = group.filter((scope) => !rowByScope.has(scope))
unknown.forEach((scope) => missing.add(scope))
return unknown.length === 0
})
}
function joinList(items: string[]) {
if (items.length < 2) return items[0] ?? ''
if (items.length === 2) return `${items[0]} and ${items[1]}`
return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`
}
function formatRequirement(groups: ScopeGroupAlternatives) {
const alternatives = Array.from(
new Set(
groups.map((group) =>
joinList(
Array.from(
new Set(
group.map((scope) => {
const row = rowByScope.get(scope)!
return `**${row.resource}** (${row.access})`
})
)
)
)
)
)
)
return alternatives.join(alternatives.some((item) => item.includes(' and ')) ? ', or ' : ' or ')
}
function missingScopesNotice(missing: Set<string>) {
if (missing.size === 0) return []
return [
'',
`{/* Not documented, missing from the shared permission catalog ` +
`(packages/shared-data/scoped-access-token-permissions.ts): ${[...missing].sort().join(', ')} */}`,
]
}
function collectEndpoints(specPaths: string[]) {
const endpoints = new Map<string, Endpoint>()
const unclassifiedOperations: string[] = []
for (const specPath of specPaths) {
const spec = readJson(specPath)
for (const [route, methods] of Object.entries<Record<string, Operation>>(spec.paths ?? {})) {
for (const [method, operation] of Object.entries(methods)) {
if (!operation?.operationId || operation['x-internal']) continue
const key = `${method.toUpperCase()} ${route}`
const fallbackGroups = webhookPermissionGroups(route, method)
const groups = operation['x-fga-permissions'] ?? fallbackGroups ?? []
if (groups.length === 0) {
if (!OPERATIONS_OUTSIDE_SCOPED_PAT_TABLE.has(operation.operationId)) {
unclassifiedOperations.push(`${key} (${operation.operationId})`)
}
continue
}
endpoints.set(key, {
operationId: operation.operationId,
label: fallbackGroups
? (operation.summary ?? endpointLabel(operation.operationId))
: endpointLabel(operation.operationId),
groups,
})
}
}
}
if (unclassifiedOperations.length > 0) {
throw new Error(
`Public Management API operations are not classified for the scoped-PAT table:\n${unclassifiedOperations.join('\n')}`
)
}
return [...endpoints.values()]
}
function generatePermissionsPartial(specPaths: string[], tools: McpMap, outputPath: string) {
const missing = new Set<string>()
const endpoints = collectEndpoints(specPaths).map((endpoint) => ({
...endpoint,
groups: knownGroups(endpoint.groups, missing),
}))
const mcpToolScopes = new Set(
Object.values(tools).flatMap((groups) => knownGroups(groups, missing).flat())
)
const footnotes = new Map<string, string>()
const lines = [
GENERATED_NOTICE,
'| Permission | Access required | Management API endpoint |',
'| ---------- | --------------- | ----------------------- |',
]
let previousCategory = ''
let previousResource = ''
for (const row of permissionRows) {
const rowScopes = new Set(row.scopes)
const rowEndpoints = endpoints
.filter((endpoint) =>
endpoint.groups.some((group) => group.some((scope) => rowScopes.has(scope)))
)
.sort((a, b) => a.label.localeCompare(b.label) || a.operationId.localeCompare(b.operationId))
if (rowEndpoints.length === 0 && !row.scopes.some((scope) => mcpToolScopes.has(scope))) continue
if (row.category !== previousCategory) {
lines.push(`| **${row.category}** | | |`)
previousCategory = row.category
previousResource = ''
}
const permissionCell = row.resource === previousResource ? '' : row.resource
previousResource = row.resource
if (rowEndpoints.length === 0) {
lines.push(`| ${permissionCell} | ${row.access} | No public Management API endpoints |`)
continue
}
rowEndpoints.forEach((endpoint, index) => {
const label = endpoint.label.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\s+/g, ' ')
const link = /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(endpoint.operationId)
? `[${label}](/docs/reference/api/${endpoint.operationId})`
: label
const unlocksAlone = endpoint.groups.some((group) =>
group.every((scope) => rowScopes.has(scope))
)
let requirement = ''
if (!unlocksAlone) {
const text = `Requires ${formatRequirement(endpoint.groups)}.`
const id = footnotes.get(text) ?? String(footnotes.size + 1)
footnotes.set(text, id)
requirement = `[^${id}]`
}
lines.push(
`| ${index === 0 ? permissionCell : ''} | ${index === 0 ? row.access : ''} | ${link}${requirement} |`
)
})
}
const definitions = Array.from(footnotes, ([text, id]) => `[^${id}]: ${text}`)
writeOutput(outputPath, [
...lines,
...(definitions.length > 0 ? ['', ...definitions] : []),
...missingScopesNotice(missing),
'',
])
}
function generateMcpToolsPartial(tools: McpMap, outputPath: string) {
const missing = new Set<string>()
const rows = Object.entries(tools)
.sort(([a], [b]) => a.localeCompare(b))
.map(([tool, groups]) => {
const publishable = knownGroups(groups, missing)
const requirement = publishable.some((group) => group.length === 0)
? 'None (always available)'
: publishable.length === 0
? 'Not available to scoped personal access tokens'
: formatRequirement(publishable)
return `| \`${tool}\` | ${requirement} |`
})
writeOutput(outputPath, [
GENERATED_NOTICE,
'| MCP tool | Required permission |',
'| -------- | ------------------- |',
...rows,
...missingScopesNotice(missing),
'',
])
}
function writeOutput(outputPath: string, lines: string[]) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
fs.writeFileSync(outputPath, lines.join('\n'), 'utf8')
console.log(`Wrote ${outputPath}`)
}
const args = process.argv.slice(2).map((arg) => path.resolve(arg))
if (args.length !== 5) {
console.error(
'Usage: generateAccessControlPartials.mts <api-v1.json> <api-v2.json> ' +
'<mcp-tools.json> <permissions-output.mdx> <mcp-tools-output.mdx>'
)
process.exit(1)
}
const tools: McpMap = readJson(args[2])
generatePermissionsPartial(args.slice(0, 2), tools, args[3])
generateMcpToolsPartial(tools, args[4])
+1
View File
@@ -37,6 +37,7 @@ Studio is migrating from the Next.js pages router (`pages/**`) to TanStack Start
- **Telemetry** — `useTrack()` from `lib/telemetry/track`; event types live in `packages/common/telemetry-constants.ts`.
- **Tests** — default to including relevant tests with any change: a couple of unit tests for extracted logic, component tests for UI behavior, E2E only when the scope demands it (`studio-testing` has the decision tree). Not every PR needs them, but "no tests" should be a considered choice, not the default. Tooling: vitest + MSW; component tests use `customRender` + `addAPIMock` from `tests/lib/`; unhandled network requests fail tests. Don't `vi.mock('@/data/...')`.
- **Shortcuts** — use the registry in `state/shortcuts/` and `components/ui/Shortcut*.tsx`; keep `G then …` chords for navigation; no one-off keyboard listeners.
- **Scoped PAT catalog** — `packages/shared-data/scoped-access-token-permissions.ts` feeds Studio and the generated Personal Access Tokens guide. After changing it, run `make -C apps/docs/spec generate.partials.access-control`; Docs Tests rejects stale tables.
- **Reuse first** — before writing a new hook or helper, search for an existing one (`hooks/`, `lib/`, `packages/common`, `packages/ui-patterns`). If you do need a new one, make it as reusable as possible: general naming, no page-specific coupling, placed where other callers can find it.
- Co-locate sub-components with their parent; avoid barrel re-export files.
@@ -1,5 +1,11 @@
import { permissions } from '@supabase/shared-types'
import { components } from 'api-types'
import {
getAction,
getResource,
PERMISSION_CATALOG,
type FgaAction,
} from 'shared-data/scoped-access-token-permissions'
export type ScopedAccessTokenPermission =
components['schemas']['CreateScopedAccessTokenBody']['permissions'][number]
@@ -26,25 +32,12 @@ export const EXPIRES_AT_OPTIONS = {
const FGA = permissions.FgaPermissions
const getAction = (key: string): string => {
if (key.endsWith('_READ')) return 'read'
if (key.endsWith('_WRITE')) return 'write'
if (key.endsWith('_CREATE')) return 'create'
if (key.endsWith('_DELETE')) return 'delete'
return 'read'
}
const getResource = (key: string): string => {
return key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase()
}
const buildPermissionList = () => {
const list: Array<{
scope: string
resource: string
action: string
action: FgaAction
id: string
title: string
}> = []
for (const [scope, scopePerms] of Object.entries(FGA)) {
@@ -54,7 +47,6 @@ const buildPermissionList = () => {
resource: getResource(key),
action: getAction(key),
id: perm.id,
title: perm.title,
})
}
}
@@ -64,20 +56,12 @@ const buildPermissionList = () => {
export const PERMISSION_LIST = buildPermissionList()
export const ACCESS_TOKEN_RESOURCES = (() => {
const resourceMap = new Map<string, { resource: string; title: string; actions: string[] }>()
for (const p of PERMISSION_LIST) {
const key = `${p.scope}:${p.resource}`
if (!resourceMap.has(key)) {
const cleanTitle = p.title.replace(/^(Read|Manage|Create|Delete)\s+/i, '')
resourceMap.set(key, { resource: key, title: cleanTitle, actions: [] })
}
const entry = resourceMap.get(key)!
if (!entry.actions.includes(p.action)) {
entry.actions.push(p.action)
}
}
return Array.from(resourceMap.values())
})()
/**
* Resources shown in token permission summaries (e.g. the post-creation banner).
* Titles come from the shared permission catalog so they match the creation form
* and the generated docs tables.
*/
export const ACCESS_TOKEN_RESOURCES = PERMISSION_CATALOG.map((entry) => ({
resource: entry.key,
title: entry.name,
}))
@@ -1,563 +1,32 @@
import { permissions } from '@supabase/shared-types'
import {
getCatalogEntry,
PERMISSION_CATALOG,
PERMISSION_CATALOG_BY_CATEGORY,
PERMISSION_MODE_LABEL,
type PermissionCatalogEntry,
type PermissionCategoryKey,
type PermissionMode,
type RiskLevel,
} from 'shared-data/scoped-access-token-permissions'
import type { ScopedAccessTokenPermission } from './AccessToken.constants'
export {
getCatalogEntry,
PERMISSION_CATALOG,
PERMISSION_CATALOG_BY_CATEGORY,
PERMISSION_MODE_LABEL,
}
export type { PermissionCatalogEntry, PermissionMode, RiskLevel }
/**
* Data model for the scoped access-token creation flow.
* Selection model for the scoped personal access token creation flow: none/read/readwrite modes per
* catalog entry, and the conversions between a selection and concrete FGA scope ids.
*
* The real permission scopes come from `@supabase/shared-types` (`FgaPermissions`). Those scopes
* carry no category or risk metadata, so this file layers editable presentation data on top:
* - PERMISSION_CATEGORIES groups every scope into one of five UI categories.
* - RESOURCE_METADATA assigns each resource a display name, description, category and risk.
*
* TODO(product): the risk levels, reasons and "Allows" copy below are proposed defaults — review
* and adjust. Where a resource has no explicit metadata entry we fall back to a heuristic.
* The catalog itself (resources, categories, risk metadata, scopes) lives in
* shared-data/scoped-access-token-permissions.ts so the docs generator can consume it too.
*/
const FGA = permissions.FgaPermissions
export type PermissionMode = 'none' | 'read' | 'readwrite'
export type RiskLevel = 'low' | 'medium' | 'high'
export type PermissionCategoryKey = 'account' | 'project' | 'database' | 'appsvc' | 'infra'
export interface PermissionCategory {
key: PermissionCategoryKey
name: string
description: string
}
/** Display order matches the accordion, where every category starts collapsed. */
export const PERMISSION_CATEGORIES: PermissionCategory[] = [
{
key: 'project',
name: 'Project',
description: 'Core project visibility, settings, and diagnostics.',
},
{
key: 'database',
name: 'Database',
description: 'SQL access, migrations, backups, and data operations.',
},
{
key: 'appsvc',
name: 'Application Services',
description: 'Auth, storage, realtime, edge functions, and service configuration.',
},
{
key: 'infra',
name: 'Infrastructure & Delivery',
description: 'Branch automation, domains, add-ons, and network.',
},
{
key: 'account',
name: 'Account & Organization',
description: 'Account-wide and organization-level access that spans projects.',
},
]
interface ResourceMeta {
category: PermissionCategoryKey
name: string
description: string
risk: RiskLevel
riskReason: string
allowsRead?: string[]
allowsWrite?: string[]
}
/**
* Per-resource presentation metadata, keyed by the derived `scope:resource` key (see
* AccessToken.constants → ACCESS_TOKEN_RESOURCES). Every resource returned from FgaPermissions
* should have an entry; RESOURCE_METADATA_FALLBACK covers anything that slips through.
*/
const RESOURCE_METADATA: Record<string, ResourceMeta> = {
// --- Account & Organization ---
'user:organizations': {
category: 'account',
name: 'Organizations',
description: 'Organizations you belong to.',
risk: 'medium',
riskReason: 'Read-write can create new organizations under your account.',
allowsRead: ['List your organizations'],
allowsWrite: ['Create organizations'],
},
'user:projects': {
category: 'account',
name: 'Projects (account-wide)',
description: 'Projects across all your organizations.',
risk: 'low',
riskReason: 'Read-only listing of the projects you can access.',
allowsRead: ['List your projects'],
},
'user:snippets': {
category: 'account',
name: 'SQL Snippets (account-wide)',
description: 'Saved SQL snippets across your account.',
risk: 'low',
riskReason: 'Read-only access to your saved snippets.',
allowsRead: ['Read your SQL snippets'],
},
'organization:admin': {
category: 'account',
name: 'Organization Settings',
description: 'Organization settings and project transfers.',
risk: 'high',
riskReason: 'Read-write grants elevated access to organization settings and project transfers.',
allowsRead: ['Read organization settings'],
allowsWrite: ['Manage organization settings', 'Transfer projects'],
},
'organization:members': {
category: 'account',
name: 'Organization Members',
description: 'Members and roles within the organization.',
risk: 'high',
riskReason: 'Read-write can add or remove members and change roles across your organization.',
allowsRead: ['Read organization members'],
allowsWrite: ['Add or remove members', 'Change member roles'],
},
'organization:projects': {
category: 'account',
name: 'Organization Projects',
description: 'Projects within the organization.',
risk: 'medium',
riskReason: 'Read-write can create new projects in the organization.',
allowsRead: ['List organization projects'],
allowsWrite: ['Create organization projects'],
},
// --- Project ---
'project:admin': {
category: 'project',
name: 'Project Settings',
description: 'Project metadata and settings.',
risk: 'high',
riskReason: 'Read-write grants elevated access to change project settings and configuration.',
allowsRead: ['Read project metadata'],
allowsWrite: ['Update project settings'],
},
'project:action_runs': {
category: 'project',
name: 'Action Runs',
description: 'Project action run status and logs.',
risk: 'medium',
riskReason: 'Read-write can trigger action runs that execute project workflows.',
allowsRead: ['Read action run status', 'Read run logs'],
allowsWrite: ['Trigger action runs'],
},
'project:advisors': {
category: 'project',
name: 'Advisors',
description: 'Security and performance advisor results.',
risk: 'low',
riskReason: 'Read-only access to advisor findings — no changes possible.',
allowsRead: ['Read security advisors', 'Read performance advisors'],
},
'project:analytics_logs': {
category: 'project',
name: 'Logs',
description: 'Operational logs and log analytics.',
risk: 'low',
riskReason: 'Read-only access to project logs.',
allowsRead: ['Read project logs'],
},
'project:analytics_usage': {
category: 'project',
name: 'Usage Analytics',
description: 'Project usage and analytics data.',
risk: 'low',
riskReason: 'Read-only access to usage analytics.',
allowsRead: ['Read usage analytics'],
},
'project:snippets': {
category: 'project',
name: 'SQL Snippets',
description: 'Saved SQL snippets for the project.',
risk: 'low',
riskReason: 'Read-write can create and edit saved SQL snippets.',
allowsRead: ['Read project SQL snippets'],
allowsWrite: ['Manage project SQL snippets'],
},
// --- Database ---
'project:database': {
category: 'database',
name: 'Database',
description: 'Database access and data operations.',
risk: 'high',
riskReason:
'Read-write lets this token run arbitrary SQL, so it can modify or delete any data in your database.',
allowsRead: ['Read tables and schema', 'Run read-only queries'],
allowsWrite: ['Run arbitrary SQL'],
},
'project:database_migrations': {
category: 'database',
name: 'Migrations',
description: 'Database migration history and application.',
risk: 'high',
riskReason:
'Read-write can apply schema changes that alter or drop tables across your database.',
allowsRead: ['Read migration history'],
allowsWrite: ['Apply migrations'],
},
'project:backups': {
category: 'database',
name: 'Backups',
description: 'Database backups, restore points, and restore.',
risk: 'high',
riskReason:
'Read-write can trigger restores that overwrite current data with an earlier snapshot.',
allowsRead: ['Read backups and restore points'],
allowsWrite: ['Trigger restores'],
},
'project:database_config': {
category: 'database',
name: 'Database Config',
description: 'Database configuration.',
risk: 'medium',
riskReason: 'Read-write can change database configuration.',
allowsRead: ['Read database configuration'],
allowsWrite: ['Update database configuration'],
},
'project:database_jit': {
category: 'database',
name: 'Database JIT',
description: 'Just-in-time database access settings.',
risk: 'medium',
riskReason: 'Read-write can change just-in-time database access settings.',
allowsRead: ['Read JIT settings'],
allowsWrite: ['Manage JIT settings'],
},
'project:database_pooling_config': {
category: 'database',
name: 'Connection Pooling',
description: 'Database connection pooling.',
risk: 'medium',
riskReason: 'Read-write can change connection pooling behavior.',
allowsRead: ['Read pooling configuration'],
allowsWrite: ['Update pooling configuration'],
},
'project:database_readonly_config': {
category: 'database',
name: 'Read-only Mode',
description: 'Database read-only mode.',
risk: 'medium',
riskReason: 'Read-write can toggle the database into or out of read-only mode.',
allowsRead: ['Read read-only mode status'],
allowsWrite: ['Toggle read-only mode'],
},
'project:database_ssl_config': {
category: 'database',
name: 'SSL Enforcement',
description: 'Database SSL configuration.',
risk: 'medium',
riskReason: 'Read-write can change SSL enforcement for database connections.',
allowsRead: ['Read SSL configuration'],
allowsWrite: ['Manage SSL enforcement'],
},
'project:database_webhooks_config': {
category: 'database',
name: 'Database Webhooks',
description: 'Webhooks triggered from the database.',
risk: 'medium',
riskReason: 'Read-write can change database webhook configuration.',
allowsRead: ['Read webhook configuration'],
allowsWrite: ['Manage database webhooks'],
},
'project:database_network_bans': {
category: 'database',
name: 'Network Bans',
description: 'Banned IPs for the database.',
risk: 'medium',
riskReason: 'Read-write can ban or unban IP addresses from reaching the database.',
allowsRead: ['Read banned IPs'],
allowsWrite: ['Manage banned IPs'],
},
'project:database_network_restrictions': {
category: 'database',
name: 'Network Restrictions',
description: 'Network restrictions for the database.',
risk: 'high',
riskReason: 'Read-write can change which networks are allowed to reach the database.',
allowsRead: ['Read network restrictions'],
allowsWrite: ['Manage network restrictions'],
},
// --- Application Services ---
'project:auth_config': {
category: 'appsvc',
name: 'Auth Config',
description: 'Authentication provider and settings.',
risk: 'high',
riskReason:
'Read-write can change authentication providers and settings, affecting how users sign in.',
allowsRead: ['Read auth configuration'],
allowsWrite: ['Update auth providers and settings'],
},
'project:auth_signing_keys': {
category: 'appsvc',
name: 'Auth Signing Keys',
description: 'Authentication signing keys.',
risk: 'high',
riskReason: 'Read-write can rotate signing keys, invalidating existing sessions and tokens.',
allowsRead: ['Read signing keys'],
allowsWrite: ['Manage signing keys'],
},
'project:api_gateway_keys': {
category: 'appsvc',
name: 'API Keys',
description: 'Project API keys.',
risk: 'high',
riskReason: 'Read exposes API keys; read-write grants elevated access to create new keys.',
allowsRead: ['Read project API keys'],
allowsWrite: ['Create and revoke API keys'],
},
'project:edge_functions': {
category: 'appsvc',
name: 'Edge Functions',
description: 'Edge functions.',
risk: 'medium',
riskReason: 'Read-write can deploy or delete edge functions.',
allowsRead: ['Read edge functions'],
allowsWrite: ['Deploy and delete edge functions'],
},
'project:edge_functions_secrets': {
category: 'appsvc',
name: 'Edge Function Secrets',
description: 'Secrets available to edge functions.',
risk: 'high',
riskReason: 'Read exposes function secrets; read-write can set new secret values.',
allowsRead: ['Read edge function secrets'],
allowsWrite: ['Set edge function secrets'],
},
'project:realtime_config': {
category: 'appsvc',
name: 'Realtime Config',
description: 'Realtime configuration.',
risk: 'medium',
riskReason: 'Read-write can change realtime settings and shut down active connections.',
allowsRead: ['Read realtime configuration'],
allowsWrite: ['Update realtime settings'],
},
'project:storage': {
category: 'appsvc',
name: 'Storage',
description: 'File storage buckets and objects.',
risk: 'medium',
riskReason: 'Read-write can modify or delete stored files.',
allowsRead: ['Read storage buckets and objects'],
allowsWrite: ['Manage storage buckets and objects'],
},
'project:storage_config': {
category: 'appsvc',
name: 'Storage Config',
description: 'Storage bucket configuration.',
risk: 'medium',
riskReason: 'Read-write can change storage configuration.',
allowsRead: ['Read storage configuration'],
allowsWrite: ['Update storage configuration'],
},
'project:data_api_config': {
category: 'appsvc',
name: 'Data API Config',
description: 'PostgREST behavior and settings.',
risk: 'medium',
riskReason: 'Read-write can change how the auto-generated Data API behaves.',
allowsRead: ['Read Data API configuration'],
allowsWrite: ['Update Data API configuration'],
},
// --- Infrastructure & Delivery ---
'project:branching_development': {
category: 'infra',
name: 'Development Branches',
description: 'Development branch automation.',
risk: 'low',
riskReason: 'Branch automation for development workflows — limited blast radius.',
allowsRead: ['Read development branches'],
allowsWrite: ['Create, update, and delete development branches'],
},
'project:branching_production': {
category: 'infra',
name: 'Production Branches',
description: 'Production branch automation.',
risk: 'high',
riskReason:
'Read-write grants elevated access to create, merge, or delete production branches.',
allowsRead: ['Read production branches'],
allowsWrite: ['Create, merge, and delete production branches'],
},
'project:custom_domain': {
category: 'infra',
name: 'Custom Domains',
description: 'Custom hostnames.',
risk: 'medium',
riskReason: 'Read-write can change custom hostnames, affecting how your project is reached.',
allowsRead: ['Read custom domain configuration'],
allowsWrite: ['Set custom hostnames'],
},
'project:vanity_subdomain': {
category: 'infra',
name: 'Vanity Subdomain',
description: 'Project vanity subdomain.',
risk: 'medium',
riskReason: 'Read-write can change the project vanity subdomain.',
allowsRead: ['Read vanity subdomain'],
allowsWrite: ['Manage vanity subdomain'],
},
'project:infra_addons': {
category: 'infra',
name: 'Add-ons',
description: 'Infrastructure add-ons.',
risk: 'medium',
riskReason: 'Read-write can enable or change paid infrastructure add-ons.',
allowsRead: ['Read infrastructure add-ons'],
allowsWrite: ['Manage infrastructure add-ons'],
},
'project:infra_disk_config': {
category: 'infra',
name: 'Disk Config',
description: 'Disk configuration.',
risk: 'medium',
riskReason: 'Read-write can change disk size and configuration, which may incur cost.',
allowsRead: ['Read disk configuration'],
allowsWrite: ['Manage disk configuration'],
},
'project:read_replicas': {
category: 'infra',
name: 'Read Replicas',
description: 'Read replica configuration.',
risk: 'medium',
riskReason: 'Read-write can provision or remove read replicas, which may incur cost.',
allowsRead: ['Read read-replica configuration'],
allowsWrite: ['Manage read replicas'],
},
}
const RESOURCE_METADATA_FALLBACK = (
resourceKey: string,
title: string,
hasWrite: boolean
): ResourceMeta => ({
category: resourceKey.startsWith('project:') ? 'project' : 'account',
name: title.replace(/^(Read|Manage|Create|Delete)\s+/i, ''),
description: title,
risk: hasWrite ? 'medium' : 'low',
riskReason: hasWrite
? 'Read-write can modify this resource.'
: 'Read-only access to this resource.',
})
const PERMISSION_LEVELS = ['user', 'organization', 'project'] as const
export type PermissionLevel = (typeof PERMISSION_LEVELS)[number]
/**
* Runtime guard for the FGA namespaces: role evaluation branches on the level, so an unrecognized
* namespace must fail loudly (at module load, caught by any test importing the catalog) rather
* than silently evaluate as project-level.
*/
const toPermissionLevel = (scope: string): PermissionLevel => {
const level = scope.toLowerCase()
const match = PERMISSION_LEVELS.find((candidate) => candidate === level)
if (match === undefined) throw new Error(`Unknown FGA namespace: ${scope}`)
return match
}
export interface PermissionCatalogEntry {
/** Derived resource key, e.g. "project:database" */
key: string
/** Which FGA namespace the resource lives in — decides which role (org vs project) governs it. */
level: PermissionLevel
category: PermissionCategoryKey
name: string
description: string
risk: RiskLevel
riskReason: string
allowsRead: string[]
allowsWrite: string[]
/** Whether a Read-write mode is offered (false => read-only resource). */
writable: boolean
/** FGA scope ids granted at Read (and above). */
readScopes: ScopedAccessTokenPermission[]
/** Additional FGA scope ids granted at Read-write (write / create / delete). */
writeScopes: ScopedAccessTokenPermission[]
}
const getAction = (key: string): 'read' | 'write' | 'create' | 'delete' => {
if (key.endsWith('_WRITE')) return 'write'
if (key.endsWith('_CREATE')) return 'create'
if (key.endsWith('_DELETE')) return 'delete'
return 'read'
}
const getResource = (key: string): string =>
key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase()
/**
* Builds the permission catalog from the real FgaPermissions. Each unique `scope:resource` becomes
* one row; its read scope maps to Read mode and its write/create/delete scopes to Read-write mode.
*/
const buildCatalog = (): PermissionCatalogEntry[] => {
const byResource = new Map<
string,
{ level: PermissionLevel; title: string; readScopes: string[]; writeScopes: string[] }
>()
for (const [scope, scopePerms] of Object.entries(FGA)) {
const level = toPermissionLevel(scope)
for (const [permKey, perm] of Object.entries(scopePerms)) {
const resourceKey = `${level}:${getResource(permKey)}`
const action = getAction(permKey)
if (!byResource.has(resourceKey)) {
byResource.set(resourceKey, { level, title: perm.title, readScopes: [], writeScopes: [] })
}
const entry = byResource.get(resourceKey)!
if (action === 'read') entry.readScopes.push(perm.id)
else entry.writeScopes.push(perm.id)
}
}
const catalog: PermissionCatalogEntry[] = []
for (const [key, { level, title, readScopes, writeScopes }] of byResource.entries()) {
const meta =
RESOURCE_METADATA[key] ?? RESOURCE_METADATA_FALLBACK(key, title, writeScopes.length > 0)
catalog.push({
key,
level,
category: meta.category,
name: meta.name,
description: meta.description,
risk: meta.risk,
riskReason: meta.riskReason,
allowsRead: meta.allowsRead ?? [`Read ${meta.name.toLowerCase()}`],
allowsWrite:
meta.allowsWrite ?? (writeScopes.length > 0 ? [`Modify ${meta.name.toLowerCase()}`] : []),
writable: writeScopes.length > 0,
readScopes: readScopes as ScopedAccessTokenPermission[],
writeScopes: writeScopes as ScopedAccessTokenPermission[],
})
}
return catalog
}
export const PERMISSION_CATALOG = buildCatalog()
const CATALOG_BY_KEY = new Map(PERMISSION_CATALOG.map((entry) => [entry.key, entry]))
export const getCatalogEntry = (key: string) => CATALOG_BY_KEY.get(key)
export interface CategoryWithEntries extends PermissionCategory {
entries: PermissionCatalogEntry[]
}
/** Catalog grouped by category, in category display order, dropping empty categories. */
export const PERMISSION_CATALOG_BY_CATEGORY: CategoryWithEntries[] = PERMISSION_CATEGORIES.map(
(category) => ({
...category,
entries: PERMISSION_CATALOG.filter((entry) => entry.category === category.key),
})
).filter((category) => category.entries.length > 0)
/** Map of resource key -> selected mode. Absent keys are treated as 'none'. */
export type PermissionSelection = Record<string, PermissionMode>
@@ -577,7 +46,7 @@ export const selectionToScopes = (
): ScopedAccessTokenPermission[] => {
const scopes: ScopedAccessTokenPermission[] = []
for (const [key, mode] of Object.entries(selection)) {
const entry = CATALOG_BY_KEY.get(key)
const entry = getCatalogEntry(key)
if (!entry) continue
scopes.push(...getEntryScopes(entry, mode))
}
@@ -590,7 +59,7 @@ export const selectionToScopes = (
* Tokens created through the Management API can hold arbitrary scope subsets that the
* none/read/readwrite modes cannot represent exactly (e.g. a lone branching_development_create).
* Any granted scope of an entry marks it at the corresponding mode, so a partial grant is never
* dropped — the mode is an upper bound and may name specific operations the token lacks, but it
* dropped. The mode is an upper bound and may name specific operations the token lacks, but it
* never understates the token's authority or risk. The endpoint and MCP-tool lists, computed
* from the actual granted scopes, remain the precise view.
*/
@@ -623,12 +92,6 @@ export const RISK_LEVEL_LABEL: Record<RiskLevel, string> = {
high: 'High risk',
}
export const PERMISSION_MODE_LABEL: Record<PermissionMode, string> = {
none: 'None',
read: 'Read',
readwrite: 'Read-write',
}
export type ResourceAccessMode = 'project' | 'organization' | 'account'
export const RISK_TONE_VARIANT: Record<RiskLevel, 'success' | 'warning' | 'destructive'> = {
@@ -13,8 +13,6 @@ import { getActivePreset, type PermissionPreset } from '../../AccessToken.preset
import type { TokenAccessEvaluation } from '../../AccessToken.roles'
import { PermissionPresetSelect } from './PermissionPresetSelect'
import { PermissionRow } from './PermissionRow'
import { InlineLink } from '@/components/ui/InlineLink'
import { DOCS_URL } from '@/lib/constants'
interface PermissionsAccordionProps {
selection: PermissionSelection
@@ -42,11 +40,7 @@ export const PermissionsAccordion = ({
description={
<p className="text-foreground-lighter text-sm">
Grant the minimum access this token needs. Everything defaults to None. Permissions
follow your role in the organizations and projects you're a member of — see{' '}
<InlineLink href={`${DOCS_URL}/guides/platform/access-control`}>
access control
</InlineLink>{' '}
for how roles work.
follow your role in the organizations and projects you're a member of.
</p>
}
>
@@ -15,6 +15,7 @@ import { ExperimentalTokenDropdown } from '../Classic/ExperimentalTokenDropdown'
import { NewScopedTokenForm } from './Form/NewScopedTokenForm'
import { getExpiryDate, type TokenFormValues } from './Form/NewScopedTokenForm.utils'
import { NewScopedTokenSuccess } from './Form/NewScopedTokenSuccess'
import { TokenDocsButtons } from './TokenDocsButtons'
import {
useAccessTokenCreateMutation,
type NewAccessToken,
@@ -124,11 +125,12 @@ export const NewScopedTokenSheet = ({ onCreateExperimentalToken }: NewScopedToke
size="default"
className="flex h-full flex-col gap-0 sm:w-[656px] lg:w-[800px]"
>
<SheetHeader>
<SheetHeader className="flex flex-col md:flex-row justify-between gap-4 items-start md:items-center">
<SheetTitle>{step === 'success' ? 'Token created' : 'Generate token'}</SheetTitle>
<SheetDescription className="sr-only">
Configure and create a new access token.
</SheetDescription>
{step !== 'success' && <TokenDocsButtons />}
</SheetHeader>
{step === 'success' && createdToken ? (
<NewScopedTokenSuccess
@@ -0,0 +1,20 @@
import { DocsButton } from '@/components/ui/DocsButton'
import { DOCS_URL } from '@/lib/constants'
/** Docs links shown in the header of the scoped token sheets. */
export const TokenDocsButtons = () => {
return (
<div className="flex items-center gap-2">
<DocsButton
href={`${DOCS_URL}/guides/platform/personal-access-tokens`}
topic="Personal access tokens"
label="Access tokens docs"
/>
<DocsButton
href={`${DOCS_URL}/guides/platform/access-control`}
topic="Access control"
label="Access control docs"
/>
</div>
)
}
@@ -21,7 +21,7 @@ import {
getCapabilityDensityTier,
type CapabilityLevelFilter,
} from './TokenCapabilities/TokenCapabilities.utils'
import { DocsButton } from '@/components/ui/DocsButton'
import { TokenDocsButtons } from './TokenDocsButtons'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query'
import {
@@ -29,7 +29,6 @@ import {
useGetEnabledEndpointsForCapability,
} from '@/data/scoped-access-tokens/permission-scope-map-query'
import { useScopedAccessTokenQuery } from '@/data/scoped-access-tokens/scoped-access-token-query'
import { DOCS_URL } from '@/lib/constants'
interface ViewTokenSheetProps {
visible: boolean
@@ -148,18 +147,7 @@ export function ViewTokenSheet({ visible, tokenId, onClose }: ViewTokenSheetProp
<p className="truncate" title={`View access for ${token?.name}`}>
View access for {token?.name}
</p>
<div className="flex items-center gap-2">
<DocsButton
href={`${DOCS_URL}/guides/platform/access-control`}
topic="Access control"
label="Access control docs"
/>
<DocsButton
href={`${DOCS_URL}/reference/api/introduction`}
topic="Management API"
label="API docs"
/>
</div>
<TokenDocsButtons />
</SheetHeader>
{/* Radix wraps viewport children in an inline-styled display:table div that grows to fit
the widest child, which would let one long endpoint path expand the sheet instead of
+1
View File
@@ -15,6 +15,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"@supabase/shared-types": "0.1.91",
"zod": "catalog:"
}
}
@@ -0,0 +1,625 @@
import { permissions } from '@supabase/shared-types'
/**
* The permission catalog for scoped personal access tokens: every FGA scope grouped into resources with
* display names, categories, and risk metadata.
*
* The real permission scopes come from `@supabase/shared-types` (`FgaPermissions`). Those scopes
* carry no category or risk metadata, so this file layers editable presentation data on top:
* - PERMISSION_CATEGORIES groups every scope into a display category.
* - RESOURCE_METADATA assigns each resource a display name, description, category and risk.
*
* Lives in shared-data (not apps/studio) because two apps consume it: Studio's scoped personal
* access token creation form, and the docs generator that renders the "Personal Access Tokens"
* guide's permission tables (apps/docs/spec/sections/generateAccessControlPartials.mts).
* The docs must show the same names, categories, and order as the form.
*
* The `name` and `category` fields are published copy: they are the permission and section labels
* in both the form and the docs guide, so renaming one renames both on the next regeneration.
*
* TODO(product): the risk levels, risk reasons, and "Allows" copy below are proposed defaults and
* still need review. They render in the form only, not in the docs. Where a resource has no
* explicit metadata entry we fall back to a heuristic.
*/
const FGA = permissions.FgaPermissions
type PermissionsOf<T> = T extends Record<string, infer P> ? P : never
/** Union of every FGA scope id literal published by @supabase/shared-types. */
export type FgaScopeId =
PermissionsOf<(typeof FGA)[keyof typeof FGA]> extends { id: infer Id } ? Id : never
export type RiskLevel = 'low' | 'medium' | 'high'
/**
* Display labels for permission modes. Part of the form<->docs contract like resource names:
* Studio's form and the docs guide's tables must label modes identically.
*/
export const PERMISSION_MODE_LABEL = {
none: 'None',
read: 'Read',
readwrite: 'Read-write',
} as const
/** Selection modes for a catalog entry. The label map's keys are the single source of truth. */
export type PermissionMode = keyof typeof PERMISSION_MODE_LABEL
export type PermissionCategoryKey = 'account' | 'project' | 'database' | 'appsvc' | 'infra'
export interface PermissionCategory {
key: PermissionCategoryKey
name: string
description: string
}
/** Display order. The form's accordion sections and the docs guide's sections both follow it. */
const PERMISSION_CATEGORIES: PermissionCategory[] = [
{
key: 'project',
name: 'Project',
description: 'Core project visibility, settings, and diagnostics.',
},
{
key: 'database',
name: 'Database',
description: 'SQL access, migrations, backups, and data operations.',
},
{
key: 'appsvc',
name: 'Application services',
description: 'Auth, storage, realtime, edge functions, and service configuration.',
},
{
key: 'infra',
name: 'Infrastructure and delivery',
description: 'Branch automation, domains, add-ons, and network.',
},
{
key: 'account',
name: 'Account and organization',
description: 'Account-wide and organization-level access that spans projects.',
},
]
interface ResourceMeta {
category: PermissionCategoryKey
name: string
description: string
risk: RiskLevel
riskReason: string
allowsRead?: string[]
allowsWrite?: string[]
}
/**
* Per-resource presentation metadata, keyed by the derived `scope:resource` key (derived in
* `buildCatalog` below). Every resource returned from FgaPermissions should have an entry;
* RESOURCE_METADATA_FALLBACK covers anything that slips through.
*/
const RESOURCE_METADATA: Record<string, ResourceMeta> = {
// --- Account and organization ---
'user:organizations': {
category: 'account',
name: 'Organizations',
description: 'Organizations you belong to.',
risk: 'medium',
riskReason: 'Read-write can create new organizations under your account.',
allowsRead: ['List your organizations'],
allowsWrite: ['Create organizations'],
},
'user:projects': {
category: 'account',
name: 'Projects (account-wide)',
description: 'Projects across all your organizations.',
risk: 'low',
riskReason: 'Read-only listing of the projects you can access.',
allowsRead: ['List your projects'],
},
'user:snippets': {
category: 'account',
name: 'SQL Snippets (account-wide)',
description: 'Saved SQL snippets across your account.',
risk: 'low',
riskReason: 'Read-only access to your saved snippets.',
allowsRead: ['Read your SQL snippets'],
},
'organization:admin': {
category: 'account',
name: 'Organization Settings',
description: 'Organization settings and project transfers.',
risk: 'high',
riskReason: 'Read-write grants elevated access to organization settings and project transfers.',
allowsRead: ['Read organization settings'],
allowsWrite: ['Manage organization settings', 'Transfer projects'],
},
'organization:members': {
category: 'account',
name: 'Organization Members',
description: 'Members and roles within the organization.',
risk: 'high',
riskReason: 'Read-write can add or remove members and change roles across your organization.',
allowsRead: ['Read organization members'],
allowsWrite: ['Add or remove members', 'Change member roles'],
},
'organization:projects': {
category: 'account',
name: 'Organization Projects',
description: 'Projects within the organization.',
risk: 'medium',
riskReason: 'Read-write can create new projects in the organization.',
allowsRead: ['List organization projects'],
allowsWrite: ['Create organization projects'],
},
'organization:platform_webhooks': {
category: 'account',
name: 'Platform Webhooks (organization)',
description: 'Platform webhook endpoints and deliveries for the organization.',
risk: 'medium',
riskReason: 'Read-write can create webhook endpoints that receive organization events.',
allowsRead: ['Read webhook endpoints and deliveries'],
allowsWrite: ['Manage webhook endpoints'],
},
// --- Project ---
'project:admin': {
category: 'project',
name: 'Project Settings',
description: 'Project metadata and settings.',
risk: 'high',
riskReason: 'Read-write grants elevated access to change project settings and configuration.',
allowsRead: ['Read project metadata'],
allowsWrite: ['Update project settings'],
},
'project:action_runs': {
category: 'project',
name: 'Action Runs',
description: 'Project action run status and logs.',
risk: 'medium',
riskReason: 'Read-write can trigger action runs that execute project workflows.',
allowsRead: ['Read action run status', 'Read run logs'],
allowsWrite: ['Trigger action runs'],
},
'project:advisors': {
category: 'project',
name: 'Advisors',
description: 'Security and performance advisor results.',
risk: 'low',
riskReason: 'Read-only access to advisor findings. No changes possible.',
allowsRead: ['Read security advisors', 'Read performance advisors'],
},
'project:analytics_logs': {
category: 'project',
name: 'Logs',
description: 'Operational logs and log analytics.',
risk: 'low',
riskReason: 'Read-only access to project logs.',
allowsRead: ['Read project logs'],
},
'project:analytics_usage': {
category: 'project',
name: 'Usage Analytics',
description: 'Project usage and analytics data.',
risk: 'low',
riskReason: 'Read-only access to usage analytics.',
allowsRead: ['Read usage analytics'],
},
'project:analytics_config': {
category: 'project',
name: 'Analytics Config',
description: 'Log drains and analytics configuration.',
risk: 'medium',
riskReason: 'Read-write can create log drains that export project logs.',
allowsRead: ['Read analytics configuration'],
allowsWrite: ['Manage log drains'],
},
'project:platform_webhooks': {
category: 'project',
name: 'Platform Webhooks',
description: 'Platform webhook endpoints and deliveries for the project.',
risk: 'medium',
riskReason: 'Read-write can create webhook endpoints that receive project events.',
allowsRead: ['Read webhook endpoints and deliveries'],
allowsWrite: ['Manage webhook endpoints'],
},
'project:snippets': {
category: 'project',
name: 'SQL Snippets',
description: 'Saved SQL snippets for the project.',
risk: 'low',
riskReason: 'Read-write can create and edit saved SQL snippets.',
allowsRead: ['Read project SQL snippets'],
allowsWrite: ['Manage project SQL snippets'],
},
// --- Database ---
'project:database': {
category: 'database',
name: 'Database',
description: 'Database access and data operations.',
risk: 'high',
riskReason:
'Read-write lets this token run arbitrary SQL, so it can modify or delete any data in your database.',
allowsRead: ['Read tables and schema', 'Run read-only queries'],
allowsWrite: ['Run arbitrary SQL'],
},
'project:database_migrations': {
category: 'database',
name: 'Migrations',
description: 'Database migration history and application.',
risk: 'high',
riskReason:
'Read-write can apply schema changes that alter or drop tables across your database.',
allowsRead: ['Read migration history'],
allowsWrite: ['Apply migrations'],
},
'project:backups': {
category: 'database',
name: 'Backups',
description: 'Database backups, restore points, and restore.',
risk: 'high',
riskReason:
'Read-write can trigger restores that overwrite current data with an earlier snapshot.',
allowsRead: ['Read backups and restore points'],
allowsWrite: ['Trigger restores'],
},
'project:database_config': {
category: 'database',
name: 'Database Config',
description: 'Database configuration.',
risk: 'medium',
riskReason: 'Read-write can change database configuration.',
allowsRead: ['Read database configuration'],
allowsWrite: ['Update database configuration'],
},
'project:database_jit': {
category: 'database',
name: 'Database JIT',
description: 'Just-in-time database access settings.',
risk: 'medium',
riskReason: 'Read-write can change just-in-time database access settings.',
allowsRead: ['Read JIT settings'],
allowsWrite: ['Manage JIT settings'],
},
'project:database_pooling_config': {
category: 'database',
name: 'Connection Pooling',
description: 'Database connection pooling.',
risk: 'medium',
riskReason: 'Read-write can change connection pooling behavior.',
allowsRead: ['Read pooling configuration'],
allowsWrite: ['Update pooling configuration'],
},
'project:database_readonly_config': {
category: 'database',
name: 'Read-only Mode',
description: 'Database read-only mode.',
risk: 'medium',
riskReason: 'Read-write can toggle the database into or out of read-only mode.',
allowsRead: ['Read read-only mode status'],
allowsWrite: ['Toggle read-only mode'],
},
'project:database_ssl_config': {
category: 'database',
name: 'SSL Enforcement',
description: 'Database SSL configuration.',
risk: 'medium',
riskReason: 'Read-write can change SSL enforcement for database connections.',
allowsRead: ['Read SSL configuration'],
allowsWrite: ['Manage SSL enforcement'],
},
'project:database_webhooks_config': {
category: 'database',
name: 'Database Webhooks',
description: 'Webhooks triggered from the database.',
risk: 'medium',
riskReason: 'Read-write can change database webhook configuration.',
allowsRead: ['Read webhook configuration'],
allowsWrite: ['Manage database webhooks'],
},
'project:database_network_bans': {
category: 'database',
name: 'Network Bans',
description: 'Banned IPs for the database.',
risk: 'medium',
riskReason: 'Read-write can ban or unban IP addresses from reaching the database.',
allowsRead: ['Read banned IPs'],
allowsWrite: ['Manage banned IPs'],
},
'project:database_network_restrictions': {
category: 'database',
name: 'Network Restrictions',
description: 'Network restrictions for the database.',
risk: 'high',
riskReason: 'Read-write can change which networks are allowed to reach the database.',
allowsRead: ['Read network restrictions'],
allowsWrite: ['Manage network restrictions'],
},
// --- Application services ---
'project:auth_config': {
category: 'appsvc',
name: 'Auth Config',
description: 'Authentication provider and settings.',
risk: 'high',
riskReason:
'Read-write can change authentication providers and settings, affecting how users sign in.',
allowsRead: ['Read auth configuration'],
allowsWrite: ['Update auth providers and settings'],
},
'project:auth_signing_keys': {
category: 'appsvc',
name: 'Auth Signing Keys',
description: 'Authentication signing keys.',
risk: 'high',
riskReason: 'Read-write can rotate signing keys, invalidating existing sessions and tokens.',
allowsRead: ['Read signing keys'],
allowsWrite: ['Manage signing keys'],
},
'project:api_gateway_keys': {
category: 'appsvc',
name: 'API Keys',
description: 'Project API keys.',
risk: 'high',
riskReason: 'Read exposes API keys; read-write grants elevated access to create new keys.',
allowsRead: ['Read project API keys'],
allowsWrite: ['Create and revoke API keys'],
},
'project:edge_functions': {
category: 'appsvc',
name: 'Edge Functions',
description: 'Edge functions.',
risk: 'medium',
riskReason: 'Read-write can deploy or delete edge functions.',
allowsRead: ['Read edge functions'],
allowsWrite: ['Deploy and delete edge functions'],
},
'project:edge_functions_secrets': {
category: 'appsvc',
name: 'Edge Function Secrets',
description: 'Secrets available to edge functions.',
risk: 'high',
riskReason: 'Read exposes function secrets; read-write can set new secret values.',
allowsRead: ['Read edge function secrets'],
allowsWrite: ['Set edge function secrets'],
},
'project:realtime_config': {
category: 'appsvc',
name: 'Realtime Config',
description: 'Realtime configuration.',
risk: 'medium',
riskReason: 'Read-write can change realtime settings and shut down active connections.',
allowsRead: ['Read realtime configuration'],
allowsWrite: ['Update realtime settings'],
},
'project:storage': {
category: 'appsvc',
name: 'Storage',
description: 'File storage buckets and objects.',
risk: 'medium',
riskReason: 'Read-write can modify or delete stored files.',
allowsRead: ['Read storage buckets and objects'],
allowsWrite: ['Manage storage buckets and objects'],
},
'project:storage_config': {
category: 'appsvc',
name: 'Storage Config',
description: 'Storage bucket configuration.',
risk: 'medium',
riskReason: 'Read-write can change storage configuration.',
allowsRead: ['Read storage configuration'],
allowsWrite: ['Update storage configuration'],
},
'project:data_api_config': {
category: 'appsvc',
name: 'Data API Config',
description: 'PostgREST behavior and settings.',
risk: 'medium',
riskReason: 'Read-write can change how the auto-generated Data API behaves.',
allowsRead: ['Read Data API configuration'],
allowsWrite: ['Update Data API configuration'],
},
// --- Infrastructure and delivery ---
'project:branching_development': {
category: 'infra',
name: 'Development Branches',
description: 'Development branch automation.',
risk: 'low',
riskReason: 'Branch automation for development workflows with limited blast radius.',
allowsRead: ['Read development branches'],
allowsWrite: ['Create, update, and delete development branches'],
},
'project:branching_production': {
category: 'infra',
name: 'Production Branches',
description: 'Production branch automation.',
risk: 'high',
riskReason:
'Read-write grants elevated access to create, merge, or delete production branches.',
allowsRead: ['Read production branches'],
allowsWrite: ['Create, merge, and delete production branches'],
},
'project:custom_domain': {
category: 'infra',
name: 'Custom Domains',
description: 'Custom hostnames.',
risk: 'medium',
riskReason: 'Read-write can change custom hostnames, affecting how your project is reached.',
allowsRead: ['Read custom domain configuration'],
allowsWrite: ['Set custom hostnames'],
},
'project:vanity_subdomain': {
category: 'infra',
name: 'Vanity Subdomain',
description: 'Project vanity subdomain.',
risk: 'medium',
riskReason: 'Read-write can change the project vanity subdomain.',
allowsRead: ['Read vanity subdomain'],
allowsWrite: ['Manage vanity subdomain'],
},
'project:infra_addons': {
category: 'infra',
name: 'Add-ons',
description: 'Infrastructure add-ons.',
risk: 'medium',
riskReason: 'Read-write can enable or change paid infrastructure add-ons.',
allowsRead: ['Read infrastructure add-ons'],
allowsWrite: ['Manage infrastructure add-ons'],
},
'project:infra_disk_config': {
category: 'infra',
name: 'Disk Config',
description: 'Disk configuration.',
risk: 'medium',
riskReason: 'Read-write can change disk size and configuration, which may incur cost.',
allowsRead: ['Read disk configuration'],
allowsWrite: ['Manage disk configuration'],
},
'project:read_replicas': {
category: 'infra',
name: 'Read Replicas',
description: 'Read replica configuration.',
risk: 'medium',
riskReason: 'Read-write can provision or remove read replicas, which may incur cost.',
allowsRead: ['Read read-replica configuration'],
allowsWrite: ['Manage read replicas'],
},
}
const RESOURCE_METADATA_FALLBACK = (
resourceKey: string,
title: string,
hasWrite: boolean
): ResourceMeta => ({
category: resourceKey.startsWith('project:') ? 'project' : 'account',
// Title-cased so an uncurated resource doesn't ship a lowercase FGA-derived name
// (e.g. "project analytics configurations") next to the curated Title Case entries.
name: title
.replace(/^(Read|Manage|Create|Delete)\s+/i, '')
.replace(/\b[a-z]/g, (char) => char.toUpperCase()),
description: title,
risk: hasWrite ? 'medium' : 'low',
riskReason: hasWrite
? 'Read-write can modify this resource.'
: 'Read-only access to this resource.',
})
const PERMISSION_LEVELS = ['user', 'organization', 'project'] as const
export type PermissionLevel = (typeof PERMISSION_LEVELS)[number]
/**
* Runtime guard for the FGA namespaces: role evaluation branches on the level, so an unrecognized
* namespace must fail loudly (at module load, caught by any test importing the catalog) rather
* than silently evaluate as project-level.
*/
const toPermissionLevel = (scope: string): PermissionLevel => {
const level = scope.toLowerCase()
const match = PERMISSION_LEVELS.find((candidate) => candidate === level)
if (match === undefined) throw new Error(`Unknown FGA namespace: ${scope}`)
return match
}
export interface PermissionCatalogEntry {
/** Derived resource key, e.g. "project:database" */
key: string
/** Which FGA namespace the resource lives in. Decides which role (org vs project) governs it. */
level: PermissionLevel
category: PermissionCategoryKey
name: string
description: string
risk: RiskLevel
riskReason: string
allowsRead: string[]
allowsWrite: string[]
/** Whether a Read-write mode is offered (false => read-only resource). */
writable: boolean
/** FGA scope ids granted at Read (and above). */
readScopes: FgaScopeId[]
/** Additional FGA scope ids granted at Read-write (write / create / delete). */
writeScopes: FgaScopeId[]
}
/** Action classes an FGA permission key's suffix can map to. */
export type FgaAction = 'read' | 'write' | 'create' | 'delete'
/**
* Classifies an FGA permission key by its action suffix: "PROJECTS_READ" -> "read".
* Keys without a write, create, or delete suffix are treated as read.
*/
export const getAction = (key: string): FgaAction => {
if (key.endsWith('_WRITE')) return 'write'
if (key.endsWith('_CREATE')) return 'create'
if (key.endsWith('_DELETE')) return 'delete'
return 'read'
}
const isReadScope = (key: string): boolean => getAction(key) === 'read'
/** Strips the FGA action suffix off a permission key: "PROJECTS_READ" -> "projects". */
export const getResource = (key: string): string =>
key.replace(/_(READ|WRITE|CREATE|DELETE)$/, '').toLowerCase()
/**
* Builds the permission catalog from the real FgaPermissions. Each unique `scope:resource` becomes
* one row; its read scope maps to Read mode and its write/create/delete scopes to Read-write mode.
*/
const buildCatalog = (): PermissionCatalogEntry[] => {
const byResource = new Map<
string,
{ level: PermissionLevel; title: string; readScopes: string[]; writeScopes: string[] }
>()
for (const [scope, scopePerms] of Object.entries(FGA)) {
const level = toPermissionLevel(scope)
for (const [permKey, perm] of Object.entries(scopePerms)) {
const resourceKey = `${level}:${getResource(permKey)}`
if (!byResource.has(resourceKey)) {
byResource.set(resourceKey, { level, title: perm.title, readScopes: [], writeScopes: [] })
}
const entry = byResource.get(resourceKey)!
if (isReadScope(permKey)) entry.readScopes.push(perm.id)
else entry.writeScopes.push(perm.id)
}
}
const catalog: PermissionCatalogEntry[] = []
for (const [key, { level, title, readScopes, writeScopes }] of byResource.entries()) {
const meta =
RESOURCE_METADATA[key] ?? RESOURCE_METADATA_FALLBACK(key, title, writeScopes.length > 0)
catalog.push({
key,
level,
category: meta.category,
name: meta.name,
description: meta.description,
risk: meta.risk,
riskReason: meta.riskReason,
allowsRead: meta.allowsRead ?? [`Read ${meta.name.toLowerCase()}`],
allowsWrite:
meta.allowsWrite ?? (writeScopes.length > 0 ? [`Modify ${meta.name.toLowerCase()}`] : []),
writable: writeScopes.length > 0,
readScopes: readScopes as FgaScopeId[],
writeScopes: writeScopes as FgaScopeId[],
})
}
return catalog
}
export const PERMISSION_CATALOG = buildCatalog()
const CATALOG_BY_KEY = new Map(PERMISSION_CATALOG.map((entry) => [entry.key, entry]))
export const getCatalogEntry = (key: string) => CATALOG_BY_KEY.get(key)
export interface CategoryWithEntries extends PermissionCategory {
entries: PermissionCatalogEntry[]
}
/** Catalog grouped by category, in category display order, dropping empty categories. */
export const PERMISSION_CATALOG_BY_CATEGORY: CategoryWithEntries[] = PERMISSION_CATEGORIES.map(
(category) => ({
...category,
entries: PERMISSION_CATALOG.filter((entry) => entry.category === category.key),
})
).filter((category) => category.entries.length > 0)
+3
View File
@@ -2592,6 +2592,9 @@ importers:
packages/shared-data:
dependencies:
'@supabase/shared-types':
specifier: 0.1.91
version: 0.1.91
zod:
specifier: 'catalog:'
version: 3.25.76