Merge branch 'master' into chore/migrate-tabs

This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-06-04 11:39:51 +02:00
commit 8a4d2f1040
88 files changed
+4746 -381

No files matched your search

@@ -25,7 +25,7 @@ const meta = {
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/database/database-linter',
pathname: '/guides/database/database-advisors',
meta,
}))
@@ -15,7 +15,7 @@ import { linkTransform, type UrlTransformFunction } from '~/lib/mdx/plugins/rehy
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import { getGitHubFileContents } from '~/lib/octokit'
import { getGitHubFileContents, octokit } from '~/lib/octokit'
import type { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import { isFeatureEnabled } from 'common'
import matter from 'gray-matter'
@@ -29,10 +29,62 @@ import { Admonition } from 'ui-patterns'
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'wrappers'
const branch = 'main'
const docsDir = 'docs/catalog'
const externalSite = 'https://supabase.github.io/wrappers'
type DocsTagsQueryResponse = {
repository: {
refs: {
nodes: { name: string }[] | null
pageInfo: { hasNextPage: boolean; endCursor: string | null }
}
}
}
const docsTagsQuery = `
query DocsTagsQuery($owner: String!, $name: String!, $after: String) {
repository(owner: $owner, name: $name) {
refs(
refPrefix: "refs/tags/",
orderBy: { field: TAG_COMMIT_DATE, direction: DESC },
first: 5,
after: $after
) {
nodes { name }
pageInfo { hasNextPage endCursor }
}
}
}
`
async function getLatestDocsTag(after: string | null = null): Promise<string | null> {
try {
/**
* We use GraphQL as it's the only way to use `orderBy` on Github API.
*/
const {
repository: {
refs: {
nodes,
pageInfo: { hasNextPage, endCursor },
},
},
} = await octokit().graphql<DocsTagsQueryResponse>(docsTagsQuery, {
owner: org,
name: repo,
after,
})
return (
nodes?.find(({ name }) => /^docs_v\d+\.\d+\.\d+/.test(name))?.name ??
(hasNextPage && endCursor ? await getLatestDocsTag(endCursor) : null)
)
} catch (error) {
console.error(`Error fetching docs tags for wrappers federated pages: ${error}`)
return null
}
}
// Each external docs page is mapped to a local page
const pageMap = [
{
@@ -378,16 +430,22 @@ const getContent = async (params: Params) => {
let remoteFile: string
;({ remoteFile, meta } = federatedPage)
editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`
const tag = await getLatestDocsTag()
if (!tag) {
throw new Error('No latest docs tag found for federated wrappers pages')
}
editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}`
let rawContent = await getGitHubFileContents({
org,
repo,
path: `${docsDir}/${remoteFile}`,
branch,
branch: tag,
})
assetsBaseUrl = `https://raw.githubusercontent.com/${org}/${repo}/${branch}/docs/assets/`
assetsBaseUrl = `https://raw.githubusercontent.com/${org}/${repo}/${tag}/docs/assets/`
const { content: contentWithoutFrontmatter } = matter(rawContent)
content = removeRedundantH1(contentWithoutFrontmatter)
@@ -87,7 +87,7 @@ const getContent = async ({ slug }: Params) => {
)
return {
pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`,
pathname: `/guides/deployment/ci/${slug}` satisfies `/${string}`,
meta,
content,
editLink,
@@ -136,7 +136,7 @@ const getContent = async ({ slug }: Params) => {
return {
pathname:
`/guides/platform/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
`/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
meta,
content,
editLink,
@@ -22,7 +22,7 @@ const meta = {
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/platform/terraform/reference',
pathname: '/guides/deployment/terraform/reference',
meta,
}))
@@ -12,7 +12,7 @@ const meta = {
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/cli/config',
pathname: '/guides/local-development/cli/config',
meta,
}))
@@ -340,6 +340,10 @@ export const gettingstarted: NavMenuConstant = {
{ name: 'API Keys', url: '/guides/getting-started/api-keys' },
{ name: 'Local Development', url: '/guides/cli/getting-started' },
{ name: 'Architecture', url: '/guides/getting-started/architecture' },
{
name: 'Migrating to new API keys',
url: '/guides/getting-started/migrating-to-new-api-keys',
},
{
name: 'Framework Quickstarts',
enabled: frameworkQuickstartsEnabled,
@@ -1571,6 +1575,10 @@ export const api: NavMenuConstant = {
{ name: 'Generating TypeScript Types', url: '/guides/api/rest/generating-types' },
{ name: 'Generating Python Types', url: '/guides/api/rest/generating-python-types' },
{ name: 'Error Codes', url: '/guides/api/rest/postgrest-error-codes' },
{
name: 'Handling Errors in supabase-js',
url: '/guides/api/handling-errors-in-supabase-js',
},
],
},
{
@@ -65,7 +65,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Construct image url from storage
@@ -0,0 +1,145 @@
---
id: handling-errors-in-supabase-js
title: 'Handling errors in `supabase-js`'
subtitle: 'Read `error.hint` first — Postgres often tells you the exact fix. Log the full error so you actually see it.'
---
Every `supabase-js` call returns a `{ data, error }` pair instead of throwing. When something fails, the single most useful field on `error` is usually `hint` — Postgres returns the _fix_, not just a description of the problem. Logging only `error.message` hides it.
## Usage of `message` and `hint` properties
Consider a `42501` permission-denied error on a table where default `GRANT`s have been revoked from `anon`:
```
message: "permission denied for table users"
hint: "Grant the required privileges to the current role with: GRANT SELECT ON public.users TO anon;"
```
The `message` exposes the error reason, and `hint` gives you the literal SQL statement to run in the dashboard SQL editor to fix it.
The same pattern shows up across many Postgres errors — missing column? `hint` suggests the column name you probably meant. Type mismatch? `hint` shows the expected type. Whenever Postgres knows the fix, it puts it in `hint`.
<Admonition type="tip">Log the full `error` object, not just `error.message`.</Admonition>
## The recommended pattern
Read `{ data, error }` from the response, check `error`, log the whole object, and return early.
```ts
const { data, error } = await supabase.from('users').select()
if (error) {
console.error(error)
return
}
```
In the case of a permission-denied error, the response body will look like this:
```json
{
"error": {
"code": "42501",
"message": "permission denied for table users",
"details": null,
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.users TO anon;"
},
"status": 401,
"statusText": "Unauthorized"
}
```
`postgrest-js` passes the body through verbatim, so `error.hint` is the exact string Postgres produced. Treat it as the answer the database is giving you, not as a suggestion to file away.
## The `PostgrestError` fields, by usefulness
Database calls (`select`, `insert`, `update`, `upsert`, `delete`, `rpc`) return a `PostgrestError` with four fields. Read them in roughly this order:
| Field | Read it when |
| --------- | ------------------------------------------------------------------------------------------------------------------ |
| `hint` | Always check first. When Postgres includes one, it's the actionable fix (a `GRANT` to run, a column name, a type). |
| `code` | When branching in code. Codes are stable across versions; `message` text isn't. |
| `details` | When `hint` and `message` aren't enough. Often contains the offending value, key, or row. |
| `message` | As the human summary. Useful in UI strings, less useful for debugging. |
A full list of PostgREST error codes is in the [Error Codes reference](/guides/api/rest/postgrest-error-codes).
## Branch on `error.code`, not `error.message`
`error.code` is more reliable than `error.message` for programmatic branching: messages change between Postgres and PostgREST versions, but codes are stable.
```ts
const { data, error } = await supabase.from('users').select()
if (error) {
console.error(error)
if (error.code === '42501') {
// Permission denied. error.hint usually contains the GRANT to run.
}
return
}
```
## Errors from Auth, Storage, and Edge Functions
The same rule applies across the SDK — log the whole error object — but the shape differs by client.
### Auth
`AuthError` exposes `error.code` (e.g. `'invalid_credentials'`, `'email_not_confirmed'`) and `error.status`. Branch on `code`; log the whole thing.
```ts
const { data, error } = await supabase.auth.signInWithPassword({
email: 'example@email.com',
password: 'example-password',
})
if (error) {
console.error(error)
return
}
```
### Storage
`StorageError` exposes `error.statusCode` (HTTP status as a string) and a structured `error` name (e.g. `'Duplicate'`, `'NotFound'`).
```ts
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', avatarFile)
if (error) {
console.error(error)
return
}
```
### Edge Functions
Functions errors arrive as one of three subclasses. Narrow with `instanceof`; for `FunctionsHttpError`, parse the body to get the function's own error payload.
```ts
import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from '@supabase/supabase-js'
const { data, error } = await supabase.functions.invoke('hello')
if (error instanceof FunctionsHttpError) {
console.error('Function error', await error.context.json())
} else if (error) {
console.error(error)
}
```
### Realtime
The `subscribe()` callback receives a `status` and, on failure, an `err` argument. Log the whole `err` — its `cause` often holds the underlying reason.
```ts
supabase.channel('room1').subscribe((status, err) => {
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
console.error(status, err)
}
})
```
## Related
- [PostgREST Error Codes](/guides/api/rest/postgrest-error-codes)
- [Automatic retries with `supabase-js`](/guides/api/automatic-retries-in-supabase-js)
- [Securing your API](/guides/api/securing-your-api)
@@ -120,7 +120,7 @@ const elevenLabsClient = new ElevenLabsClient({
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL') || '',
Deno.env.get(SUPABASE_SECRET_KEYS['default']) || ''
SUPABASE_SECRET_KEYS['default'] || ''
)
async function scribe({
@@ -36,7 +36,7 @@ There are 4 types of API keys that you can use with Supabase:
Supabase has changed the way keys work to improve project security and developer experience. You can read [the full announcement](https://github.com/orgs/supabase/discussions/29260).
`anon` and `service_role` keys are based on the project's JWT secret. They are generated when your project is created and you can only change them when you rotate the JWT secret. This can cause significant issues in production applications. **You should now use the `sb_publishable_xxx` and `sb_secret_xxx` keys instead**.
`anon` and `service_role` keys are based on the project's JWT secret. They are generated when your project is created and you can only change them when you rotate the JWT secret. This can cause significant issues in production applications. **You should now use the `sb_publishable_xxx` and `sb_secret_xxx` keys instead**. See [Migrate to publishable and secret API keys](/docs/guides/getting-started/migrating-to-new-api-keys) for a step-by-step guide.
You can still find legacy keys in the **Legacy anon, service_role API keys** tab of the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard:
@@ -0,0 +1,225 @@
---
id: 'migrating-to-new-api-keys'
title: 'Migrating to publishable and secret API keys'
description: 'Move from legacy JWT-based anon and service_role keys to publishable and secret keys.'
---
Supabase has changed the way API keys work. The legacy `anon` and `service_role` keys are based on your project's JWT secret, which makes them hard to rotate without downtime. The new publishable (`sb_publishable_...`) and secret (`sb_secret_...`) keys can be created, named, and revoked independently, so you can rotate a single key without touching the rest of your app.
This guide covers migrating an **existing project.** Both key types work simultaneously, so you can swap clients one at a time and deactivate the legacy keys only after nothing depends on them.
<Admonition type="note">
The legacy `anon` and `service_role` keys keep working until the end of 2026. You don't have to migrate today, but doing it early lets you rotate keys safely from now on.
</Admonition>
## Before you start
The migration maps onto your existing keys:
| Legacy key | Replace with | Used by |
| -------------- | --------------- | ------------------------------------------------------ |
| `anon` | Publishable key | Browsers, mobile and desktop apps, CLIs, public source |
| `service_role` | Secret key | Servers, Edge Functions, workers, other backend code |
For a full explanation of each key type, read [the Understanding API keys guide](/docs/guides/getting-started/api-keys).
## Step 1: Create the new API keys
Open the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard and select the **Publishable and secret API keys** tab.
Older projects don't have these keys yet. If you see a **Create new API keys** button, your project is still on legacy keys only. Creating the new keys is safe. It adds a publishable key and a secret key alongside your existing `anon` and `service_role` keys. Your legacy keys keep working.
The new keys are created under the name `default`. You can add more keys with different names later, for example, one secret key per backend component, so you can rotate them independently. For an initial migration, the `default` publishable and secret keys are all you need.
## Step 2: Swap the publishable key in client code
Anywhere you use the `anon` key in public code, switch to the publishable key. This includes web pages, mobile and desktop apps, and any CLI or script that ships to users.
```ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'sb_publishable_...' // was the anon key
)
```
The publishable key carries the same low privileges as the `anon` key, so your [Row Level Security](/docs/guides/database/postgres/row-level-security) policies behave the same. User authentication through Supabase Auth is unchanged. The user still signs in and gets their own JWT.
## Step 3: Swap the secret key in backend code
Anywhere you use the `service_role` key on a server you control, switch to a secret key.
```ts
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
'https://your-project.supabase.co',
'sb_secret_...' // was the service_role key
)
```
Secret keys add protections the `service_role` key doesn't have. They return HTTP 401 if used in a browser (matched on the `User-Agent` header), and you can run a separate key per service so a single leak only forces one rotation.
<Admonition type="caution">
Secret keys bypass Row Level Security and have full access to your data. Keep them on backends you control, out of source control, and out of client code.
</Admonition>
### Database Webhooks and `pg_net`
Calls made from Postgres with `pg_net`, including Database Webhooks, usually send the `service_role` key on the `Authorization: Bearer` header. The new secret keys aren't JWTs, so they're rejected there. Send the secret key on the `apikey` header instead.
```sql
-- before
select net.http_post(
url := 'https://your-project.supabase.co/functions/v1/your-function',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer <service_role key>'
),
body := jsonb_build_object('event', 'ping')
);
-- after
select net.http_post(
url := 'https://your-project.supabase.co/functions/v1/your-function',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'apikey', 'sb_secret_...'
),
body := jsonb_build_object('event', 'ping')
);
```
For Database Webhooks created in the Dashboard, edit each webhook's HTTP headers: remove the `Authorization` header that holds the key and add an `apikey` header with a secret key instead.
<Admonition type="caution">
Don't hardcode a secret key in SQL or a webhook configuration, where it's stored in plain text. Store it in [Vault](/docs/guides/database/vault) and read it at call time:
```sql
headers := jsonb_build_object(
'Content-Type', 'application/json',
'apikey', (select decrypted_secret from vault.decrypted_secrets where name = 'secret_key')
)
```
</Admonition>
## Step 4: Update Edge Functions
Edge Functions read their keys from environment variables. Supabase adds two new ones to your functions' environment, `SUPABASE_PUBLISHABLE_KEYS` and `SUPABASE_SECRET_KEYS`, alongside the legacy `SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY`. Confirm they exist in the [**Edge Functions > Secrets**](/dashboard/project/_/functions/secrets) section of the Dashboard before you start.
You have two options: a minimal change that swaps which variable you read, or a fuller upgrade to the [`@supabase/server`](https://github.com/supabase/server) SDK.
### Option 1: Read the new keys from the environment
For most functions, the only change is how you read the key. The legacy variables held a plain string. The new ones hold a JSON object keyed by name, so you parse them and read the key by name. The key you created in [step 1](#step-1-create-the-new-api-keys) is named `default`.
```ts
// before
const secretKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
// after
const secretKey = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)['default']
```
The publishable keys work the same way through `SUPABASE_PUBLISHABLE_KEYS`. Read [the Managing Secrets guide](/docs/guides/functions/secrets) for more on environment variables in Edge Functions.
If you created more than one secret key in [step 1](#step-1-create-the-new-api-keys), every key lives in the same `SUPABASE_SECRET_KEYS` object, each under its own name. Read a non-default key the same way:
```ts
const secretKeys = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const defaultKey = secretKeys['default']
const billingKey = secretKeys['billing'] // the secret key you named "billing"
```
Send publishable and secret keys on the `apikey` header only. If you also pass the key on the `Authorization: Bearer` header, which many Supabase clients do by default, the platform tries to parse it as a JWT and rejects the request with `Invalid JWT`. The platform's built-in `verify_jwt` check only understands the legacy JWT-based keys, so set `verify_jwt = false` for these functions and authorize the request in your own code, or let the `@supabase/server` SDK do it for you ([Option 2](#option-2-adopt-the-supabaseserver-sdk)).
```toml
[functions.my-function]
verify_jwt = false
```
### Option 2: Adopt the @supabase/server SDK
To get the most out of the new key model, migrate to the [`@supabase/server`](https://github.com/supabase/server) SDK. It removes the client-setup boilerplate every function repeats: reading keys from the environment, parsing the `Authorization` header, and initializing a user-scoped client and a separate admin client. You declare who can call the function, and get both clients ready to use on `ctx` (`ctx.supabase` respects Row Level Security, `ctx.supabaseAdmin` uses the secret key). This is the recommended approach for new functions.
Wrap your existing `Deno.serve` handler with `withSupabase` and declare an `auth` mode for who can call it. Keep `verify_jwt = false` so the SDK does the authorization.
For a function your users call from the client, use `auth: 'user'`. The SDK validates the user's session JWT and gives you a client scoped to their Row Level Security policies.
```ts
import { withSupabase } from 'npm:@supabase/server'
Deno.serve(
withSupabase({ auth: 'user' }, async (_req, ctx) => {
// ctx.supabase is scoped to the authenticated user
return Response.json({ email: ctx.userClaims?.email })
})
)
```
For a function called by your own backend, a worker, or `pg_net`, use `auth: 'secret'`. The SDK validates the secret key and gives you a client that bypasses Row Level Security.
```ts
import { withSupabase } from 'npm:@supabase/server'
Deno.serve(
withSupabase({ auth: 'secret' }, async (_req, ctx) => {
// ctx.supabaseAdmin is authenticated with a valid secret key
return Response.json({ ok: true })
})
)
```
To accept a specific named key instead of `default`, add its name after the mode with a colon. For example, `auth: 'secret:billing'` validates the request against the secret key you named `billing`, and `auth: 'publishable:web'` against a publishable key named `web`.
`withSupabase` returns a standard request handler, so you can also export it as a `fetch` handler instead of passing it to `Deno.serve`:
```ts
import { withSupabase } from 'npm:@supabase/server'
export default {
fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
// ctx.supabase is scoped to the authenticated user
return Response.json({ email: ctx.userClaims?.email })
}),
}
```
`export default { fetch }` is equivalent to `Deno.serve(...)`: both define a request handler. The `fetch` style is portable across Edge Functions, Cloudflare Workers, and Bun, so prefer it if you want the same function to run in more than one environment. `Deno.serve` keeps working on Edge Functions, so you can leave it in place during a migration and switch later.
A good way to try this is to duplicate one of your functions and migrate the copy first. See [Securing Edge Functions](/docs/guides/functions/auth) for every auth mode and use case, and [Authorization headers](/docs/guides/functions/auth-headers) for how the headers work.
## Step 5: Verify nothing uses the legacy keys
Before turning the legacy keys off, confirm nothing still depends on them. There's no automatic usage indicator, so this is a manual check. Go through every place that holds a Supabase key and make sure it now uses a publishable or secret key.
Don't forget callers that are easy to miss:
- Mobile or desktop app versions already in users' hands.
- CI/CD pipelines and deployment scripts.
- Third-party integrations and webhooks.
- Cron jobs, workers, and `pg_net` calls or Database Webhooks (see [Database Webhooks and `pg_net`](#database-webhooks-and-pgnet)).
## Step 6: Deactivate the legacy keys
Once you've confirmed nothing uses the legacy keys, deactivate them in the [**Settings > API Keys**](/dashboard/project/_/settings/api-keys/) section of the Dashboard. You can re-activate them if you find a client you missed, so this step is reversible.
## Known limitations
A few behaviors differ from the legacy JWT-based keys. Plan for them during the migration:
- You can't send a publishable or secret key in the `Authorization: Bearer ...` header. Send it on the `apikey` header instead.
- Edge Functions don't verify the `apikey` header for the new keys. Use `verify_jwt = false` and authorize in code, as shown in [Step 4](#step-4-update-edge-functions).
- Public Realtime connections are limited to 24 hours unless the connection is upgraded with user-level authentication through Supabase Auth or a supported third-party auth provider.
## Next steps
After migrating your keys, consider moving to the [JWT signing keys](/docs/guides/auth/signing-keys) system as well. This is a separate, independent migration. The new publishable and secret keys aren't JWTs, so they no longer touch your project's JWT secret. But the access tokens Supabase Auth issues to your users are still signed by that shared secret. Signing keys replace it with rotatable keys you can change without downtime. Together, the two migrations get your whole project off the shared JWT secret.
@@ -264,7 +264,7 @@ The table below shows the actions each role can take on the resources belonging
| Production Branch | Read | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
| | Write | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| Development Branches | List | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> |
| | Create | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| | Create[^8] | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| | Update | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
| | Delete | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconCheck size={14} color="#3FCF8E" /> | <IconX size={14} /> |
@@ -281,3 +281,5 @@ The table below shows the actions each role can take on the resources belonging
[^6]: Listed permissions are for the API and Dashboard.
[^7]: Limited to executing SELECT queries. SQL Query Snippets run by the Read-Only role are run against the database using the **supabase_read_only_user**. This role has the [predefined Postgres role pg_read_all_data](https://www.postgresql.org/docs/current/predefined-roles.html).
[^8]: When using dashboard branching without a GitHub integration, the first branch creation also registers the project's production branch — a one-time step that requires Owner or Administrator. See [Branching via the dashboard](/docs/guides/deployment/branching/dashboard) for details. Developers can create, update, and delete branches normally after that.
+54 -33
View File
@@ -1,7 +1,6 @@
import { describe, it, expect } from 'vitest'
import { mdxToMarkdown } from 'mdast-util-mdx'
import { toMarkdown } from 'mdast-util-to-markdown'
import { describe, expect, it } from 'vitest'
import { partialsRemark } from './Partial'
import { fromDocsMarkdown } from './utils.server'
@@ -116,7 +115,12 @@ Some more text.
await expect(partialsRemark()(mdast)).rejects.toThrowError(/valid JSON/)
})
it('should error when required variable is missing', async () => {
it('should render an unprovided variable as an empty string', async () => {
// The variables.mdx fixture reads "Here is a partial that takes a {{ .var }}."
// When `var` is not provided, the `{{ .var }}` placeholder is replaced with
// an empty string rather than throwing. The trailing " ." in the expected
// output is the intended result: the placeholder is gone, leaving nothing
// between "a" and the period.
const markdown = `
# Embed partial
@@ -126,27 +130,48 @@ Some more text.
`.trim()
const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Missing required variable in \$Partial ".*variables\.mdx": "var"/
)
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })
// Note the empty gap where `{{ .var }}` used to be — this is deliberate.
const expected = `
# Embed partial
Here is a partial that takes a .
Some more text.
`.trimStart()
expect(output).toEqual(expected)
// The placeholder must be fully removed, not left as literal `{{ .var }}`.
expect(output).not.toContain('{{')
})
it('should error when unexpected variable is provided', async () => {
it('should ignore a variable that is not referenced in the partial', async () => {
// The variables.mdx fixture only references `var`. Providing an additional
// `extra` variable that the partial never uses is silently ignored rather
// than throwing — `var` is substituted and `extra` leaves no trace.
const markdown = `
# Embed partial
<$Partial path="/_fixtures/variables.mdx" variables={{ "var": "correct", "extra": "unexpected" }} />
<$Partial path="/_fixtures/variables.mdx" variables={{ "var": "correct", "extra": "unused" }} />
Some more text.
`.trim()
const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Unexpected variable in \$Partial ".*variables\.mdx": "extra"/
)
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })
expect(output).toContain('Here is a partial that takes a correct.')
expect(output).not.toContain('unused')
})
it('should error with detailed message for multiple missing variables', async () => {
it('should render only the unprovided variables as empty when some are provided', async () => {
// The multiple-variables.mdx fixture reads:
// "This partial has {{ .var1 }}, {{ .var2 }}, and {{ .var3 }}."
// Only `var1` is provided here, so `var2` and `var3` collapse to empty
// strings while `var1` is substituted normally.
const markdown = `
# Embed partial
@@ -156,24 +181,13 @@ Some more text.
`.trim()
const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Missing required variables.*"var2".*"var3".*Expected variables.*"var1".*"var2".*"var3".*Provided variable: "var1"/s
)
})
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })
it('should error with detailed message for multiple unexpected variables', async () => {
const markdown = `
# Embed partial
<$Partial path="/_fixtures/variables.mdx" variables={{ "var": "correct", "extra1": "wrong", "extra2": "also wrong" }} />
Some more text.
`.trim()
const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Unexpected variables.*"extra1".*"extra2".*Expected variable: "var".*Provided variables.*"var".*"extra1".*"extra2"/s
)
// Provided variable is substituted; the two unprovided ones leave empty gaps.
expect(output).toContain('This partial has value1, , and .')
// No placeholder text survives for the unprovided variables.
expect(output).not.toContain('{{')
})
it('should succeed when all variables match exactly', async () => {
@@ -212,7 +226,12 @@ Some more text.
expect(output).toContain('alphanumeric value')
})
it('should error when hyphenated variable is missing', async () => {
it('should render unprovided hyphenated variables as empty', async () => {
// The hyphenated-variables.mdx fixture reads:
// "This partial has {{ .my-var }}, {{ .another_var }}, and {{ .myVar123 }}."
// Only `my-var` is provided, so the underscore and alphanumeric variables
// collapse to empty strings — confirming the empty-substitution behavior
// applies to all supported variable name styles.
const markdown = `
# Embed partial
@@ -222,8 +241,10 @@ Some more text.
`.trim()
const mdast = fromDocsMarkdown(markdown)
await expect(partialsRemark()(mdast)).rejects.toThrowError(
/Missing required variables.*"another_var".*"myVar123".*Expected variables.*"my-var".*"another_var".*"myVar123".*Provided variable: "my-var"/s
)
const transformed = await partialsRemark()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown()] })
expect(output).toContain('This partial has value, , and .')
expect(output).not.toContain('{{')
})
})
+24 -76
View File
@@ -7,6 +7,10 @@
* Simple string replacement is supported. The replacement strings are
* specified using the `variables` field.
*
* Variable substitution is optional. Any variable referenced in the partial
* content but not provided is rendered as an empty string, and any variable
* provided but not referenced in the content is ignored.
*
* ## Examples
*
* ### Simple partial
@@ -33,14 +37,14 @@
* ```
*/
import { type Root } from 'mdast'
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { PARTIALS_DIRECTORY } from '~/lib/docs'
import { type Root } from 'mdast'
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
import { type Parent } from 'unist'
import { visitParents } from 'unist-util-visit-parents'
import { PARTIALS_DIRECTORY } from '~/lib/docs'
import { fromDocsMarkdown, getAttributeValue, getAttributeValueExpression } from './utils.server'
export function partialsRemark() {
@@ -74,67 +78,19 @@ function toFilePath(node: MdxJsxFlowElement) {
}
/**
* Extracts all variable names expected in the partial content.
* Returns a Set of variable names found in {{ .variableName }} patterns.
* Variable names can contain alphanumeric characters, hyphens, and underscores.
* Substitutes provided variables into the partial content. Variable
* substitution is optional: any variable referenced in the content but not
* provided is replaced with an empty string, and any variable provided but not
* referenced is ignored. The leading `\` escape (`\{{ .var }}`) opts a
* placeholder out of substitution.
*/
function extractExpectedVariables(content: string): Set<string> {
const variablePattern = /(?<!\\)\{\{\s*\.([\w-]+)\s*\}\}/g
const variables = new Set<string>()
let match
while ((match = variablePattern.exec(content)) !== null) {
variables.add(match[1])
}
return variables
}
/**
* Validates that all expected variables are provided and no unexpected variables are included.
* Throws descriptive errors if validation fails.
*/
function validateVariables(
content: string,
vars: Record<string, string> | undefined,
partialPath: string
) {
const expectedVars = extractExpectedVariables(content)
const providedVars = vars ? new Set(Object.keys(vars)) : new Set<string>()
// Check for missing variables
const missingVars = [...expectedVars].filter((v) => !providedVars.has(v))
if (missingVars.length > 0) {
const varList = missingVars.map((v) => `"${v}"`).join(', ')
const plural = missingVars.length > 1
throw new Error(
`Missing required variable${plural ? 's' : ''} in $Partial "${partialPath}": ${varList}\n` +
`Expected variable${expectedVars.size > 1 ? 's' : ''}: ${[...expectedVars].map((v) => `"${v}"`).join(', ')}\n` +
`Provided variable${providedVars.size !== 1 ? 's' : ''}: ${providedVars.size > 0 ? [...providedVars].map((v) => `"${v}"`).join(', ') : 'none'}`
)
}
// Check for unexpected variables
const unexpectedVars = [...providedVars].filter((v) => !expectedVars.has(v))
if (unexpectedVars.length > 0) {
const varList = unexpectedVars.map((v) => `"${v}"`).join(', ')
const plural = unexpectedVars.length > 1
throw new Error(
`Unexpected variable${plural ? 's' : ''} in $Partial "${partialPath}": ${varList}\n` +
`Expected variable${expectedVars.size !== 1 ? 's' : ''}: ${expectedVars.size > 0 ? [...expectedVars].map((v) => `"${v}"`).join(', ') : 'none'}\n` +
`Provided variable${plural ? 's' : ''}: ${[...providedVars].map((v) => `"${v}"`).join(', ')}`
)
}
}
function substituteVars(content: string, vars: Record<string, string> | undefined) {
if (vars === undefined) {
return content
}
for (const [key, value] of Object.entries(vars)) {
for (const [key, value] of Object.entries(vars ?? {})) {
content = content.replace(new RegExp(`(?<!\\\\)\\{\\{\\s*\\.${key}\\s*\\}\\}`, 'g'), value)
}
// Clear any remaining (unprovided) placeholders.
content = content.replace(/(?<!\\)\{\{\s*\.[\w-]+\s*\}\}/g, '')
return content
}
@@ -159,12 +115,7 @@ function getVariables(node: MdxJsxFlowElement): undefined | Record<string, strin
async function fetchPartialsContent(tree: Root) {
// INVARIANT: These must be pushed to in the same order because the index is // used to keep track of the relationship.
const partialNodes = [] as [
Parent,
MdxJsxFlowElement,
undefined | Record<string, string>,
string,
][]
const partialNodes = [] as [Parent, MdxJsxFlowElement, undefined | Record<string, string>][]
const pendingFetches = [] as Promise<string>[]
visitParents(tree, 'mdxJsxFlowElement', (node: MdxJsxFlowElement, ancestors) => {
@@ -174,9 +125,8 @@ async function fetchPartialsContent(tree: Root) {
const filePath = toFilePath(node)
const variables = getVariables(node)
const fetchTask = readFile(filePath, 'utf-8')
const partialPath = getAttributeValue(node, 'path') as string
partialNodes.push([parent, node, variables, partialPath])
partialNodes.push([parent, node, variables])
pendingFetches.push(fetchTask)
})
@@ -184,21 +134,19 @@ async function fetchPartialsContent(tree: Root) {
const nodeContentMap = new Map<
MdxJsxFlowElement,
[Parent, string, undefined | Record<string, string>, string]
[Parent, string, undefined | Record<string, string>]
>()
partialNodes.forEach(([parent, node, variables, partialPath], index) => {
nodeContentMap.set(node, [parent, resolvedContent[index], variables, partialPath])
partialNodes.forEach(([parent, node, variables], index) => {
nodeContentMap.set(node, [parent, resolvedContent[index], variables])
})
return nodeContentMap
}
function rewriteNodes(
contentMap: Map<MdxJsxFlowElement, [Parent, string, undefined | Record<string, string>, string]>
contentMap: Map<MdxJsxFlowElement, [Parent, string, undefined | Record<string, string>]>
) {
for (const [node, [parent, rawContent, vars, partialPath]] of contentMap) {
const trimmedContent = rawContent.trim()
validateVariables(trimmedContent, vars, partialPath)
let content = substituteVars(trimmedContent, vars)
for (const [node, [parent, rawContent, vars]] of contentMap) {
const content = substituteVars(rawContent.trim(), vars)
const replacementContent = fromDocsMarkdown(content)
parent.children.splice(parent.children.indexOf(node), 1, replacementContent)
}
@@ -17,7 +17,8 @@ import selfHostingRealtimeCommonSections from '~/spec/common-self-hosting-realti
import selfHostingStorageCommonSections from '~/spec/common-self-hosting-storage-sections.json' with { type: 'json' }
import storageSpec from '~/spec/storage_v0_openapi.json' with { type: 'json' }
import analyticsSpec from '~/spec/transforms/analytics_v0_openapi_deparsed.json' with { type: 'json' }
import openApiSpec from '~/spec/transforms/api_v1_openapi_deparsed.json' with { type: 'json' }
import apiV1Spec from '~/spec/transforms/api_v1_openapi_deparsed.json' with { type: 'json' }
import apiV2Spec from '~/spec/transforms/api_v2_openapi_deparsed.json' with { type: 'json' }
import { isPlainObject, keyBy } from 'lodash-es'
import slugify from 'slugify'
import { parse } from 'yaml'
@@ -239,7 +240,25 @@ async function writeCliReferenceSections() {
}
async function writeApiReferenceSections() {
const endpointsById = mapEndpointsById(openApiSpec)
const mergedSpec = {
...apiV1Spec,
paths: {
...apiV1Spec.paths,
...apiV2Spec.paths,
},
components: {
...apiV1Spec.components,
schemas: {
...apiV1Spec.components?.schemas,
...apiV2Spec.components?.schemas,
},
securitySchemes: {
...apiV1Spec.components?.securitySchemes,
...apiV2Spec.components?.securitySchemes,
},
},
}
const endpointsById = mapEndpointsById(mergedSpec)
const pendingEndpointsByIdWrite = writeFile(
join(GENERATED_DIRECTORY, 'api.latest.endpointsById.json'),
JSON.stringify(Array.from(endpointsById.entries()))
+1 -1
View File
@@ -31,7 +31,7 @@
"postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:references:new && pnpm run codegen:examples && pnpm run build:guides-markdown && pnpm run build:gz-archive",
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:references:new && pnpm run codegen:examples",
"preembeddings": "pnpm run codegen:references",
"preembeddings": "pnpm run codegen:references && pnpm run codegen:references:new",
"preinstall": "npx only-allow pnpm",
"presync": "pnpm run codegen:graphql",
"pretest": "pnpm run codegen:examples",
-2
View File
@@ -9,7 +9,6 @@ Adam Mokan
AJ Matias
Akash Manimaran
Alaister Young
Alan De Los Santos
Aleksi Immonen
Alex Hall
Alex Hsu
@@ -93,7 +92,6 @@ Emmett Folger
Eric Kharitonashvili
Etienne Stalmans
Eyal Ehrlich
Fabrizio Cataldo
Fabrizio Fenoglio
Fady A
Fatuma Abdullahi
+6 -2
View File
@@ -13,7 +13,7 @@ download: download.api.v1 download.storage.v1 download.tsdoc.v2
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
# This flow needs to be updated, so we'l comment out for the moment
# Manual flow for now:
@@ -58,6 +58,7 @@ transform: dereference.api.v1 dereference.auth.v1 dereference.storage.v0
dereference.api.v1:
pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/api_v1_openapi.json
pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/api_v2_openapi_deparsed.json $(REPO_DIR)/api_v2_openapi.json
dereference.auth.v1:
pnpm exec redocly bundle --dereferenced -o $(REPO_DIR)/transforms/auth_v1_openapi_deparsed.json $(REPO_DIR)/auth_v1_openapi.json
@@ -74,7 +75,10 @@ dereference.analytics.v0:
generate: generate.sections.api.v1
generate.sections.api.v1:
npx tsx $(REPO_DIR)/sections/generateMgmtApiSections.cts $(REPO_DIR)/transforms/api_v1_openapi_deparsed.json $(REPO_DIR)/common-api-sections.json
npx tsx $(REPO_DIR)/sections/generateMgmtApiSections.cts \
$(REPO_DIR)/transforms/api_v1_openapi_deparsed.json \
$(REPO_DIR)/transforms/api_v2_openapi_deparsed.json \
$(REPO_DIR)/common-api-sections.json
###############################################################################
# Validate OpenAPI 3.0
+899
View File
@@ -0,0 +1,899 @@
{
"openapi": "3.0.0",
"paths": {
"/v2/projects/{ref}/analytics/log-drains": {
"get": {
"operationId": "v2-list-log-drains",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"example": "abcdefghijklmnopqrst",
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ListLogDrainsResponse" }
}
}
},
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" },
"500": { "description": "Failed to fetch log drains" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["analytics_config_read"] }],
"summary": "List project log drains",
"tags": ["Analytics"],
"x-badges": [{ "name": "OAuth scope: analytics_config:read", "position": "after" }],
"x-endpoint-owners": ["analytics"],
"x-oauth-scope": "analytics_config:read"
},
"post": {
"operationId": "v2-create-log-drain",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"example": "abcdefghijklmnopqrst",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/CreateLogDrainRequestOpenApi" }
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/LogDrainResponse" } }
}
},
"401": { "description": "Unauthorized" },
"402": {
"description": "This feature requires the Pro, Team, or Enterprise organization plan."
},
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" },
"500": { "description": "Failed to create a log drain" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["analytics_config_write"] }],
"summary": "Create a log drain for a project",
"tags": ["Analytics"],
"x-allowed-plans": ["Pro", "Team", "Enterprise"],
"x-badges": [
{ "name": "Only available on Pro, Team, Enterprise", "position": "before" },
{ "name": "OAuth scope: analytics_config:write", "position": "after" }
],
"x-endpoint-owners": ["analytics"],
"x-oauth-scope": "analytics_config:write"
}
},
"/v2/projects/{ref}/analytics/log-drains/{id}": {
"put": {
"operationId": "v2-update-log-drain",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"example": "abcdefghijklmnopqrst",
"type": "string"
}
},
{
"name": "id",
"required": true,
"in": "path",
"description": "Log drains identifier",
"schema": { "format": "uuid", "type": "string" }
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UpdateLogDrainRequestOpenApi" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/LogDrainResponse" } }
}
},
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" },
"500": { "description": "Failed to update log drain" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["analytics_config_write"] }],
"summary": "Update a project log drain",
"tags": ["Analytics"],
"x-badges": [{ "name": "OAuth scope: analytics_config:write", "position": "after" }],
"x-endpoint-owners": ["analytics"],
"x-oauth-scope": "analytics_config:write"
},
"delete": {
"operationId": "v2-delete-log-drain",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"example": "abcdefghijklmnopqrst",
"type": "string"
}
},
{
"name": "id",
"required": true,
"in": "path",
"description": "Log drains identifier",
"schema": { "format": "uuid", "type": "string" }
}
],
"responses": {
"204": { "description": "" },
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" },
"500": { "description": "Failed to delete a log drain" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["analytics_config_write"] }],
"summary": "Delete a project log drain",
"tags": ["Analytics"],
"x-badges": [{ "name": "OAuth scope: analytics_config:write", "position": "after" }],
"x-endpoint-owners": ["analytics"],
"x-oauth-scope": "analytics_config:write"
}
},
"/v2/projects/{ref}/transfers/previews": {
"post": {
"operationId": "v2-preview-a-project-transfer",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"example": "abcdefghijklmnopqrst",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/V2TransferProjectBody" }
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/V2PreviewProjectTransferResponse" }
}
}
},
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["project_admin_read"] }],
"summary": "Previews transferring a project to a different organizations, shows eligibility and impact",
"tags": ["Projects"],
"x-endpoint-owners": ["management-api"]
}
},
"/v2/projects/{ref}/transfers": {
"post": {
"operationId": "v2-transfer-a-project",
"parameters": [
{
"name": "ref",
"required": true,
"in": "path",
"description": "Project ref",
"schema": {
"minLength": 20,
"maxLength": 20,
"pattern": "^[a-z]+$",
"example": "abcdefghijklmnopqrst",
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/V2TransferProjectBody" }
}
}
},
"responses": {
"200": { "description": "" },
"401": { "description": "Unauthorized" },
"403": { "description": "Forbidden action" },
"429": { "description": "Rate limit exceeded" }
},
"security": [{ "bearer": [] }, { "fga_permissions": ["organization_admin_write"] }],
"summary": "Transfers a project to a different organization",
"tags": ["Projects"],
"x-endpoint-owners": ["management-api"]
}
}
},
"info": {
"title": "Supabase API (v2)",
"description": "Supabase API generated from the OpenAPI specification.<br>Visit [https://supabase.com/docs](https://supabase.com/docs) for a complete documentation.",
"version": "1.0.0",
"contact": {}
},
"tags": [],
"servers": [],
"components": {
"securitySchemes": { "bearer": { "scheme": "bearer", "bearerFormat": "JWT", "type": "http" } },
"schemas": {
"ListLogDrainsResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["log_drain"],
"description": "Resource type."
},
"id": { "type": "string" },
"attributes": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"config": {
"oneOf": [
{
"type": "object",
"properties": {
"url": { "type": "string", "nullable": true },
"schema": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"port": { "type": "number", "nullable": true },
"hostname": { "type": "string" }
},
"additionalProperties": false,
"title": "postgres"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"http": { "type": "string", "enum": ["http1", "http2"] },
"gzip": { "type": "boolean" },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "webhook"
},
{
"type": "object",
"properties": {
"project_id": { "type": "string" },
"dataset_id": { "type": "string" }
},
"additionalProperties": false,
"title": "bigquery"
},
{
"type": "object",
"properties": {
"api_key": { "type": "string" },
"region": { "type": "string" }
},
"additionalProperties": false,
"title": "datadog"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "loki"
},
{
"type": "object",
"properties": { "dsn": { "type": "string" } },
"additionalProperties": false,
"title": "sentry"
},
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"api_token": { "type": "string" },
"dataset_name": { "type": "string" }
},
"additionalProperties": false,
"title": "axiom"
},
{
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
"tls": { "default": false, "type": "boolean" },
"structured_data": { "type": "string" },
"cipher_key": { "type": "string" },
"ca_cert": { "type": "string" },
"client_cert": { "type": "string" },
"client_key": { "type": "string" }
},
"additionalProperties": false,
"title": "syslog"
}
]
},
"backend_type": {
"type": "string",
"enum": [
"postgres",
"bigquery",
"clickhouse",
"webhook",
"datadog",
"loki",
"sentry",
"s3",
"axiom",
"last9",
"otlp",
"syslog"
]
}
},
"required": ["name", "config", "backend_type"]
}
},
"required": ["type", "id", "attributes"]
}
}
},
"required": ["data"]
},
"CreateLogDrainRequestOpenApi": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["log_drain"], "description": "Resource type." },
"attributes": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"config": {
"oneOf": [
{
"type": "object",
"properties": {
"url": { "type": "string", "nullable": true },
"schema": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"port": { "type": "number", "nullable": true },
"hostname": { "type": "string" }
},
"additionalProperties": false,
"title": "postgres"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"http": { "type": "string", "enum": ["http1", "http2"] },
"gzip": { "type": "boolean" },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "webhook"
},
{
"type": "object",
"properties": {
"project_id": { "type": "string" },
"dataset_id": { "type": "string" }
},
"additionalProperties": false,
"title": "bigquery"
},
{
"type": "object",
"properties": {
"api_key": { "type": "string" },
"region": { "type": "string" }
},
"additionalProperties": false,
"title": "datadog"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "loki"
},
{
"type": "object",
"properties": { "dsn": { "type": "string" } },
"additionalProperties": false,
"title": "sentry"
},
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"api_token": { "type": "string" },
"dataset_name": { "type": "string" }
},
"additionalProperties": false,
"title": "axiom"
},
{
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
"tls": { "default": false, "type": "boolean" },
"structured_data": { "type": "string" },
"cipher_key": { "type": "string" },
"ca_cert": { "type": "string" },
"client_cert": { "type": "string" },
"client_key": { "type": "string" }
},
"additionalProperties": false,
"title": "syslog"
}
]
},
"backend_type": {
"type": "string",
"enum": [
"postgres",
"bigquery",
"clickhouse",
"webhook",
"datadog",
"loki",
"sentry",
"s3",
"axiom",
"last9",
"otlp",
"syslog"
]
}
},
"required": ["name", "config", "backend_type"]
}
},
"required": ["type", "attributes"]
}
},
"required": ["data"]
},
"LogDrainResponse": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["log_drain"], "description": "Resource type." },
"id": { "type": "string" },
"attributes": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"config": {
"oneOf": [
{
"type": "object",
"properties": {
"url": { "type": "string", "nullable": true },
"schema": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"port": { "type": "number", "nullable": true },
"hostname": { "type": "string" }
},
"additionalProperties": false,
"title": "postgres"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"http": { "type": "string", "enum": ["http1", "http2"] },
"gzip": { "type": "boolean" },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "webhook"
},
{
"type": "object",
"properties": {
"project_id": { "type": "string" },
"dataset_id": { "type": "string" }
},
"additionalProperties": false,
"title": "bigquery"
},
{
"type": "object",
"properties": {
"api_key": { "type": "string" },
"region": { "type": "string" }
},
"additionalProperties": false,
"title": "datadog"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "loki"
},
{
"type": "object",
"properties": { "dsn": { "type": "string" } },
"additionalProperties": false,
"title": "sentry"
},
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"api_token": { "type": "string" },
"dataset_name": { "type": "string" }
},
"additionalProperties": false,
"title": "axiom"
},
{
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
"tls": { "default": false, "type": "boolean" },
"structured_data": { "type": "string" },
"cipher_key": { "type": "string" },
"ca_cert": { "type": "string" },
"client_cert": { "type": "string" },
"client_key": { "type": "string" }
},
"additionalProperties": false,
"title": "syslog"
}
]
},
"backend_type": {
"type": "string",
"enum": [
"postgres",
"bigquery",
"clickhouse",
"webhook",
"datadog",
"loki",
"sentry",
"s3",
"axiom",
"last9",
"otlp",
"syslog"
]
}
},
"required": ["name", "config", "backend_type"]
}
},
"required": ["type", "id", "attributes"]
}
},
"required": ["data"]
},
"UpdateLogDrainRequestOpenApi": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["log_drain"], "description": "Resource type." },
"attributes": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"config": {
"oneOf": [
{
"type": "object",
"properties": {
"url": { "type": "string", "nullable": true },
"schema": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"port": { "type": "number", "nullable": true },
"hostname": { "type": "string" }
},
"additionalProperties": false,
"title": "postgres"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"http": { "type": "string", "enum": ["http1", "http2"] },
"gzip": { "type": "boolean" },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "webhook"
},
{
"type": "object",
"properties": {
"project_id": { "type": "string" },
"dataset_id": { "type": "string" }
},
"additionalProperties": false,
"title": "bigquery"
},
{
"type": "object",
"properties": {
"api_key": { "type": "string" },
"region": { "type": "string" }
},
"additionalProperties": false,
"title": "datadog"
},
{
"type": "object",
"properties": {
"url": { "type": "string" },
"username": { "type": "string", "nullable": true },
"password": { "type": "string", "nullable": true },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": false,
"title": "loki"
},
{
"type": "object",
"properties": { "dsn": { "type": "string" } },
"additionalProperties": false,
"title": "sentry"
},
{
"type": "object",
"properties": {
"domain": { "type": "string" },
"api_token": { "type": "string" },
"dataset_name": { "type": "string" }
},
"additionalProperties": false,
"title": "axiom"
},
{
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer", "minimum": 0, "maximum": 65535 },
"tls": { "default": false, "type": "boolean" },
"structured_data": { "type": "string" },
"cipher_key": { "type": "string" },
"ca_cert": { "type": "string" },
"client_cert": { "type": "string" },
"client_key": { "type": "string" }
},
"additionalProperties": false,
"title": "syslog"
}
]
},
"backend_type": {
"type": "string",
"enum": [
"postgres",
"bigquery",
"clickhouse",
"webhook",
"datadog",
"loki",
"sentry",
"s3",
"axiom",
"last9",
"otlp",
"syslog"
]
}
},
"required": ["backend_type"]
}
},
"required": ["type", "attributes"]
}
},
"required": ["data"]
},
"V2TransferProjectBody": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["project_transfer_input"],
"description": "Resource type."
},
"attributes": {
"type": "object",
"properties": { "target_organization_slug": { "type": "string" } },
"required": ["target_organization_slug"]
}
},
"required": ["type", "attributes"]
}
},
"required": ["data"]
},
"V2PreviewProjectTransferResponse": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["project_transfer_result"],
"description": "Resource type."
},
"attributes": {
"type": "object",
"properties": {
"valid": { "type": "boolean" },
"warnings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": { "type": "string" },
"message": { "type": "string" }
},
"required": ["key", "message"]
}
},
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": { "type": "string" },
"message": { "type": "string" }
},
"required": ["key", "message"]
}
},
"info": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": { "type": "string" },
"message": { "type": "string" }
},
"required": ["key", "message"]
}
}
},
"required": ["valid", "warnings", "errors", "info"]
}
},
"required": ["type", "attributes"]
}
},
"required": ["data"]
}
}
}
}
+36
View File
@@ -28,6 +28,18 @@
"type": "category",
"title": "Analytics",
"items": [
{
"id": "v2-create-log-drain",
"title": "Create log drain",
"slug": "v2-create-log-drain",
"type": "operation"
},
{
"id": "v2-delete-log-drain",
"title": "Delete log drain",
"slug": "v2-delete-log-drain",
"type": "operation"
},
{
"id": "v1-get-project-function-combined-stats",
"title": "Get project function combined stats",
@@ -51,6 +63,18 @@
"title": "Get project usage request count",
"slug": "v1-get-project-usage-request-count",
"type": "operation"
},
{
"id": "v2-list-log-drains",
"title": "List log drains",
"slug": "v2-list-log-drains",
"type": "operation"
},
{
"id": "v2-update-log-drain",
"title": "Update log drain",
"slug": "v2-update-log-drain",
"type": "operation"
}
]
},
@@ -856,12 +880,24 @@
"slug": "v1-pause-a-project",
"type": "operation"
},
{
"id": "v2-preview-a-project-transfer",
"title": "Preview a project transfer",
"slug": "v2-preview-a-project-transfer",
"type": "operation"
},
{
"id": "v1-restore-a-project",
"title": "Restore a project",
"slug": "v1-restore-a-project",
"type": "operation"
},
{
"id": "v2-transfer-a-project",
"title": "Transfer a project",
"slug": "v2-transfer-a-project",
"type": "operation"
},
{
"id": "v1-update-a-project",
"title": "Update a project",
@@ -3,7 +3,6 @@ import path from 'path'
function slugToTitle(slug) {
if (!slug) return ''
// remove version prefix if available
const prefixRegex = /^v\d+/
const title = slug.replace(prefixRegex, '').replace(/-/g, ' ').trimStart()
return title.charAt(0).toUpperCase() + title.slice(1)
@@ -14,100 +13,83 @@ function isValidSlug(slug) {
return slugRegex.test(slug)
}
function extractSectionsFromOpenApi(filePath, outputPath) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(`Error reading file ${filePath}:`, err)
return
}
function readJson(filePath: string) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
}
function extractSectionsFromOpenApi(filePaths: string[], outputPath: string) {
try {
// Merge paths from all specs, later specs override earlier ones on conflict
const mergedPaths = Object.assign({}, ...filePaths.map((p) => readJson(p).paths ?? {}))
try {
const openApiJson = JSON.parse(data)
const categories: string[] = []
const sections: Array<{
const categories: string[] = []
const sections: Array<{
type: string
title: string
id?: string
slug?: string
items: Array<{
type: string
title: string
id?: string
slug?: string
items: Array<{
type: string
title: string
id: string
slug: string
}>
}> = []
id: string
slug: string
}>
}> = []
if (openApiJson.paths) {
for (const route in openApiJson.paths) {
const methods = openApiJson.paths[route]
for (const method in methods) {
// We are using `x-internal` to hide endpoints from the docs,
// but still have them included in the spec so they generate types and can be used.
if (methods[method]['x-internal']) {
continue
}
const tag = methods[method].tags?.[0]
const operationId = methods[method].operationId
// If operationId is not in the form of a slug ignore it.
// This is intentional because operationId is not defined under the swagger
// spec and is extracted automatically from the function name.
if (!tag || !isValidSlug(operationId)) continue
if (!categories.includes(tag)) {
categories.push(tag)
sections.push({
type: 'category',
title: tag,
items: [],
})
}
const sectionCate = sections.find((i) => i.title === tag)
sectionCate?.items.push({
id: operationId,
title: slugToTitle(operationId),
slug: operationId,
type: 'operation',
})
}
for (const route in mergedPaths) {
const methods = mergedPaths[route]
for (const method in methods) {
if (methods[method]['x-internal']) {
continue
}
const tag = methods[method].tags?.[0]
const operationId = methods[method].operationId
if (!tag || !isValidSlug(operationId)) continue
if (!categories.includes(tag)) {
categories.push(tag)
sections.push({
type: 'category',
title: tag,
items: [],
})
}
const sectionCate = sections.find((i) => i.title === tag)
sectionCate?.items.push({
id: operationId,
title: slugToTitle(operationId),
slug: operationId,
type: 'operation',
})
}
}
// finalize sections
sections.sort((a, b) => a.title.localeCompare(b.title))
sections.forEach((i) => i.items.sort((a, b) => a.title.localeCompare(b.title)))
sections.unshift({
title: 'Introduction',
id: 'introduction',
slug: 'introduction',
type: 'markdown',
items: [],
})
sections.sort((a, b) => a.title.localeCompare(b.title))
sections.forEach((i) => i.items.sort((a, b) => a.title.localeCompare(b.title)))
sections.unshift({
title: 'Introduction',
id: 'introduction',
slug: 'introduction',
type: 'markdown',
items: [],
})
fs.writeFile(outputPath, JSON.stringify(sections, null, 2), 'utf8', (err) => {
if (err) {
console.error(`Error writing to file ${outputPath}:`, err)
return
}
console.log(`Sections successfully generated!!!`)
})
} catch (error) {
console.error('Error parsing JSON:', error)
}
})
fs.writeFileSync(outputPath, JSON.stringify(sections, null, 2), 'utf8')
console.log(`Sections successfully generated!!!`)
} catch (error) {
console.error('Error:', error)
}
}
// Get file paths from command line arguments
const args = process.argv.slice(2)
if (args.length < 2) {
console.error('Please provide the openapi file path and output file path as arguments.')
console.error(
'Please provide at least one openapi file path and an output file path as arguments.'
)
process.exit(1)
}
const inputFilePath = path.resolve(args[0])
const outputFilePath = path.resolve(args[1])
// Last arg is output, everything before is input files
const outputFilePath = path.resolve(args[args.length - 1])
const inputFilePaths = args.slice(0, -1).map((p) => path.resolve(p))
;(async () => {
extractSectionsFromOpenApi(inputFilePath, outputFilePath)
})()
extractSectionsFromOpenApi(inputFilePaths, outputFilePath)
File diff suppressed because it is too large. Load diff
@@ -401,7 +401,11 @@ export const Grid = memo(
)}
<DataGrid
ref={ref}
className={cn(gridClass, 'grow', isContextMenuOpen && 'rdg-context-menu-open')}
className={cn(
gridClass,
'grow border-t-default! border-b-0!',
isContextMenuOpen && 'rdg-context-menu-open'
)}
rowClass={computedRowClass}
columns={columnsWithDirtyCellClass}
rows={rows ?? []}
@@ -1,7 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
@@ -17,13 +16,14 @@ import {
Input,
Switch,
} from 'ui'
import { PageSection, PageSectionContent } from 'ui-patterns'
import { Admonition } from 'ui-patterns/admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
import { OAuthEndpointsTable } from './OAuthEndpointsTable'
import { InlineLink } from '@/components/ui/InlineLink'
import NoPermission from '@/components/ui/NoPermission'
import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
@@ -32,10 +32,6 @@ import { useOAuthServerAppsQuery } from '@/data/oauth-server-apps/oauth-server-a
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { DOCS_URL } from '@/lib/constants'
const OAuthEndpointsTable = dynamic(() =>
import('./OAuthEndpointsTable').then((mod) => ({ default: mod.OAuthEndpointsTable }))
)
const configUrlSchema = z.object({
id: z.string(),
name: z.string(),
@@ -81,7 +81,7 @@ export const useAvailableIntegrations = () => {
edge_function_secret_name: edgeFunctionSecretName,
images,
content,
partner_name: authorName,
built_by: authorName,
listing_logo: listingLogo,
} = integration
@@ -175,7 +175,7 @@ export const useAvailableIntegrations = () => {
documentation_url: docsUrl,
website_url: siteUrl,
images,
partner_name: authorName,
built_by: authorName,
listing_logo: listingLogo,
} = marketplaceWrapper
@@ -1427,7 +1427,7 @@ export const WRAPPERS: WrapperMeta[] = [
description: 'Cloud storage service for high-dimensional vectors',
extensionName: 'S3VectorsFdw',
label: 'S3 Vectors',
docsUrl: `${DOCS_URL}/guides/database/extensions/wrappers/s3-vectors`,
docsUrl: `${DOCS_URL}/guides/database/extensions/wrappers/s3_vectors`,
categories: ['ai_vectors', 'storage'],
minimumExtensionVersion: '0.5.6',
server: {
@@ -0,0 +1,34 @@
import { Column } from 'react-data-grid'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import type { LogData } from '../Logs.types'
import { parseMultigresEventMessage } from '../Logs.utils'
import { RowLayout, SeverityFormatter, TextFormatter } from '../LogsFormatters'
import { defaultRenderCell } from './DefaultPreviewColumnRenderer'
const columns: Column<LogData>[] = [
{
name: 'multigres-first-column',
key: 'multigres-first-column',
renderHeaderCell: () => null,
renderCell: (props) => {
const parsed = parseMultigresEventMessage(props.row.event_message)
const level = typeof parsed?.level === 'string' ? parsed.level : undefined
const msg = typeof parsed?.msg === 'string' ? parsed.msg : undefined
if (!level && !msg) {
return defaultRenderCell(props)
}
return (
<RowLayout>
{props.row.timestamp && <TimestampInfo utcTimestamp={props.row.timestamp} />}
{level && <SeverityFormatter value={level} />}
<TextFormatter className="w-full" value={msg ?? props.row.event_message} />
</RowLayout>
)
},
},
]
export default columns
@@ -12,8 +12,8 @@ import {
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import type { LogData, QueryType } from './Logs.types'
import { apiKey, role as extractRole, jwtAPIKey } from './Logs.utils'
import type { LogData, PreviewLogData, QueryType } from './Logs.types'
import { apiKey, role as extractRole, jwtAPIKey, parseMultigresEventMessage } from './Logs.utils'
import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
@@ -70,6 +70,16 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio
return <DefaultPreviewSelectionRenderer log={apiLog} />
case 'multigres': {
const parsedMultigresMessage = parseMultigresEventMessage(log.event_message)
// Spread the log last so its canonical fields (id, timestamp, event_message)
// always win over any same-named keys inside the parsed event_message.
const multigresLog = (
parsedMultigresMessage ? { ...parsedMultigresMessage, ...log } : log
) as PreviewLogData
return <DefaultPreviewSelectionRenderer log={multigresLog} />
}
case 'database':
const hint = log?.metadata?.[0]?.parsed?.[0]?.hint
const detail = log?.metadata?.[0]?.parsed?.[0]?.detail
@@ -24,6 +24,7 @@ import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresC
import DefaultPreviewColumnRenderer from './LogColumnRenderers/DefaultPreviewColumnRenderer'
import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender'
import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
import MultigresColumnRender from './LogColumnRenderers/MultigresColumnRender'
import type { LogData, LogQueryError, QueryType } from './Logs.types'
import {
formatLogsAsCsv,
@@ -265,6 +266,9 @@ export const LogTable = ({
case 'pg_cron':
columns = DatabasePostgresColumnRender
break
case 'multigres':
columns = MultigresColumnRender
break
default:
if (firstRow && isDefaultLogPreviewFormat(firstRow)) {
columns = DefaultPreviewColumnRenderer
@@ -396,6 +396,7 @@ export enum LogsTableName {
PG_UPGRADE = 'pg_upgrade_logs',
PG_CRON = 'pg_cron_logs',
ETL = 'etl_replication_logs',
MULTIGRES = 'multigres_logs',
}
export const LOGS_TABLES = {
@@ -412,6 +413,7 @@ export const LOGS_TABLES = {
pg_cron: LogsTableName.POSTGRES,
pgbouncer: LogsTableName.PGBOUNCER,
etl: LogsTableName.ETL,
multigres: LogsTableName.MULTIGRES,
}
export const LOGS_SOURCE_DESCRIPTION = {
@@ -429,6 +431,7 @@ export const LOGS_SOURCE_DESCRIPTION = {
[LogsTableName.PG_UPGRADE]: 'Logs generated by the Postgres version upgrade process',
[LogsTableName.PG_CRON]: 'Postgres logs from pg_cron cron jobs',
[LogsTableName.ETL]: 'Logs from the replication process',
[LogsTableName.MULTIGRES]: 'Logs from the Multigres high availability service',
}
export const FILTER_OPTIONS: FilterTableSet = {
@@ -96,6 +96,7 @@ export type QueryType =
| 'pg_cron'
| 'pgbouncer'
| 'etl'
| 'multigres'
export type Mode = 'simple' | 'custom'
@@ -7,6 +7,7 @@ import {
formatLogsAsCsv,
formatLogsAsJson,
formatLogsAsMarkdown,
parseMultigresEventMessage,
} from './Logs.utils'
const createLog = (overrides: Partial<LogData> = {}): LogData => ({
@@ -182,4 +183,40 @@ describe('Logs.utils', () => {
expect(extractEdgeFunctionName('/functions/v1/hello-world-1/')).toBe('hello-world-1')
})
})
describe('parseMultigresEventMessage', () => {
test('parses a JSON object event_message into a plain object', () => {
const eventMessage = JSON.stringify({
time: '2026-06-02T15:44:52.84043038Z',
level: 'ERROR',
msg: 'Failed to write heartbeat',
error: 'context deadline exceeded',
})
expect(parseMultigresEventMessage(eventMessage)).toEqual({
time: '2026-06-02T15:44:52.84043038Z',
level: 'ERROR',
msg: 'Failed to write heartbeat',
error: 'context deadline exceeded',
})
})
test('returns null when event_message is not valid JSON', () => {
expect(parseMultigresEventMessage('connection closed')).toBeNull()
})
test('returns null when event_message parses to an array', () => {
expect(parseMultigresEventMessage('[1, 2, 3]')).toBeNull()
})
test('returns null when event_message parses to a primitive', () => {
expect(parseMultigresEventMessage('42')).toBeNull()
expect(parseMultigresEventMessage('"a string"')).toBeNull()
})
test('returns null for non-string input', () => {
expect(parseMultigresEventMessage(undefined)).toBeNull()
expect(parseMultigresEventMessage(null)).toBeNull()
expect(parseMultigresEventMessage(42)).toBeNull()
})
})
})
@@ -303,13 +303,34 @@ export const LOG_TABLE_SQL: Record<LogsTableName, SafeLogSqlFragment> = {
[LogsTableName.PG_UPGRADE]: safeSql`pg_upgrade_logs`,
[LogsTableName.PG_CRON]: safeSql`pg_cron_logs`,
[LogsTableName.ETL]: safeSql`etl_replication_logs`,
[LogsTableName.MULTIGRES]: safeSql`multigres_logs`,
}
/**
* SQL query to retrieve only one log
*/
export const genSingleLogQuery = (table: LogsTableName, id: string): SafeLogSqlFragment =>
safeSql`select id, timestamp, event_message, metadata from ${LOG_TABLE_SQL[table]} where id = ${analyticsLiteral(id)} limit 1`
export const genSingleLogQuery = (table: LogsTableName, id: string): SafeLogSqlFragment => {
// multigres logs have no metadata column
const metadataColumn = table === LogsTableName.MULTIGRES ? safeSql`` : safeSql`, metadata`
return safeSql`select id, timestamp, event_message${metadataColumn} from ${LOG_TABLE_SQL[table]} where id = ${analyticsLiteral(id)} limit 1`
}
/**
* Multigres logs store their structured payload as a JSON string in
* `event_message`. Parse it into a plain object, returning `null` when the
* value is missing, not valid JSON, or not a plain object (e.g. an array).
*/
export const parseMultigresEventMessage = (
eventMessage: unknown
): Record<string, unknown> | null => {
if (typeof eventMessage !== 'string') return null
try {
const parsed = JSON.parse(eventMessage)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
} catch {
return null
}
}
/**
* Determine if we should show the user an upgrade prompt while browsing logs
@@ -699,6 +720,8 @@ function getErrorCondition(table: LogsTableName): SafeLogSqlFragment {
return safeSql`metadata.level IN ('error', 'fatal')`
case 'pg_cron_logs':
return safeSql`parsed.error_severity IN ('ERROR', 'FATAL', 'PANIC')`
case 'multigres_logs':
return safeSql`JSON_VALUE(event_message, '$.level') IN ('ERROR', 'FATAL', 'PANIC')`
default:
return safeSql`false`
}
@@ -716,6 +739,8 @@ function getWarningCondition(table: LogsTableName): SafeLogSqlFragment {
return safeSql`response.status_code >= 400 AND response.status_code < 500`
case 'function_logs':
return safeSql`metadata.level IN ('warning')`
case 'multigres_logs':
return safeSql`JSON_VALUE(event_message, '$.level') IN ('WARN', 'WARNING')`
default:
return safeSql`false`
}
@@ -830,6 +855,7 @@ const QUERY_TYPE_LABELS: Record<QueryType, string> = {
pg_cron: 'pg_cron',
pgbouncer: 'PgBouncer',
etl: 'ETL',
multigres: 'Multigres',
}
const LOG_TABLE_TO_SERVICE_LABEL: Record<LogsTableName, string> = {
@@ -847,6 +873,7 @@ const LOG_TABLE_TO_SERVICE_LABEL: Record<LogsTableName, string> = {
pg_upgrade_logs: 'Postgres upgrade',
pg_cron_logs: 'pg_cron',
etl_replication_logs: 'ETL',
multigres_logs: 'Multigres',
}
const isLogsTableName = (value: string): value is LogsTableName =>
@@ -185,6 +185,7 @@ export const SeverityFormatter = ({
)
break
case 'WARN':
case 'WARNING':
return (
<Layout className="gap-1">
@@ -38,6 +38,7 @@ import { DatePickerValue, LogsDatePicker } from './Logs.DatePickers'
import { LogsWarning, LogTemplate } from './Logs.types'
import Table from '@/components/to-be-cleaned/Table'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useShowMultigresLogs } from '@/hooks/misc/useShowMultigresLogs'
import { DOCS_URL } from '@/lib/constants'
export interface LogsQueryPanelProps {
@@ -98,7 +99,13 @@ const LogsQueryPanel = ({
}, [value.from, value.to, value.text, value.isHelper])
const [open, setOpen] = useState(false)
const [selectedSchema, setSelectedSchema] = useState(logConstants.schemas[0])
const showMultigresLogs = useShowMultigresLogs()
const schemas = logConstants.schemas.filter(
(schema) => schema.reference !== 'multigres_logs' || showMultigresLogs
)
const [selectedSchema, setSelectedSchema] = useState(schemas[0])
return (
<div className="flex items-center border-b bg-surface-100 h-(--header-height)">
@@ -285,7 +292,7 @@ const LogsQueryPanel = ({
<CommandList>
<CommandEmpty>No source found.</CommandEmpty>
<CommandGroup>
{logConstants.schemas.map((schema) => (
{schemas.map((schema) => (
<CommandItem
key={schema.reference}
value={schema.reference}
@@ -209,7 +209,7 @@ export const CreateVectorBucketDialog = ({
Supabase will install the{' '}
{wrappersExtensionState !== 'installed' ? 'Wrappers extension and ' : ''}
S3 Vectors Wrapper integration on your behalf.{' '}
<InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/s3-vectors`}>
<InlineLink href={`${DOCS_URL}/guides/database/extensions/wrappers/s3_vectors`}>
Learn more
</InlineLink>
.
@@ -34,6 +34,7 @@ import { useContentQuery } from '@/data/content/content-query'
import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useShowMultigresLogs } from '@/hooks/misc/useShowMultigresLogs'
export function SidebarCollapsible({
children,
@@ -100,6 +101,7 @@ export function LogsSidebarMenuV2() {
const showETLLogs = enablePgReplicate && (etlData?.sources?.length ?? 0) > 0 && !isETLLoading
const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
const showMultigresLogs = useShowMultigresLogs()
const { data: savedQueriesRes, isPending: savedQueriesLoading } = useContentQuery({
projectRef: ref,
@@ -193,6 +195,14 @@ export function LogsSidebarMenuV2() {
items: [],
}
: null,
showMultigresLogs
? {
name: 'Multigres',
key: 'multigres-logs',
url: `/project/${ref}/logs/multigres-logs`,
items: [],
}
: null,
].filter((x) => x !== null)
const OPERATIONAL_COLLECTIONS = IS_PLATFORM
@@ -176,11 +176,6 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
[privateSnippetsTreeState]
)
const validExpandedFolderIds = useMemo(
() => expandedFolderIds.filter((id) => privateSnippetsTreeNodeIds.has(id)),
[expandedFolderIds, privateSnippetsTreeNodeIds]
)
const privateSnippetsLastItemIds = useMemo(
() => getLastItemIds(privateSnippetsTreeState),
[privateSnippetsTreeState]
@@ -407,7 +402,11 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
} else if (snippet.visibility === 'user') {
setSectionVisibility({ ...sectionVisibility, private: true })
}
if (snippet.folder_id && !expandedFolderIds.includes(snippet.folder_id)) {
if (
snippet.folder_id &&
!expandedFolderIds.includes(snippet.folder_id) &&
privateSnippetsTreeNodeIds.has(snippet.folder_id)
) {
setExpandedFolderIds([...expandedFolderIds, snippet.folder_id])
}
}
@@ -654,7 +653,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
setExpandedFolderIds(expandedFolderIds.filter((x) => x !== folderId))
}
}}
expandedIds={validExpandedFolderIds}
expandedIds={expandedFolderIds}
nodeRenderer={({ element, ...props }) => {
const isOpened = Object.values(tabs.tabsMap).some(
(tab) => tab.metadata?.sqlId === element.metadata?.id
@@ -15,6 +15,7 @@ import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/L
import { useProjectLintsQuery } from '@/data/lint/lint-query'
import { Notification, useNotificationsV2Query } from '@/data/notifications/notifications-v2-query'
import { useNotificationsV2UpdateMutation } from '@/data/notifications/notifications-v2-update-mutation'
import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
@@ -95,6 +96,18 @@ export const AdvisorPanel = () => {
return notificationsData?.pages.flatMap((page) => page) ?? []
}, [notificationsData?.pages])
const { data: projectsData } = useProjectsInfiniteQuery({}, { enabled: shouldLoadNotifications })
const projectNameByRef = useMemo(() => {
const map = new Map<string, string>()
projectsData?.pages.forEach((page) => {
page.projects.forEach((project) => {
if (project.ref) map.set(project.ref, project.name)
})
})
return map
}, [projectsData?.pages])
const markNotificationsRead = () => {
if (markedRead.current.length > 0) {
updateNotifications({ ids: markedRead.current, status: 'seen' })
@@ -282,6 +295,7 @@ export const AdvisorPanel = () => {
hiddenItemsCount={hiddenItemsCount}
hasAnyFilters={hasAnyFilters}
hasProjectRef={hasProjectRef}
projectNameByRef={projectNameByRef}
/>
</div>
</>
@@ -26,6 +26,7 @@ export type AdvisorLintItem = AdvisorBaseItem & {
export type AdvisorNotificationItem = AdvisorBaseItem & {
source: 'notification'
original: Notification
project_ref?: string
}
export type AdvisorSignalItem = AdvisorBaseItem & {
@@ -71,4 +71,44 @@ describe('AdvisorPanel.utils', () => {
const bannedIpSignal = createBannedIPSignalItem('203.0.113.10')
expect(getAdvisorItemSecondaryText(bannedIpSignal)).toBe('Database · 203.0.113.10')
})
describe('notification secondary text', () => {
const [notificationWithProject] = createAdvisorNotificationItems([
createNotification({
id: 'notification-with-project',
data: {
title: 'CPU usage is high on my-project.',
message: 'Project my-project has high CPU usage.',
project_ref: 'abcd1234',
actions: [],
},
}),
])
const [notificationWithoutProject] = createAdvisorNotificationItems([
createNotification({
id: 'notification-without-project',
data: { title: 'Generic notification', message: 'Body', actions: [] },
}),
])
it('returns the resolved project name when available in the map', () => {
const projectNameByRef = new Map([['abcd1234', 'my-production-db']])
expect(getAdvisorItemSecondaryText(notificationWithProject, projectNameByRef)).toBe(
'my-production-db'
)
})
it('falls back to the project ref when the name is missing from the map', () => {
expect(getAdvisorItemSecondaryText(notificationWithProject, new Map())).toBe('abcd1234')
})
it('falls back to the project ref when no map is provided', () => {
expect(getAdvisorItemSecondaryText(notificationWithProject)).toBe('abcd1234')
})
it('returns undefined for notifications without a project_ref', () => {
expect(getAdvisorItemSecondaryText(notificationWithoutProject)).toBeUndefined()
})
})
})
@@ -83,6 +83,7 @@ export const createAdvisorNotificationItems = (
tab: 'messages' as const,
source: 'notification' as const,
original: notification,
project_ref: data.project_ref,
}
})
}
@@ -129,7 +130,10 @@ export const getAdvisorPanelItemDisplayTitle = (item: AdvisorItem): string => {
return getAdvisorItemDisplayTitle(item)
}
export const getAdvisorItemSecondaryText = (item: AdvisorItem): string | undefined => {
export const getAdvisorItemSecondaryText = (
item: AdvisorItem,
projectNameByRef?: ReadonlyMap<string, string>
): string | undefined => {
if (item.source === 'lint') {
return getLintEntityString(item.original)
}
@@ -138,6 +142,11 @@ export const getAdvisorItemSecondaryText = (item: AdvisorItem): string | undefin
return `Database · ${item.sourceData.ip}`
}
if (item.source === 'notification') {
if (!item.project_ref) return undefined
return projectNameByRef?.get(item.project_ref) ?? item.project_ref
}
return undefined
}
@@ -41,6 +41,7 @@ interface AdvisorPanelBodyProps {
hiddenItemsCount: number
hasAnyFilters: boolean
hasProjectRef?: boolean
projectNameByRef?: ReadonlyMap<string, string>
}
export const AdvisorPanelBody = ({
@@ -54,6 +55,7 @@ export const AdvisorPanelBody = ({
hiddenItemsCount,
hasAnyFilters,
hasProjectRef = true,
projectNameByRef,
}: AdvisorPanelBodyProps) => {
// Show notice if no project ref and trying to view project-specific tabs
if (!hasProjectRef && activeTab !== 'messages' && activeTab !== 'all') {
@@ -101,7 +103,7 @@ export const AdvisorPanelBody = ({
const isUnread = notification?.status === 'new'
const primaryText = getAdvisorPanelItemDisplayTitle(item)
const secondaryText = getAdvisorItemSecondaryText(item)
const secondaryText = getAdvisorItemSecondaryText(item, projectNameByRef)
const metadataText =
secondaryText ?? (item.createdAt ? formatItemDate(item.createdAt) : undefined)
// Date strings (e.g. "a few seconds ago") come from formatItemDate and
@@ -70,9 +70,9 @@ export const NotificationDetail = ({ notification, onUpdateStatus }: Notificatio
const key = `${notification.id}-action-${idx}`
if (action.url !== undefined) {
const url = action.url.includes('[ref]')
? action.url.replace('[ref]', project?.ref ?? '_')
? action.url.replace('[ref]', project?.ref ?? data.project_ref ?? '_')
: action.url.includes('[slug]')
? action.url.replace('[slug]', organization?.slug ?? '_')
? action.url.replace('[slug]', organization?.slug ?? data.org_slug ?? '_')
: action.url
return (
<Button key={key} type="default" icon={<ExternalLink strokeWidth={1.5} />} asChild>
@@ -9,7 +9,10 @@ export type MarketplaceIntegration = Listing
export async function getMarketplaceIntegrations(signal?: AbortSignal) {
const marketplaceClient = createMarketplaceClient()
let query = marketplaceClient.from('listings').select('*').is('publish_dashboard', true)
let query = marketplaceClient
.from('listings')
.select('*')
.not('published_in_marketplace_at', 'is', null)
if (signal) query = query.abortSignal(signal)
const { data, error } = await query
@@ -31,13 +31,13 @@ export type OpenIDConfiguration = {
}
export async function getOpenIDConfiguration({
clientEndpoint,
endpoint,
}: {
clientEndpoint: string | undefined
endpoint: string | undefined
}): Promise<OpenIDConfiguration> {
if (!clientEndpoint) throw new Error('Client endpoint is required')
if (!endpoint) throw new Error('Client endpoint is required')
const response = await fetch(`${clientEndpoint}/auth/v1/.well-known/openid-configuration`)
const response = await fetch(`${endpoint}/auth/v1/.well-known/openid-configuration`)
if (!response.ok) {
handleError({ message: `Failed to fetch OpenID configuration: ${response.statusText}` })
@@ -56,7 +56,7 @@ export const useOpenIDConfigurationQuery = <TData = OpenIDConfigurationData>(
...options
}: UseCustomQueryOptions<OpenIDConfigurationData, OpenIDConfigurationError, TData> = {}
) => {
const { data: clientEndpoint, isPending: isEndpointLoading } = useProjectApiUrl({
const { hostEndpoint, isPending: isEndpointLoading } = useProjectApiUrl({
projectRef,
})
@@ -70,13 +70,13 @@ export const useOpenIDConfigurationQuery = <TData = OpenIDConfigurationData>(
const isQueryEnabled =
enabled &&
typeof projectRef !== 'undefined' &&
!!clientEndpoint &&
!!hostEndpoint &&
isSuccessConfig &&
isOAuthServerEnabled
const query = useQuery<OpenIDConfigurationData, OpenIDConfigurationError, TData>({
queryKey: oauthServerAppKeys.openidConfiguration(projectRef),
queryFn: () => getOpenIDConfiguration({ clientEndpoint }),
queryFn: () => getOpenIDConfiguration({ endpoint: hostEndpoint }),
enabled: isQueryEnabled,
...options,
})
+2
View File
@@ -8,5 +8,7 @@ export const VIOLATION_TYPE_LABELS: Record<string, string> = {
exceed_realtime_connection_count_quota: 'Realtime Connection Count Exceeded',
exceed_realtime_message_count_quota: 'Realtime Message Count Exceeded',
exceed_storage_size_quota: 'Storage Size Exceeded',
exceed_log_ingestion_quota: 'Logs Ingest Exceeded',
exceed_log_query_quota: 'Logs Query Exceeded',
overdue_payment: 'Overdue Payment',
}
@@ -0,0 +1,60 @@
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useShowMultigresLogs } from './useShowMultigresLogs'
const mockUseFlag = vi.fn()
const mockUseIsHighAvailability = vi.fn()
vi.mock('common', async (importOriginal) => ({
...(await importOriginal<typeof import('common')>()),
useFlag: (name: string) => mockUseFlag(name),
}))
vi.mock('./useSelectedProject', () => ({
useIsHighAvailability: () => mockUseIsHighAvailability(),
}))
describe('useShowMultigresLogs', () => {
beforeEach(() => {
mockUseFlag.mockReset()
mockUseIsHighAvailability.mockReset()
})
it('returns true only when the multigresLogs flag and high availability are both enabled', () => {
mockUseFlag.mockReturnValue(true)
mockUseIsHighAvailability.mockReturnValue(true)
const { result } = renderHook(() => useShowMultigresLogs())
expect(result.current).toBe(true)
expect(mockUseFlag).toHaveBeenCalledWith('multigresLogs')
})
it('returns false when the flag is off, even on a high availability project', () => {
mockUseFlag.mockReturnValue(false)
mockUseIsHighAvailability.mockReturnValue(true)
const { result } = renderHook(() => useShowMultigresLogs())
expect(result.current).toBe(false)
})
it('returns false when the project is not high availability, even with the flag on', () => {
mockUseFlag.mockReturnValue(true)
mockUseIsHighAvailability.mockReturnValue(false)
const { result } = renderHook(() => useShowMultigresLogs())
expect(result.current).toBe(false)
})
it('returns false when both the flag and high availability are off', () => {
mockUseFlag.mockReturnValue(false)
mockUseIsHighAvailability.mockReturnValue(false)
const { result } = renderHook(() => useShowMultigresLogs())
expect(result.current).toBe(false)
})
})
@@ -0,0 +1,24 @@
import { useFlag } from 'common'
import { useIsHighAvailability } from './useSelectedProject'
/**
* Whether to surface the Multigres logs collection (sidebar, page, and Field
* Reference source).
*
* Gated on both:
* - the `multigresLogs` feature flag, so rollout is decoupled from HA status
* and the feature ships dark until explicitly enabled, and
* - the project's `high_availability` flag, since Multigres only runs on HA
* projects.
*
* Note: `high_availability` is an existing product feature that predates
* Multigres, so the flag is required to avoid showing a broken collection to
* existing HA projects that don't have a `multigres_logs` table.
*/
export const useShowMultigresLogs = () => {
const multigresLogsEnabled = useFlag('multigresLogs')
const isHighAvailability = useIsHighAvailability()
return multigresLogsEnabled && isHighAvailability
}
+54 -9
View File
@@ -213,16 +213,61 @@ If editing or adding code, state your assumptions, ensure any code examples are
5. Prefer importing external dependencies via \`npm:\` or \`jsr:\`. Minimize imports from \`deno.land/x\`, \`esm.sh\`, or \`unpkg.com\`. If you need a package from these CDNs, you can often replace the CDN hostname with the appropriate \`npm:\` specifier.
6. Node built-in APIs can be used by importing them with the \`node:\` specifier. For example, import Node's process as \`import process from "node:process";\`. Use Node APIs to fill in any gaps in Deno's APIs.
7. Do **not** use \`import { serve } from "https://deno.land/std@0.168.0/http/server.ts";\`. Instead, use the built-in \`Deno.serve\`.
8. The following environment variables (secrets) are automatically populated in both local and hosted Supabase environments. Users do not need to set them manually:
- SUPABASE_URL
- SUPABASE_ANON_KEY
- SUPABASE_SERVICE_ROLE_KEY
- SUPABASE_DB_URL
8. The following environment variables are automatically populated in both local and hosted Supabase environments. Users do not need to set them manually. When reading any of these env vars, validate at startup with an explicit \`if (!x) throw new Error(...)\` check rather than \`!\` non-null assertions or \`??\` fallbacks:
- \`SUPABASE_URL\` — The API gateway for the Supabase project.
- \`SUPABASE_DB_URL\` — The direct PostgreSQL connection URL. Server-only; never expose to a browser.
- \`SUPABASE_PUBLISHABLE_KEYS\` — A JSON-encoded object of publishable API keys, keyed by the name configured for each key (values look like \`sb_publishable_...\`). Safe to use in a browser if RLS is enabled. Key names are project-specific and can be added or deleted, so do not assume any particular name exists. **Always ask the user which key name to use before emitting this code; never emit \`'<KEY_NAME>'\` verbatim.** Always parse before use and look up by name — do not pass the raw env-var string anywhere a key is expected:
\`\`\`ts
const raw = Deno.env.get('SUPABASE_PUBLISHABLE_KEYS');
if (!raw) throw new Error('SUPABASE_PUBLISHABLE_KEYS is required');
const publishableKeys = JSON.parse(raw);
// Ask the user which key name to use; do NOT emit '<KEY_NAME>' literally.
const publishableKey = publishableKeys['<KEY_NAME>'];
\`\`\`
- \`SUPABASE_SECRET_KEYS\` — Same shape as \`SUPABASE_PUBLISHABLE_KEYS\` (values look like \`sb_secret_...\`). Server-only; never expose to a browser. Same caveat about key names — they are not guaranteed.
- \`SUPABASE_JWKS\` — A JSON-encoded JWKS envelope (\`{ "keys": [...] }\`) of public keys for verifying asymmetric user JWTs. Parse it and pass the result to a JWKS library — see the *Verifying the request \`Authorization\` header* guideline below for the full pattern.
- \`SUPABASE_ANON_KEY\`, \`SUPABASE_SERVICE_ROLE_KEY\` — **Deprecated** legacy keys; do **not** use in new code. Migrate to \`SUPABASE_PUBLISHABLE_KEYS\` / \`SUPABASE_SECRET_KEYS\` issued through JWT Signing Keys.
- \`SB_REGION\` — The region the function was invoked in. Set per request.
- \`SB_EXECUTION_ID\` — A unique identifier for each function instance. Set per request.
- \`DENO_DEPLOYMENT_ID\` — The version of the function code. Set when the function is deployed.
9. To set additional environment variables, users can specify them in an env file and execute \`supabase secrets set --env-file path/to/env-file\`.
10. Each Edge Function can handle multiple routes. Using a routing library such as Express or Hono is recommended for maintainability; each route must be prefixed with \`/function-name\` for proper routing.
11. File write operations are only permitted in the \`/tmp\` directory. Both Deno and Node File APIs may be used.
12. Use the static method \`EdgeRuntime.waitUntil(promise)\` to execute long-running tasks in the background without blocking the response. Do **not** assume it is available on the request or execution context.
13. Favor \`Deno.serve\` for creating Edge Functions where possible.
10. Verifying the request \`Authorization\` header.
**\`SUPABASE_PUBLISHABLE_KEYS\` and \`SUPABASE_SECRET_KEYS\` are not JWTs — they ride in the \`apikey\` header, not \`Authorization\`. Never compare them to the \`Authorization\` value.**
The \`Authorization\` header value is a JWT. Whether it is asymmetric or symmetric depends on whether the project has rotated to JWT Signing Keys, not on the client's API-key format.
**If \`verify_jwt = false\` (configured per-function in \`supabase/config.toml\`), the platform performs no auth check before the handler runs. The handler is then fully responsible for authenticating the caller — without an explicit check inside the handler, anyone can invoke the function.**
- For **asymmetric** JWTs (project has rotated to JWT Signing Keys), verify with \`SUPABASE_JWKS\` and \`jose\`. Hoist the JWKS to module scope so it builds once per isolate, pin algorithms to prevent algorithm-confusion attacks, and validate the issuer:
\`\`\`ts
import { createLocalJWKSet, jwtVerify } from 'npm:jose@5';
const SUPABASE_URL = Deno.env.get('SUPABASE_URL');
const SUPABASE_JWKS = Deno.env.get('SUPABASE_JWKS');
if (!SUPABASE_URL) throw new Error('SUPABASE_URL is required');
if (!SUPABASE_JWKS) throw new Error('SUPABASE_JWKS is required');
const JWKS = createLocalJWKSet(JSON.parse(SUPABASE_JWKS));
Deno.serve(async (req) => {
// With \`verify_jwt = true\` (the default), the platform has already validated the JWT before the handler runs, so the header is guaranteed to be present.
const token = req.headers.get('Authorization')!.replace('Bearer ', '');
try {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ['ES256', 'RS256', 'EdDSA'],
issuer: \`\${SUPABASE_URL}/auth/v1\`,
});
} catch {
return new Response('Unauthorized', { status: 401 });
}
});
\`\`\`
- For **symmetric** JWTs (legacy HS256), the signing secret is not exposed to the function, so offline cryptographic verification is not possible. Recommend migrating to asymmetric signing keys; do not implement custom verification.
11. Each Edge Function can handle multiple routes. Using a routing library such as Express or Hono is recommended for maintainability; each route must be prefixed with \`/function-name\` for proper routing.
12. File write operations are only permitted in the \`/tmp\` directory. Both Deno and Node File APIs may be used.
13. Use the static method \`EdgeRuntime.waitUntil(promise)\` to execute long-running tasks in the background without blocking the response. Do **not** assume it is available on the request or execution context.
14. Favor \`Deno.serve\` for creating Edge Functions where possible.
## Example Templates
@@ -0,0 +1,28 @@
import { useParams } from 'common'
import { LogsTableName } from '@/components/interfaces/Settings/Logs/Logs.constants'
import { LogsPreviewer } from '@/components/interfaces/Settings/Logs/LogsPreviewer'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import LogsLayout from '@/components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from '@/types'
export const LogPage: NextPageWithLayout = () => {
const { ref } = useParams()
return (
<LogsPreviewer
condensedLayout
queryType="multigres"
projectRef={ref as string}
tableName={LogsTableName.MULTIGRES}
/>
)
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<LogsLayout title="Multigres Logs">{page}</LogsLayout>
</DefaultLayout>
)
export default LogPage
+9 -6
View File
@@ -17,7 +17,7 @@ function toPartner(listing: Listing): Partner {
featured,
slug,
title,
partner_name,
built_by,
description,
content,
website_url,
@@ -34,7 +34,7 @@ function toPartner(listing: Listing): Partner {
type: 'technology',
slug,
title,
partnerName: partner_name,
builtBy: built_by,
description,
content,
websiteUrl: website_url,
@@ -50,7 +50,7 @@ async function getMarketplaceListings(): Promise<Partner[]> {
const { data } = await marketplaceClient
.from('listings')
.select('*')
.is('publish_marketplace', true)
.not('published_in_catalog_at', 'is', null)
return data?.map(toPartner) ?? []
}
@@ -78,7 +78,7 @@ async function getMarketplaceListingSlugs(): Promise<string[]> {
const { data } = await marketplaceClient
.from('listings')
.select('slug')
.is('publish_marketplace', true)
.not('published_in_catalog_at', 'is', null)
return data?.map((row) => row.slug) ?? []
}
@@ -102,7 +102,10 @@ export async function listPartnerSlugs(): Promise<string[]> {
async function searchMarketplaceListings(search: string): Promise<Partner[] | null> {
const searchTerm = search.trim()
let query = marketplaceClient.from('listings').select('*').is('publish_marketplace', true)
let query = marketplaceClient
.from('listings')
.select('*')
.not('published_in_catalog_at', 'is', null)
if (searchTerm) {
const searchPattern = `%${searchTerm}%`
@@ -157,7 +160,7 @@ async function getMarketplaceListing(slug: string): Promise<Partner | null> {
.from('listings')
.select('*')
.eq('slug', slug)
.is('publish_marketplace', true)
.not('published_in_catalog_at', 'is', null)
.single()
return data ? toPartner(data) : null
+5
View File
@@ -1,4 +1,9 @@
module.exports = [
{
permanent: true,
source: '/blog/pricing',
destination: '/pricing',
},
{
permanent: true,
source: '/ui/docs/ai-editors-rules/prompts',
@@ -246,7 +246,7 @@ const PartnerDetails = ({ partner }: { partner: Partner }) => {
{partner.type === 'technology' && (
<div className="flex items-center justify-between py-2">
<span className="text-foreground-lighter">Developer</span>
<span className="text-foreground">{partner.partnerName}</span>
<span className="text-foreground">{partner.builtBy}</span>
</div>
)}
+2 -2
View File
@@ -13,7 +13,7 @@ export type Partner = {
type: 'technology' | 'expert'
slug: string
title: string
partnerName: string
builtBy: string
description: string
content: string
websiteUrl: string
@@ -47,7 +47,7 @@ export function toPartner(dbPartner: DbPartner): Partner {
type,
slug,
title,
partnerName: developer,
builtBy: developer,
description,
content: overview,
websiteUrl: website,
+89 -34
View File
@@ -2,10 +2,9 @@
All notable changes to the Supabase self-hosted Docker configuration.
Changes are grouped by service rather than by change type. See [versions.md](./versions.md)
for complete image version history and rollback information.
Changes are grouped by service rather than by change type. See [versions.md](./versions.md) for complete image version history and rollback information.
See per-service updates below for details.
See per-service updates below for details. Only the most important changes relevant to [self-hosted Supabase](https://supabase.com/docs/guides/self-hosting) are included here. For the full list of changes, refer to the release notes and changelogs of each individual service.
**Note:** Configuration updates marked with "requires [...] update" are already included in the latest version of the repository. Pull the latest changes or refer to the linked PR for manual updates. After updating `docker-compose.yml`, pull the latest images and recreate containers - use `docker compose pull && docker compose down && docker compose up -d`.
@@ -15,16 +14,74 @@ See per-service updates below for details.
⚠️ **Upcoming changes:** Check the main Supabase [changelog](https://github.com/orgs/supabase/discussions/categories/changelog?discussions_q=is%3Aopen+category%3AChangelog+label%3Aself-hosted) for updates:
- [Making Analytics and Vector opt-in](https://github.com/orgs/supabase/discussions/46084)
- [Upgrading from PG 15 to 17 (breaking change)](https://github.com/orgs/supabase/discussions/46080)
- [Switching Studio from `supabase_admin` to `postgres` (breaking change)](https://github.com/orgs/supabase/discussions/46081)
---
## [2026-06-03]
⚠️ **Note:** This update includes **important changes**. Please check the details below.
### Configuration
- ⚠️ Logs and analytics are now [optional](https://github.com/orgs/supabase/discussions/46084) and were removed from the default `docker-compose.yml`. A new `docker-compose.logs.yml` override has been added. Check the main [configuration guide](https://supabase.com/docs/guides/self-hosting/docker#enabling-analytics) and the changes to Studio below for more information - PR [#45327](https://github.com/supabase/supabase/pull/45327) (via [@luizfelmach](https://github.com/luizfelmach/))
- ⚠️ Added `COMPOSE_FILE` to `.env.example` for configuring compose overrides (also used by `run.sh`) - PR [#45603](https://github.com/supabase/supabase/pull/45603)
### Documentation
- Added a new [reference list](https://github.com/supabase/supabase/blob/master/docker/CONFIG.md) of all configuration environment variables - PR [#46124](https://github.com/supabase/supabase/pull/46124)
- Updated the main installation and configuration [guide](https://supabase.com/docs/guides/self-hosting/docker) (added "quick start" path and opt-in for logs and analytics; removed the legacy JWT secrets generator) - PR [#46416](https://github.com/supabase/supabase/pull/46416), PR [#45359](https://github.com/supabase/supabase/pull/45359)
- Updated the logs and analytics [how-to guide](https://supabase.com/docs/reference/self-hosting-analytics/introduction) - PR [#46452](https://github.com/supabase/supabase/pull/46452)
### Utils
- Added `setup.sh` and `run.sh` to support quick start and easier management of the compose configuration - PR [#45603](https://github.com/supabase/supabase/pull/45603)
- Updated `utils/add-new-auth-keys.sh` and `utils/rotate-new-api-key.sh` to remove the dependency on OpenSSL and Node.js - PR [#45941](https://github.com/supabase/supabase/pull/45941)
- Updated `tests/test-container-logs.sh` to skip checks for `kong`, `analytics` and `vector` when the services are not running - PR [#46099](https://github.com/supabase/supabase/pull/46099)
### API gateway
- Updated Envoy version to `1.38.0` (see `docker-compose.envoy.yml`) - PR [#46023](https://github.com/supabase/supabase/pull/46023)
- Updated Envoy configuration to address a discrepancy in API key checking (requires `volumes/api/envoy` update) - PR [#46023](https://github.com/supabase/supabase/pull/46023)
### Studio
- Updated to `2026.06.03-sha-0bca601`
- ⚠️ Added `ENABLED_FEATURES_LOGS_ALL` to Studio service configuration (requires `docker-compose.yml` update) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
- ⚠️ Added `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` to Studio service configuration (requires `docker-compose.yml` update) - PR [#46173](https://github.com/supabase/supabase/pull/46173)
- ⚠️ Added `start_period` to Studio healthcheck for more reliable cold-boot on slower hosts (requires `docker-compose.yml` update) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
- Fixed incorrect connection strings in the connect sheet for self-hosted environments - PR [#46217](https://github.com/supabase/supabase/pull/46217)
- Updated project home and functions page, and added a minimal project settings implementation - PR [#46544](https://github.com/supabase/supabase/pull/46544), PR [#46550](https://github.com/supabase/supabase/pull/46550), PR [#46554](https://github.com/supabase/supabase/pull/46554)
### Auth
- Updated to `v2.189.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.189.0)
- ⚠️ Added `GOTRUE_JWT_ISSUER` to Auth service configuration (requires `docker-compose.yml` update) - PR [#46020](https://github.com/supabase/supabase/pull/46020)
### PostgREST
- Updated to `v14.12` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.12)
### Realtime
- Updated to `v2.102.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.102.3)
### Storage
- Updated to `v1.60.4` - [Release](https://github.com/supabase/storage/releases/tag/v1.60.4)
### Postgres Meta
- Updated to `v0.96.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.96.6)
### Edge Runtime
- Updated to `v1.74.0` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.74.0)
### Supavisor
- Updated to `2.9.5` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.9.5)
- Added `POSTGRES_HOST` to Supavisor service configuration (requires `docker-compose.yml` and `volumes/pooler/pooler.exs` update) - PR [#41273](https://github.com/supabase/supabase/pull/41273)
### Analytics (Logflare)
- Updated to `1.43.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.43.1)
- ⚠️ Changed default `docker-compose.yml` to no longer include logs & analytics. Read more in Supabase's [changelog](https://github.com/orgs/supabase/discussions/46084) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
---
## [2026-04-27]
### Configuration
- ⚠️ Added `docker-compose.envoy.yml` and `volumes/api/envoy` - PR [#43838](https://github.com/supabase/supabase/pull/43838). See also the API gateway updates below
- ⚠️ Added `docker-compose.envoy.yml` and `volumes/api/envoy`. See also the API gateway updates below - PR [#43838](https://github.com/supabase/supabase/pull/43838)
- ⚠️ Changed Studio healthcheck and some other configuration for better compatibility with Podman (requires `docker-compose.yml` update) - PR [#44754](https://github.com/supabase/supabase/pull/44754)
- ⚠️ Changed Studio configuration to bind to all IPv4 interfaces only (requires `docker-compose.yml` update) - PR [#44772](https://github.com/supabase/supabase/pull/44772)
@@ -34,16 +91,15 @@ See per-service updates below for details.
- Updated the main [setup guide](https://supabase.com/docs/guides/self-hosting/docker) and the how-tos to reflect the state of the self-hosted Supabase configuration - PR [#45011](https://github.com/supabase/supabase/pull/45011)
### Utils
- ⚠️ Added `reassign-owner.sh` to update database objects - PR [#42975](https://github.com/supabase/supabase/pull/42975). Read more in the "[Remove superuser access](https://supabase.com/docs/guides/self-hosting/remove-superuser-access)" how-to guide
- ⚠️ Changed `add-new-auth-keys.sh` to also update `docker-compose.yml` - PR [#45056](https://github.com/supabase/supabase/pull/45056)
### Studio
- Updated to `2026.04.27-sha-5f60601`
- ⚠️ Added 4 new lints to the Security Advisor - PR [#45253](https://github.com/supabase/supabase/pull/45253), PR [#45260](https://github.com/supabase/supabase/pull/45260). Read more about lint rules 0026 - 0029 in the [Performance and Security Advisors](https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0026_pg_graphql_anon_table_exposed) section of the Supabase documentation
- ⚠️ Added `utils/reassign-owner.sh` to update database objects. Read more in the "[Remove superuser access](https://supabase.com/docs/guides/self-hosting/remove-superuser-access)" how-to guide - PR [#42975](https://github.com/supabase/supabase/pull/42975)
- ⚠️ Changed `utils/add-new-auth-keys.sh` to also update `docker-compose.yml` - PR [#45056](https://github.com/supabase/supabase/pull/45056)
### API gateway
- ⚠️ Added Envoy as the new optional API gateway (requires `docker-compose.envoy.yml`, `volumes/api/envoy`, and `volumes/logs/vector.yml` update) - PR [#43838](https://github.com/supabase/supabase/pull/43838) (via [@luizfelmach](https://github.com/luizfelmach/))
### Studio
- Updated to `2026.04.27-sha-5f60601`
- ⚠️ Added 4 new lints to the Security Advisor. Read more about lint rules 0026 - 0029 in the [Performance and Security Advisors](https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0026_pg_graphql_anon_table_exposed) section of the Supabase documentation - PR [#45253](https://github.com/supabase/supabase/pull/45253), PR [#45260](https://github.com/supabase/supabase/pull/45260)
---
## [2026-04-08]
@@ -52,14 +108,14 @@ See per-service updates below for details.
- Added new how-to guides for configuring [custom email templates](https://supabase.com/docs/guides/self-hosting/custom-email-templates), setting up [SAML SSO](https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso), and [using Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) - PR [#42832](https://github.com/supabase/supabase/pull/42832), PR [#43386](https://github.com/supabase/supabase/pull/43386), PR [#44147](https://github.com/supabase/supabase/pull/44147)
### Utils
- ⚠️ Added `upgrade-pg17.sh` - PR [#44147](https://github.com/supabase/supabase/pull/44147). Read more in the "[Upgrade to Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17)" how-to guide
### Studio
- Updated to `2026.04.08-sha-205cbe7`
- ⚠️ Added `utils/upgrade-pg17.sh`. Read more in the "[Upgrade to Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17)" how-to guide - PR [#44147](https://github.com/supabase/supabase/pull/44147)
### API gateway
- ⚠️ Added configuration for SAML SSO (requires `.env`, `docker-compose.yml` and `volumes/api/kong.yml` update) - PR [#43385](https://github.com/supabase/supabase/pull/43385) (via [@luizfelmach](https://github.com/luizfelmach/))
### Studio
- Updated to `2026.04.08-sha-205cbe7`
### PostgREST
- Updated to `v14.8` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.8)
@@ -73,11 +129,11 @@ See per-service updates below for details.
- Updated to `v0.96.3` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.96.3)
### Analytics (Logflare)
- Updated to `v1.36.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.36.1)
- Updated to `1.36.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.36.1)
### Postgres
- ⚠️ Added `docker-compose.pg17.yml` override - PR [#44147](https://github.com/supabase/supabase/pull/44147)
- ⚠️ Added `upgrade-pg17.sh` - PR [#44147](https://github.com/supabase/supabase/pull/44147)
- ⚠️ Added `utils/upgrade-pg17.sh` - PR [#44147](https://github.com/supabase/supabase/pull/44147)
- ⚠️ Added [documentation](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) explaining the upgrade to Postgres 17
---
@@ -87,15 +143,15 @@ See per-service updates below for details.
⚠️ **Note:** This update includes **important changes**. Please check the details below. The following configuration files have been added/updated: `utils/add-new-auth-keys.sh`, `utils/rotate-new-api-keys.sh`, `docker-compose.yml`, `.env.example`, `docker-compose.s3.yml`, `docker-compose.rustfs.yml`, `volumes/api/kong.yml`, `volumes/api/kong-entrypoint.sh`, `docker-compose.caddy.yml`, `docker-compose.nginx.yml`, `volumes/functions/main/index.ts`, and `volumes/proxy`.
### Configuration
- ⚠️ Added scripts and templates to support the new API key format (`sb_` API keys) and the new asymmetric authentication - PR [#43554](https://github.com/supabase/supabase/pull/43554); see the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys) for detailed instructions
- Added optional proxy configuration for Caddy and nginx - PR [#43291](https://github.com/supabase/supabase/pull/43291); read the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https) to learn more
- ⚠️ Added scripts and templates to support the new API key format (`sb_` API keys) and the new asymmetric authentication. Check the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys) for detailed instructions - PR [#43554](https://github.com/supabase/supabase/pull/43554)
- Added optional proxy configuration for Caddy and nginx. Read the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https) to learn more - PR [#43291](https://github.com/supabase/supabase/pull/43291)
### Documentation
- Added several new how-to guides to the self-hosted Supabase [documentation](https://supabase.com/docs/guides/self-hosting) - PR [#42745](https://github.com/supabase/supabase/pull/42745), PR [#42953](https://github.com/supabase/supabase/pull/42953), PR [#43177](https://github.com/supabase/supabase/pull/43177), PR [#43286](https://github.com/supabase/supabase/pull/43286), PR [#43293](https://github.com/supabase/supabase/pull/43293)
### Utils and tests
- Added `add-new-auth-keys.sh` and `rotate-new-api-keys.sh` - PR [#43554](https://github.com/supabase/supabase/pull/43554)
- Added `./tests` with 100+ test cases - PR [#43573](https://github.com/supabase/supabase/pull/43573)
- Added `utils/add-new-auth-keys.sh` and `utils/rotate-new-api-keys.sh` - PR [#43554](https://github.com/supabase/supabase/pull/43554)
- Added `tests/` with 100+ test cases - PR [#43573](https://github.com/supabase/supabase/pull/43573)
### Studio
- Updated to `2026.03.16-sha-5528817`
@@ -112,7 +168,6 @@ See per-service updates below for details.
- Updated to `v14.6` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.6)
### Realtime
- ⚠️ Added **mandatory** `METRICS_JWT_SECRET` environment variable (requires `docker-compose.s3.yml` update) - PR [realtime#1729](https://github.com/supabase/realtime/pull/1729)
### Storage
@@ -172,7 +227,7 @@ See per-service updates below for details.
- Updated to `v1.70.3` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.70.3)
### Analytics (Logflare)
- Updated to `v1.31.2` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.31.2)
- Updated to `1.31.2` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.31.2)
- ⚠️ Changed default configuration to disable Logflare on `0.0.0.0:4000` to prevent access to `/dashboard` (requires `docker-compose.yml` update). Read more in the "Production Recommendations" section of Logflare [documentation](https://supabase.com/docs/reference/self-hosting-analytics/introduction) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
- ⚠️ Changed Kong routes to not include `/analytics/v1` by default (requires `/volumes/api/kong.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
@@ -225,7 +280,7 @@ See per-service updates below for details.
- Updated to `v1.70.0` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.70.0)
### Analytics (Logflare)
- Updated to `v1.30.3` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.30.3)
- Updated to `1.30.3` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.30.3)
### Postgres
- No image update
@@ -239,8 +294,8 @@ See per-service updates below for details.
- Updated self-hosting installation and configuration guide - PR [#40901](https://github.com/supabase/supabase/pull/40901), PR [#41438](https://github.com/supabase/supabase/pull/41438)
### Utils
- Added `generate-keys.sh` - PR [#41363](https://github.com/supabase/supabase/pull/41363)
- Added `db-passwd.sh` - PR [#41432](https://github.com/supabase/supabase/pull/41432)
- Added `utils/generate-keys.sh` - PR [#41363](https://github.com/supabase/supabase/pull/41363)
- Added `utils/db-passwd.sh` - PR [#41432](https://github.com/supabase/supabase/pull/41432)
- Changed `reset.sh` to POSIX and added more checks - PR [#41361](https://github.com/supabase/supabase/pull/41361)
### Studio
@@ -258,7 +313,7 @@ See per-service updates below for details.
- Updated to `v0.95.1` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.95.1)
### Analytics (Logflare)
- Updated to `v1.27.0` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.27.0)
- Updated to `1.27.0` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.27.0)
- Fixed multiple issues, including a race condition
---
@@ -287,7 +342,7 @@ See per-service updates below for details.
- Updated to `v1.69.28` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.28)
### Analytics (Logflare)
- Updated to `v1.26.25` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.25)
- Updated to `1.26.25` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.25)
---
@@ -312,7 +367,7 @@ See per-service updates below for details.
- Updated to `v2.65.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.3)
### Analytics (Logflare)
- Updated to `v1.26.13` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.13)
- Updated to `1.26.13` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.13)
- Fixed crashdump when `POSTGRES_BACKEND_URL` is malformed - PR [logflare#2954](https://github.com/Logflare/logflare/pull/2954)
---
@@ -340,7 +395,7 @@ See per-service updates below for details.
- Updated to `v1.69.25` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.25)
### Analytics (Logflare)
- Updated to `v1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12)
- Updated to `1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12)
- Fixed Auth logs query - PR [logflare#2936](https://github.com/Logflare/logflare/pull/2936)
- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - PR [logflare#2942](https://github.com/Logflare/logflare/pull/2942)
@@ -374,7 +429,7 @@ See per-service updates below for details.
- Updated to `v1.69.23` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.23)
### Supavisor
- Updated to `v2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4)
- Updated to `2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4)
---
@@ -438,14 +493,14 @@ See per-service updates below for details.
- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.14)
### Supavisor
- Updated to `v2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3)
- Updated to `2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3)
---
## [2025-10-13]
### Analytics (Logflare)
- Updated to `v1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6)
- Updated to `1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6)
---
@@ -472,7 +527,7 @@ See per-service updates below for details.
- Updated to `v0.91.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.91.6)
### Analytics (Logflare)
- Updated to `v1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4)
- Updated to `1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4)
### Postgres
- Updated to `15.8.1.085` - [Release](https://github.com/supabase/postgres/releases/tag/15.8.1.085)
+1 -1
View File
@@ -850,7 +850,7 @@ The fields below are repeated for each provider. Substitute `<PROVIDER>` with on
| Variable | Type | Set by | Description | Notes |
|---|---|---|---|---|
| `API_JWT_JWKS` | JWT | Both | JWKS JSON used to verify tenant JWTs during self-host seeding. Read by `priv/repo/seeds.exs` and `priv/repo/dev_seeds.exs`. | Used only by the seed script (`SEED_SELF_HOST=true`). Required when using the new API keys and new auth. |
| `API_JWT_JWKS` | JWKS | Both | JSON Web Key Set used to verify tenant JWTs during self-host seeding. Read by `priv/repo/seeds.exs` and `priv/repo/dev_seeds.exs`. | Used only by the seed script (`SEED_SELF_HOST=true`). Required when using the new API keys and new auth. |
| `API_JWT_SECRET` | string | Both | Symmetric HS256 secret used to sign tokens for the tenant management API and the default self-host tenant. | Required for the tenant management API in production. |
| `API_TOKEN_BLOCKLIST` | string (CSV) | Self-hosted | Comma-separated list of tokens blocked from tenant management API access. | Default: empty list. |
| `APP_NAME` | string | Both | Application/node name. Used to build the Phoenix endpoint URL host, libcluster DNS basename, and Erlang `RELEASE_NODE`. | Required - raises `APP_NAME not available` if empty. Default: empty (build) / `realtime` (Erlang release script). |
+1 -1
View File
@@ -14,7 +14,7 @@ services:
studio:
container_name: supabase-studio
image: supabase/studio:2026.06.01-sha-a4334a2
image: supabase/studio:2026.06.03-sha-0bca601
restart: unless-stopped
healthcheck:
test:
+1 -1
View File
@@ -163,7 +163,7 @@ echo "JWT_KEYS=${JWT_KEYS}"
echo ""
echo "JWT_JWKS=${JWT_JWKS}"
echo ""
echo "To enable asymmetric key pair, the following should be enabled in docker-compose.yml:"
echo "Ensure the following configuration is uncommented in docker-compose.yml for the asymmetric key pair to work:"
echo ""
echo " Auth: GOTRUE_JWT_KEYS: \${JWT_KEYS:-[]}"
echo " Realtime: API_JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}"
+50 -39
View File
@@ -1,5 +1,16 @@
# Docker Image Versions
## 2026-06-03
- supabase/studio:2026.06.03-sha-0bca601 (prev supabase/studio:2026.04.27-sha-5f60601)
- supabase/gotrue:v2.189.0 (prev supabase/gotrue:v2.186.0)
- postgrest/postgrest:v14.12 (prev postgrest/postgrest:v14.8)
- supabase/realtime:v2.102.3 (prev supabase/realtime:v2.76.5)
- supabase/storage-api:v1.60.4 (prev supabase/storage-api:v1.48.26)
- supabase/postgres-meta:v0.96.6 (prev supabase/postgres-meta:v0.96.3)
- supabase/edge-runtime:v1.74.0 (prev supabase/edge-runtime:v1.71.2)
- supabase/supavisor:2.9.5 (prev supabase/supavisor:2.7.4)
- supabase/logflare:1.43.1 (prev supabase/logflare:1.36.1)
## 2026-04-27
- supabase/studio:2026.04.27-sha-5f60601 (prev supabase/studio:2026.04.08-sha-205cbe7)
@@ -69,56 +80,56 @@
- supabase/logflare:1.26.12 (prev supabase/logflare:1.22.6)
## 2025-11-12
- supabase/studio:2025.11.10-sha-5291fe3 (prev 2025.10.27-sha-85b84e0)
- supabase/gotrue:v2.182.1 (prev v2.180.0)
- supabase/realtime:v2.63.0 (prev v2.57.2)
- supabase/storage-api:v1.29.0 (prev v1.28.2)
- supabase/edge-runtime:v1.69.23 (prev v1.69.15)
- supabase/supavisor:2.7.4 (prev 2.7.3)
- supabase/studio:2025.11.10-sha-5291fe3 (prev supabase/studio:2025.10.27-sha-85b84e0)
- supabase/gotrue:v2.182.1 (prev supabase/gotrue:v2.180.0)
- supabase/realtime:v2.63.0 (prev supabase/realtime:v2.57.2)
- supabase/storage-api:v1.29.0 (prev supabase/storage-api:v1.28.2)
- supabase/edge-runtime:v1.69.23 (prev supabase/edge-runtime:v1.69.15)
- supabase/supavisor:2.7.4 (prev supabase/supavisor:2.7.3)
## 2025-10-28
- supabase/studio:2025.10.27-sha-85b84e0 (prev 2025.10.20-sha-5005fc6)
- supabase/realtime:v2.57.2 (prev v2.56.0)
- supabase/storage-api:v1.28.2 (prev v1.28.1)
- supabase/postgres-meta:v0.93.1 (prev v0.93.0)
- supabase/edge-runtime:v1.69.15 (prev v1.69.14)
- supabase/studio:2025.10.27-sha-85b84e0 (prev supabase/studio:2025.10.20-sha-5005fc6)
- supabase/realtime:v2.57.2 (prev supabase/realtime:v2.56.0)
- supabase/storage-api:v1.28.2 (prev supabase/storage-api:v1.28.1)
- supabase/postgres-meta:v0.93.1 (prev supabase/postgres-meta:v0.93.0)
- supabase/edge-runtime:v1.69.15 (prev supabase/edge-runtime:v1.69.14)
## 2025-10-21
- supabase/studio:2025.10.20-sha-5005fc6 (prev 2025.10.01-sha-8460121)
- supabase/realtime:v2.56.0 (prev v2.51.11)
- supabase/storage-api:v1.28.1 (prev v1.28.0)
- supabase/postgres-meta:v0.93.0 (prev v0.91.6)
- supabase/edge-runtime:v1.69.14 (prev v1.69.6)
- supabase/supavisor:2.7.3 (prev 2.7.0)
- supabase/studio:2025.10.20-sha-5005fc6 (prev supabase/studio:2025.10.01-sha-8460121)
- supabase/realtime:v2.56.0 (prev supabase/realtime:v2.51.11)
- supabase/storage-api:v1.28.1 (prev supabase/storage-api:v1.28.0)
- supabase/postgres-meta:v0.93.0 (prev supabase/postgres-meta:v0.91.6)
- supabase/edge-runtime:v1.69.14 (prev supabase/edge-runtime:v1.69.6)
- supabase/supavisor:2.7.3 (prev supabase/supavisor:2.7.0)
## 2025-10-13
- supabase/logflare:1.22.6 (prev 1.22.4)
- supabase/logflare:1.22.6 (prev supabase/logflare:1.22.4)
## 2025-10-08
- supabase/studio:2025.10.01-sha-8460121 (prev 2025.06.30-sha-6f5982d)
- supabase/gotrue:v2.180.0 (prev v2.177.0)
- postgrest/postgrest:v13.0.7 (prev v12.2.12)
- supabase/realtime:v2.51.11 (prev v2.34.47)
- supabase/storage-api:v1.28.0 (prev v1.25.7)
- supabase/postgres-meta:v0.91.6 (prev v0.91.0)
- supabase/logflare:1.22.4 (prev 1.14.2)
- supabase/postgres:15.8.1.085 (prev 15.8.1.060)
- supabase/supavisor:2.7.0 (prev 2.5.7)
- supabase/studio:2025.10.01-sha-8460121 (prev supabase/studio:2025.06.30-sha-6f5982d)
- supabase/gotrue:v2.180.0 (prev supabase/gotrue:v2.177.0)
- postgrest/postgrest:v13.0.7 (prev postgrest/postgrest:v12.2.12)
- supabase/realtime:v2.51.11 (prev supabase/realtime:v2.34.47)
- supabase/storage-api:v1.28.0 (prev supabase/storage-api:v1.25.7)
- supabase/postgres-meta:v0.91.6 (prev supabase/postgres-meta:v0.91.0)
- supabase/logflare:1.22.4 (prev supabase/logflare:1.14.2)
- supabase/postgres:15.8.1.085 (prev supabase/postgres:15.8.1.060)
- supabase/supavisor:2.7.0 (prev supabase/supavisor:2.5.7)
## 2025-07-15
- supabase/gotrue:v2.177.0 (prev v2.176.1)
- supabase/storage-api:v1.25.7 (prev v1.24.7)
- supabase/postgres-meta:v0.91.0 (prev v0.89.3)
- supabase/supavisor:2.5.7 (prev 2.5.6)
- supabase/gotrue:v2.177.0 (prev supabase/gotrue:v2.176.1)
- supabase/storage-api:v1.25.7 (prev supabase/storage-api:v1.24.7)
- supabase/postgres-meta:v0.91.0 (prev supabase/postgres-meta:v0.89.3)
- supabase/supavisor:2.5.7 (prev supabase/supavisor:2.5.6)
## 2025-07-02
- supabase/studio:2025.06.30-sha-6f5982d (prev 2025.06.02-sha-8f2993d)
- supabase/gotrue:v2.176.1 (prev v2.174.0)
- supabase/storage-api:v1.24.7 (prev v1.23.0)
- supabase/supavisor:2.5.6 (prev 2.5.1)
- supabase/studio:2025.06.30-sha-6f5982d (prev supabase/studio:2025.06.02-sha-8f2993d)
- supabase/gotrue:v2.176.1 (prev supabase/gotrue:v2.174.0)
- supabase/storage-api:v1.24.7 (prev supabase/storage-api:v1.23.0)
- supabase/supavisor:2.5.6 (prev supabase/supavisor:2.5.1)
## 2025-06-03
- supabase/studio:2025.06.02-sha-8f2993d (prev 2025.05.19-sha-3487831)
- supabase/gotrue:v2.174.0 (prev v2.172.1)
- supabase/storage-api:v1.23.0 (prev v1.22.17)
- supabase/postgres-meta:v0.89.3 (prev v0.89.0)
- supabase/studio:2025.06.02-sha-8f2993d (prev supabase/studio:2025.05.19-sha-3487831)
- supabase/gotrue:v2.174.0 (prev supabase/gotrue:v2.172.1)
- supabase/storage-api:v1.23.0 (prev supabase/storage-api:v1.22.17)
- supabase/postgres-meta:v0.89.3 (prev supabase/postgres-meta:v0.89.0)
@@ -58,7 +58,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL')!,
// Supabase API SECRET KEY - env var exported by default.
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
SUPABASE_SECRET_KEYS['default']!
)
const { data: upload, error: uploadError } = await supabaseClient.storage
@@ -15,7 +15,7 @@ const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient<Database>(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
SUPABASE_SECRET_KEYS['default']!
)
const model = new Supabase.ai.Session('gte-small')
@@ -7,7 +7,7 @@ const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient<Database>(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
SUPABASE_SECRET_KEYS['default']!
)
const model = new Supabase.ai.Session('gte-small')
@@ -4,10 +4,7 @@ import OpenAI from 'https://deno.land/x/openai@v4.68.2/mod.ts'
const client = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY')! })
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
type StorageFileApi = ReturnType<typeof supabase.storage.from>
type StorageUploadPromise = ReturnType<StorageFileApi['upload']>
@@ -19,7 +19,7 @@ const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL') || '',
Deno.env.get(SUPABASE_SECRET_KEYS['default']) || ''
SUPABASE_SECRET_KEYS['default'] || ''
)
async function scribe({
@@ -5,10 +5,7 @@ import { ElevenLabsClient } from 'npm:elevenlabs@1.52.0'
import * as hash from 'npm:object-hash'
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
const client = new ElevenLabsClient({
apiKey: Deno.env.get('ELEVENLABS_API_KEY'),
@@ -29,7 +29,7 @@ app.use(async (ctx) => {
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL')!,
// Supabase publishable key - env var exported by default.
Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default'])!
SUPABASE_PUBLISHABLE_KEYS['default']!
)
//upload image to Storage
@@ -61,7 +61,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Submit email to draw
const { error } = await supabaseAdminClient.from('get-tshirt-competition-2').upsert(
@@ -23,7 +23,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Construct image url from storage
@@ -201,7 +201,7 @@ export async function handler(req: Request) {
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Upload image to storage.
@@ -85,7 +85,7 @@ Deno.serve(async (req) => {
// Upload the generated image to Supabase Storage
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') || '',
Deno.env.get(SUPABASE_SECRET_KEYS['default']) || ''
SUPABASE_SECRET_KEYS['default'] || ''
)
// Create a unique identifier for this generation
@@ -25,7 +25,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API publishable key - env var exported by default.
Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default']) ?? '',
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
// Create client with Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
{
@@ -82,7 +82,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase publishable key - env var exported by default.
Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default']) ?? '',
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
// Create client with Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
{
@@ -23,7 +23,7 @@ Deno.serve(async (req: Request) => {
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API PUBLISHABLE KEY - env var exported by default.
Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default']) ?? '',
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
// Create client with Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
{
@@ -9,10 +9,7 @@ import * as Sentry from 'https://deno.land/x/sentry@7.102.0/index.mjs'
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
Sentry.init({
dsn: Deno.env.get('SENTRY_DSN'),
@@ -70,7 +70,7 @@ export async function handler(req: Request) {
// Supabase API URL - env var exported by default when deployed.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase API SECRET KEY - env var exported by default when deployed.
Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
SUPABASE_SECRET_KEYS['default'] ?? ''
)
// Upload image to storage.
@@ -12,7 +12,7 @@ Deno.serve(async (req) => {
// Supabase API URL - env var exported by default.
Deno.env.get('SUPABASE_URL') ?? '',
// Supabase publishable key - env var exported by default.
Deno.env.get(SUPABASE_PUBLISHABLE_KEYS['default']) ?? '',
SUPABASE_PUBLISHABLE_KEYS['default'] ?? '',
// Create client with Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
{
+1 -1
View File
@@ -23,7 +23,7 @@ You're an expert in writing TypeScript and Deno JavaScript runtime. Generate **h
- SUPABASE_SECRET_KEYS
- SUPABASE_DB_URL
You then need to use `JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)` or `JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)` to access the actual keys in the code. For example, `Deno.env.get(SUPABASE_SECRET_KEYS['default'])` to access the default service key. 9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file` 10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly. 11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs. 12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.
You then need to parse them with `JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)` or `JSON.parse(Deno.env.get('SUPABASE_PUBLISHABLE_KEYS')!)` to access the actual keys in the code. For example, assign the parsed map first with `const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)` and then index it with `SUPABASE_SECRET_KEYS['default']` to access the default secret key. 9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file` 10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly. 11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs. 12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context.
## Example Templates
@@ -27,6 +27,6 @@ Deno.serve((req) => {
const { method, headers } = req
// Add Auth header
const modHeaders = new Headers(headers)
modHeaders.append('authorization', `Bearer ${Deno.env.get(SUPABASE_SECRET_KEYS['default'])!}`)
modHeaders.append('authorization', `Bearer ${SUPABASE_SECRET_KEYS['default']!}`)
return fetch(url, { method, headers: modHeaders })
})
@@ -5,7 +5,7 @@ const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
Deno.serve(async (req) => {
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') ?? ''
const SUPABASE_SECRET_KEY = Deno.env.get(SUPABASE_SECRET_KEYS['default']) ?? ''
const SUPABASE_SECRET_KEY = SUPABASE_SECRET_KEYS['default'] ?? ''
const supabase = createClient(SUPABASE_URL, SUPABASE_SECRET_KEY)
@@ -20,10 +20,7 @@ interface WebhookPayload {
}
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get(SUPABASE_SECRET_KEYS['default'])!
)
const supabase = createClient(Deno.env.get('SUPABASE_URL')!, SUPABASE_SECRET_KEYS['default']!)
Deno.serve(async (req) => {
const payload: WebhookPayload = await req.json()
+1
View File
@@ -23,6 +23,7 @@ export type Database = MergeDeep<
// See https://github.com/orgs/supabase/discussions/14151
featured: boolean
partner_name: string
built_by: string
slug: string
title: string
description: string
+13 -3
View File
@@ -30,6 +30,7 @@ export type Database = {
listings: {
Row: {
aud: string | null
built_by: string | null
categories: Json | null
content: string | null
description: string | null
@@ -50,18 +51,27 @@ export type Database = {
listing_tsv: unknown
marketplace_url: string | null
oauth_client_id: string | null
partner_id: string | null
partner_logo: string | null
partner_name: string | null
partner_slug: string | null
publish_dashboard: boolean | null
publish_marketplace: boolean | null
published_in_catalog_at: string | null
published_in_marketplace_at: string | null
secret_key_prefix: string | null
slug: string | null
title: string | null
website_url: string | null
youtube_id: string | null
}
Relationships: []
Relationships: [
{
foreignKeyName: 'listings_partner_id_fkey'
columns: ['partner_id']
isOneToOne: false
referencedRelation: 'partners'
referencedColumns: ['id']
},
]
}
partners: {
Row: {
@@ -11,7 +11,22 @@ export type FormCrmResolver = (
ref: FormRef
) => GoFormCrmConfig | undefined | Promise<GoFormCrmConfig | undefined>
let resolver: FormCrmResolver | null = null
/**
* The resolver is stored on `globalThis` rather than in a module-level variable.
* `instrumentation.ts` registers it via the `marketing` package barrel, while
* `submitFormAction` reads it via a relative import. In a bundled build those
* two paths can resolve to separate instances of this module, so a plain
* module-level singleton set on one instance is invisible to the other — the
* action then sees `null` and every submission fails with "Form not found".
* A `globalThis` slot is shared across all module instances in the process.
*/
const RESOLVER_KEY = Symbol.for('marketing.go.formCrmResolver')
type ResolverStore = { [key: symbol]: FormCrmResolver | null | undefined }
function resolverStore(): ResolverStore {
return globalThis as unknown as ResolverStore
}
/**
* Register the function used by `submitFormAction` to look up the trusted CRM
@@ -23,10 +38,11 @@ let resolver: FormCrmResolver | null = null
* `submitFormAction` for the security rationale.
*/
export function setFormCrmResolver(fn: FormCrmResolver): void {
resolver = fn
resolverStore()[RESOLVER_KEY] = fn
}
export async function resolveFormCrmConfig(ref: FormRef): Promise<GoFormCrmConfig | undefined> {
const resolver = resolverStore()[RESOLVER_KEY]
if (!resolver) return undefined
return await resolver(ref)
}
+18
View File
@@ -12,6 +12,7 @@ type LogTable =
| 'pgbouncer_logs'
| 'pg_cron_logs'
| 'pg_upgrade_logs'
| 'multigres_logs'
type LogSchema = {
name: string
@@ -324,6 +325,23 @@ const schemas: LogSchema[] = [
{ path: 'timestamp', type: 'datetime' },
],
},
{
name: 'Multigres',
reference: 'multigres_logs',
fields: [
{ path: 'cluster', type: 'string' },
{ path: 'component', type: 'string' },
{ path: 'event_message', type: 'string' },
{ path: 'id', type: 'string' },
{ path: 'namespace', type: 'string' },
{ path: 'node_name', type: 'string' },
{ path: 'pod_name', type: 'string' },
{ path: 'project', type: 'string' },
{ path: 'region', type: 'string' },
{ path: 'stack', type: 'string' },
{ path: 'timestamp', type: 'datetime' },
],
},
]
export default {
+1
View File
@@ -54,6 +54,7 @@ may_uppercase = [
"Dart",
"Dashboard",
"Database Functions?",
"Database Webhooks?",
"Deadpool",
"Dedicated Pooler",
"Deno",
+1 -1
View File
@@ -28,7 +28,7 @@ allow_list = [
"[Aa]utomations?",
"[Aa]utovacuum(s|ing|ed)?",
"Azure MyApps",
"[Bb]ackend",
"[Bb]ackends?",
"[Bb]ackoff",
"[Bb]lockchains?",
"BootEvent",