Skip to content

Conversation

@maparent
Copy link
Collaborator

@maparent maparent commented Feb 6, 2026

https://linear.app/discourse-graphs/issue/ENG-925/upgrade-supabase-jwt-keys

Status: This is now functional. We have to decide when to do it.
It is not urgent, but the work was done, and is worth using, since it is where supabase wants us to go.

The upsides are better key management and revocation.
The downside is that, when we change the keys, we have to change the function secrets. Supabase claims this may be automated someday.

https://github.com/supabase/supabase/blob/037e5f90a5689c3d847bd2adf9c8ec3956a0e7a0/apps/docs/content/guides/functions/auth.mdx


Open with Devin

@linear
Copy link

linear bot commented Feb 6, 2026

@supabase
Copy link

supabase bot commented Feb 6, 2026

Updates to Preview Branch (eng-925-upgrade-supabase-jwt-keys) ↗︎

Deployments Status Updated
Database Sat, 07 Feb 2026 18:17:16 UTC
Services Sat, 07 Feb 2026 18:17:16 UTC
APIs Sat, 07 Feb 2026 18:17:16 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Sat, 07 Feb 2026 18:17:16 UTC
Migrations Sat, 07 Feb 2026 18:17:16 UTC
Seeding Sat, 07 Feb 2026 18:17:16 UTC
Edge Functions Sat, 07 Feb 2026 18:17:19 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

export const getVariant = () => {
const processHasVars =
!!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_ANON_KEY"];
!!process.env["SUPABASE_URL"] && !!process.env["SUPABASE_PUBLISHABLE_KEY"];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Local env generation produces SUPABASE_ANON_KEY but code expects SUPABASE_PUBLISHABLE_KEY

The createEnv.mts script generates local environment files by running supabase status -o env and prefixing variables with SUPABASE_. Supabase outputs ANON_KEY, which becomes SUPABASE_ANON_KEY. However, the PR changed all code to expect SUPABASE_PUBLISHABLE_KEY instead.

Root Cause

In packages/database/scripts/createEnv.mts:37-45, the script transforms supabase output:

const prefixed = stdout
  .split("\n")
  .map((line) =>
    /^API_URL=/.test(line)
      ? `SUPABASE_URL=${line.substring(8)}`
      : `SUPABASE_${line}`,
  )

This produces SUPABASE_ANON_KEY from supabase's ANON_KEY output, but packages/database/src/dbDotEnv.mjs:20 and packages/database/src/dbDotEnv.mjs:77 now check for SUPABASE_PUBLISHABLE_KEY.

Impact: Local development will fail because SUPABASE_PUBLISHABLE_KEY will be undefined, causing the database connection to fail with "Missing required Supabase environment variables".

Prompt for agents
Update packages/database/scripts/createEnv.mts to transform ANON_KEY to SUPABASE_PUBLISHABLE_KEY instead of SUPABASE_ANON_KEY. In the makeLocalEnv function around line 37-45, add a transformation that converts ANON_KEY= to PUBLISHABLE_KEY= before prefixing with SUPABASE_, similar to how API_URL is handled. The transformation should result in SUPABASE_PUBLISHABLE_KEY being written to the .env.local file.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supabase outputs both the ANON_KEY and the PUBLISHABLE_KEY, once the latter have been set up.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +30 to +33
const makeFnEnv = (envTxt: string): string => {
return envTxt.split('\n').filter(l=>l.match(/^SUPABASE_\w+_KEY/)).map((l)=> l.replace('SUPABASE_', 'SB_')).join('\n');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug: The makeFnEnv function transforms SUPABASE_SERVICE_ROLE_KEY to SB_SERVICE_ROLE_KEY, but the edge functions expect SB_SECRET_KEY. This mismatch will cause the edge functions to fail with "Missing SUPABASE_URL or SB_SECRET_KEY" errors.

The regex pattern matches SUPABASE_SERVICE_ROLE_KEY and replaces the prefix to create SB_SERVICE_ROLE_KEY, but create-space/index.ts (line 215) and create-group/index.ts (line 53) both look for SB_SECRET_KEY.

Fix:

const makeFnEnv = (envTxt: string): string => {
  return envTxt.split('\n')
    .filter(l=>l.match(/^SUPABASE_\w+_KEY/))
    .map((l)=> l.replace('SUPABASE_SERVICE_ROLE_KEY', 'SB_SECRET_KEY').replace('SUPABASE_PUBLISHABLE_KEY', 'SB_PUBLISHABLE_KEY'))
    .join('\n');
}
Suggested change
const makeFnEnv = (envTxt: string): string => {
return envTxt.split('\n').filter(l=>l.match(/^SUPABASE_\w+_KEY/)).map((l)=> l.replace('SUPABASE_', 'SB_')).join('\n');
}
const makeFnEnv = (envTxt: string): string => {
return envTxt.split('\n').filter(l=>l.match(/^SUPABASE_\w+_KEY/)).map((l)=> l.replace('SUPABASE_SERVICE_ROLE_KEY', 'SB_SECRET_KEY').replace('SUPABASE_PUBLISHABLE_KEY', 'SB_PUBLISHABLE_KEY')).join('\n');
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code as stated does transform SUPABASE_SERVICE_ROLE_KEY into SB_SERVICE_ROLE_KEY, but also SUPABASE_SECRET_KEY into SB_SECRET_KEY, which is what the functions now use. Incidentally, SUPABASE_SERVICE_ROLE_KEY is still provided by the function environment irrespective of whether it's in the .env file.

@maparent maparent force-pushed the eng-925-upgrade-supabase-jwt-keys branch from 7a0a3c4 to 99e4c4b Compare February 6, 2026 22:52
@maparent maparent force-pushed the eng-925-upgrade-supabase-jwt-keys branch from 99e4c4b to bc00faa Compare February 7, 2026 18:11
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

🐛 1 issue in files not directly in the diff

🐛 /api/supabase/env route still returns SUPABASE_ANON_KEY but consumers expect SUPABASE_PUBLISHABLE_KEY (apps/website/app/api/supabase/env/route.ts:9-14)

The env API route was not migrated to the new key name, breaking the Roam production build pipeline.

Root Cause

apps/website/app/api/supabase/env/route.ts:9-14 still destructures and returns SUPABASE_ANON_KEY from process.env. This endpoint is called during the Roam build (packages/database/scripts/createEnv.mts:131), which fetches from https://discoursegraphs.com/api/supabase/env and writes the result to .env. The .env file will therefore contain SUPABASE_ANON_KEY=....

When envContents() at packages/database/src/dbDotEnv.mjs:82-83 reads and parses this file, the result will have SUPABASE_ANON_KEY as a key. But the compile scripts (apps/roam/scripts/compile.ts:165, apps/obsidian/scripts/compile.ts:126) now access dbEnv.SUPABASE_PUBLISHABLE_KEY, which will be undefined.

Impact: The Roam and Obsidian production builds (when triggered via ROAM_BUILD_SCRIPT) will embed null for the Supabase key, breaking all Supabase client initialization in the built extensions.

View 9 additional findings in Devin Review.

Open in Devin Review

[functions.create-space]
enabled = true
verify_jwt = true
verify_jwt = false

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 create-space function has verify_jwt = false but no manual authentication check, allowing unauthenticated access

The create-space edge function was changed from verify_jwt = true to verify_jwt = false without adding any manual JWT verification, unlike create-group which does verify tokens.

Root Cause

In packages/database/supabase/config.toml:328, verify_jwt was changed from true to false for the create-space function. Previously, Supabase would automatically reject requests without a valid JWT. Now any unauthenticated request can reach the function.

The create-space function at packages/database/supabase/functions/create-space/index.ts:225 creates a Supabase client with the service role key (which bypasses RLS) and proceeds to create spaces, auth users, platform accounts, and space access records without verifying the caller's identity.

In contrast, the create-group function (also set to verify_jwt = false) manually verifies the JWT via supabase.auth.getClaims(token) at packages/database/supabase/functions/create-group/index.ts:74 and rejects invalid tokens.

Impact: Anyone who knows the function URL can create arbitrary spaces, anonymous users, and associated records without any authentication. This is a security vulnerability that could be exploited for abuse.

Prompt for agents
The create-space function at packages/database/supabase/config.toml line 328 has verify_jwt set to false, but the function handler at packages/database/supabase/functions/create-space/index.ts does not perform any manual JWT/authentication verification. Either revert verify_jwt to true, or add manual JWT verification in the create-space handler similar to how create-group does it (using supabase.auth.getClaims to validate the Authorization header before proceeding).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

if (!SUPABASE_URL || !SUPABASE_ANON_KEY)
const { SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_PUBLISHABLE_KEY } =
process.env;
if (!SUPABASE_URL || !SUPABASE_ANON_KEY || !SUPABASE_PUBLISHABLE_KEY)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Env route requires deprecated SUPABASE_ANON_KEY, will return 500 when it's removed

The /api/supabase/env route now requires ALL three environment variables (SUPABASE_URL, SUPABASE_ANON_KEY, AND SUPABASE_PUBLISHABLE_KEY) to be present. Since the entire purpose of this PR is to migrate from SUPABASE_ANON_KEY to SUPABASE_PUBLISHABLE_KEY, when the old key is eventually removed from the server environment, this route will return a 500 error for all callers.

Root Cause and Impact

The guard at line 11 checks:

if (!SUPABASE_URL || !SUPABASE_ANON_KEY || !SUPABASE_PUBLISHABLE_KEY)
  return new NextResponse("Missing variables", { status: 500 });

This route is called by the Roam build script at packages/database/scripts/createEnv.mts:131:

const response = execSync('curl https://discoursegraphs.com/api/supabase/env');

No downstream code reads SUPABASE_ANON_KEY from this route's response anymore — every consumer was migrated to SUPABASE_PUBLISHABLE_KEY. Yet the route will fail entirely if the deprecated key is removed from the environment.

Impact: When SUPABASE_ANON_KEY is removed from the Vercel production environment (the expected outcome of this migration), the Roam build script will fail, breaking production builds.

Prompt for agents
In apps/website/app/api/supabase/env/route.ts, the guard on line 11 should not require SUPABASE_ANON_KEY to be present, since the codebase is migrating away from it. Change the check to only require SUPABASE_URL and SUPABASE_PUBLISHABLE_KEY. Also, make the response include SUPABASE_ANON_KEY only if it is defined (for backward compatibility with old clients), rather than requiring it. For example:

if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY)
  return new NextResponse("Missing variables", { status: 500 });
return NextResponse.json(
  { SUPABASE_URL, ...(SUPABASE_ANON_KEY ? { SUPABASE_ANON_KEY } : {}), SUPABASE_PUBLISHABLE_KEY },
  { status: 200 },
);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant