Supabase client with Secret Key is not compatible with local development
When developing locally, I created a supabase admin client with the Secret Key from supabase status
const SUPABASE_URL = "http://127.0.0.1:54321";
const SUPABASE_SECRET_KEY = "<local secret key>" // TODO
const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_SECRET_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
and tried to add some mock users with supabase.auth.admin.createUser, I got the following error message:
ā Failed to connect to Supabase: invalid JWT: unable to parse or verify signature, token is malformed: token contains an invalid number of segments
However, if I change the to the old Service Role Key (which no longer prints in version 2.51.0), this works just fine.
Minimally reproducible example: test-supabase-local.zip
index.ts
#!/usr/bin/env tsx
import { createClient, User } from '@supabase/supabase-js';
import { Database } from './database.js';
// Configuration for local Supabase development
const SUPABASE_URL = "http://127.0.0.1:54321";
const SUPABASE_SECRET_KEY = "sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz"
// Create Supabase client with service role key for admin operations
const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_SECRET_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
// Test users to insert
const testUsers = [
{
email: '[email protected]',
password: 'TestPassword123!',
user_metadata: {
full_name: 'Test User One',
username: 'testuser1'
}
},
{
email: '[email protected]',
password: 'TestPassword123!',
user_metadata: {
full_name: 'Test User Two',
username: 'testuser2'
}
},
{
email: '[email protected]',
password: 'TestPassword123!',
user_metadata: {
full_name: 'Test User Three',
username: 'testuser3'
}
}
];
async function insertTestUsers() {
console.log('š Starting test user insertion...');
console.log(`š Supabase URL: ${SUPABASE_URL}`);
for (const [index, userData] of testUsers.entries()) {
try {
console.log(`\nš¤ Creating user ${index + 1}: ${userData.email}`);
// Use admin.createUser for creating users with passwords
const { data, error } = await supabase.auth.admin.createUser({
email: userData.email,
password: userData.password,
user_metadata: userData.user_metadata,
email_confirm: true // Auto-confirm email for testing
});
if (error) {
console.error(`ā Failed to create user ${userData.email}:`, error.message);
continue;
}
if (data.user) {
console.log(`ā
Successfully created user: ${userData.email}`);
console.log(` User ID: ${data.user.id}`);
console.log(` Email confirmed: ${data.user.email_confirmed_at ? 'Yes' : 'No'}`);
}
} catch (error) {
console.error(`š„ Unexpected error creating user ${userData.email}:`, error);
}
}
console.log('\nš Test user insertion completed!');
}
async function listExistingUsers() {
try {
console.log('\nš Listing existing users...');
const { data, error } = await supabase.auth.admin.listUsers();
if (error) {
console.error('ā Failed to list users:', error.message);
return;
}
console.log(`Found ${data.users.length} users:`);
data.users.forEach((user: User, index: number) => {
console.log(` ${index + 1}. ${user.email} (ID: ${user.id})`);
});
} catch (error) {
console.error('š„ Unexpected error listing users:', error);
}
}
async function main() {
console.log('š§ Supabase Test User Insertion Script');
console.log('=====================================');
// Check if we can connect to Supabase
try {
const { data, error } = await supabase.auth.admin.listUsers({ page: 1, perPage: 1 });
if (error) {
console.error('ā Failed to connect to Supabase:', error.message);
console.log('\nš” Make sure:');
console.log(' 1. Supabase is running locally (supabase start)');
console.log(' 2. VITE_SUPABASE_URL is set correctly');
console.log(' 3. SUPABASE_SERVICE_ROLE_KEY is set correctly');
process.exit(1);
}
console.log('ā
Successfully connected to Supabase');
} catch (error) {
console.error('š„ Connection error:', error);
process.exit(1);
}
// List existing users first
await listExistingUsers();
// Insert test users
await insertTestUsers();
// List users again to show the results
await listExistingUsers();
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}
- I also tried using Python client and got the same error.
- I am on Supabase CLI 2.51.0 which is the latest version at the time of writing.
- I have tried to delete everything and reinstall but it didn't make a difference.
- In the previous thread (currently locked) #29260, local development compatibility has been fixed in a previous release, but I am not sure it specifically address the case I mentioned above.
- Is there any settings that I need to configure to allow the server to accept the new secret key?
Linking original discussion thread: https://github.com/orgs/supabase/discussions/39605. Please let me know if this is the appropriate place to open this issue or help me migrate it to an appropriate place.
Running into the same issue, old local service role key works fine but not the new secret key
Hi all, Is this happening for you on cli or self-hosted?
For me, it's in the local dev environment.
Is there anything I can do to help resolve this issue?
@Hallidayo this is on cli, local dev enrichment.
@sweatybridge
Transferred over from the supabase/supabase repo.
Can you try the latest cli release? I believe it's fixed by https://github.com/supabase/cli/pull/4489
npx supabase@latest start
I'm encountering this issue locally with Supabase CLI v2.72.0.
My setup has gotrue issuing ES256 tokens for users, but internal service tokens (e.g., for Edge Function workers) remain HS256.
The Problem
The Edge Runtime crashes with:
TypeError: Key for the ES256 algorithm must be of type CryptoKey. Received an instance of Uint8Array
This happens because the runtime defaults to initializing with the symmetric JWT_SECRET (Uint8Array), but receives an ES256 token.
Failed Attempt: Explicit signing_keys.json
I tried to fix this by explicitly configuring signing_keys.json in config.toml to provide the proper ES256 JWK.
- ES256 Only: If I provide only the ES256 key, user auth works, but internal services verification fails (e.g., database webhooks, cron jobs) because they use the default HS256 service key, which is no longer in the keyring (replaced by the JSON file).
-
Mixed Keys (ES256 + HS256): I attempted to include both keys in
signing_keys.jsonto support both users and services.[ { "alg": "HS256", "k": "...", ... }, { "alg": "ES256", ... } ]
Result: The CLI refuses to start with the error:
failed to decode signing keys: failed to parse response body: must be one of [RS256 ES256]
Conclusion It seems seemingly impossible to configure a "Mixed Environment" locally via signing_keys.json because the CLI validator rejects HS256 keys in that file. Currently, the only workaround is to disable verify_jwt in config.toml and manually verify tokens inside the Edge Function code.
so, is this getting fixed? Still happening
Still happening after upgrading to lates cli
Also #4726 š¤
I wanted to start a new fresh project and spent hours to try to make it work because of this silly bug... I will go to another backend platform
How the hell is this acceptable? Completely broken the local setup...
supabase status -o env
The secret key provided in supabase status command is NOT the actual secret key.
Use the above command to output the raw status and you will see the JWT.
Thank me later.
supabase status -o env
This gives me a new SERVICE_ROLE_KEY for each call, caused by an updated exp timestamp in the payload.
This is described in https://github.com/supabase/cli/issues/4751 - there seems to be a problem with asymmetric JWT api keys status indeed.
Also, to clarify the current secrets and keys configuration in the latest versions of CLI after it's switched recently to asymmetric "ES256" keys by default:
- New opaque api keys (
sb_publishable_andsb_secret_) are hardcoded in CLI - Asymmetric keypair is also hardcoded
- Auth and other services are now configured to use asymmetric encryption keys by default (you can check it via
docker inspectfor individual containers - look for something like_JWT_KEYS,JWT_JWKS, etc.) - There's still ANON_KEY and SERVICE_ROLE_KEY env-vars, but these are now ES256-signed JWTs (not the old HS256 symmetric ones). They're signed with the hardcoded asymmetric private key. Both are passed as part of container configuration to Functions and Storage š¤
- These updated asymmetric JWT api keys are generated on each
supabase start- and although the asymmetric keypair is hardcoded, the keys keep changing because a new timestamp is used on every start. - In Kong configuration there's a "translation" from
sb_api keys to role-containing asymmetric JWT api keys from # 4 above. You can check what's happening there viadocker exec -it supabase_kong_PROJ_NAME grep "sb_secret" /home/kong/kong.yml | head -1. Requests with minted user sessions asymmetric JWT tokens are just passed through. - The
JWT_SECRETstill exists for backward compatibility with HS256 tokens, but there's a misconfiguration at the CLI-vs-Auth level - currently being investigated. -
It is not advisable to extract and use the asymmetric JWT api keys in your application - those aren't supposed to be directly used for authentication/authorization. As I understand, the expectation has been that users start switching their application architectures to using the "new API keys" - the
sb_keys. This makessupabase status -o enva bit obsolete with regard to anon and service role api keys.
facing the same issue but for local edge functions
im facing
2026-01-24T19:42:23.173836546Z TypeError: Key for the ES256 algorithm must be of type CryptoKey. Received an instance of Uint8Array
2026-01-24T19:42:23.173877463Z at asymmetricTypeCheck (https://deno.land/x/[email protected]/lib/check_key_type.ts:14:11)
2026-01-24T19:42:23.173880213Z at checkKeyType (https://deno.land/x/[email protected]/lib/check_key_type.ts:39:5)
2026-01-24T19:42:23.173881296Z at flattenedVerify (https://deno.land/x/[email protected]/jws/flattened/verify.ts:78:3)
2026-01-24T19:42:23.173882296Z at compactVerify (https://deno.land/x/[email protected]/jws/compact/verify.ts:15:26)
2026-01-24T19:42:23.173883255Z at Module.jwtVerify (https://deno.land/x/[email protected]/jwt/verify.ts:5:26)
2026-01-24T19:42:23.173884296Z at verifyJWT (file:///var/tmp/sb-compile-edge-runtime/root/index.ts:95:16)
2026-01-24T19:42:23.173885296Z at Object.handler (file:///var/tmp/sb-compile-edge-runtime/root/index.ts:140:34)
2026-01-24T19:42:23.173886213Z at mapped (ext:runtime/http.js:231:42)
2026-01-24T19:42:23.173887130Z at respond (ext:runtime/http.js:339:14)
2026-01-24T19:42:23.173888046Z at handleHttp (ext:runtime/http.js:160:9)
Hi there, we've released v2.74.4 to address this issue with local dev. In summary, you should be able to
- Continue using legacy
anonandservice_rolekeys with supabase-js locally. These legacy keys are shown withsupabase status -o envcommand.
const supabase = createClient<Database>(
"http://127.0.0.1:54321",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
);
- Opt into the new publishable and secret keys that are shown under
supabase startcommand. These are drop-in replacements for the legacy keys you are using in supabase-js.
const supabase = createClient<Database>(
"http://127.0.0.1:54321",
"sb_secret_...",
);
With both keys enabled, you should be able to test the transition to new api keys fully locally. Once you are done testing, go to supabase dashboard and enable the new api keys. Your app should work seamlessly on next deploy.
For differences between legacy and new api keys, see also: https://github.com/orgs/supabase/discussions/29260