Supabase Is Deprecating anon and service_role Keys: How to Migrate Before End of 2026
Supabase is deprecating its legacy __INLINE_CODE_0__ and __INLINE_CODE_1__ API keys by the end of 2026. Existing projects can create the replacement keys now and run both systems side by side: __INLINE_CODE_2__ becomes a publishable key such as __INLINE_CODE_3__, while __INLINE_CODE_4__ becomes a secret key such as __INLINE_CODE_5__.
The migration looks simple until an application crosses trust boundaries. New keys are opaque strings rather than JWTs. Publishable keys still depend on Row Level Security for client safety. Secret keys still bypass RLS and belong only on trusted servers. Edge Functions need explicit authentication logic, and Database Webhooks or __INLINE_CODE_0__ calls that put a legacy __INLINE_CODE_1__ JWT in __INLINE_CODE_2__ need special attention.
A safe migration is therefore:
Create new keys alongside legacy keys
↓
Migrate public clients to publishable keys
↓
Migrate each trusted backend to its own secret key
↓
Fix Edge Function and webhook authentication
↓
Verify old mobile versions, CI, workers and integrations
↓
Deactivate legacy keys only when nothing still needs themThis guide focuses on the parts that can break production or weaken security even when a simple key replacement appears to work.
What is Supabase deprecating?
Supabase's current migration guide says the legacy __INLINE_CODE_0__ and __INLINE_CODE_1__ keys are being deprecated by the end of 2026.
The replacement mapping is:
| Legacy key | Replacement | Intended location |
|---|---|---|
| __INLINE_CODE_0__ | Publishable key, __INLINE_CODE_0__ | Browser, mobile, desktop, CLI/public code |
| __INLINE_CODE_0__ | Secret key, __INLINE_CODE_0__ | Server, worker, Edge Function, trusted backend |
Supabase allows the old and new keys to coexist during migration. Older projects can create publishable and secret keys from Settings → API Keys without immediately disabling the legacy pair. That is the feature that makes a controlled migration possible rather than a one-shot credential rotation.
Do not interpret "deprecated by end of 2026" as "replace both values in one environment file tonight". The useful migration unit is each caller and its trust level.
Why is Supabase changing the key model?
The old Supabase API keys are JWTs. The newer publishable and secret keys are shorter opaque API-key strings rather than JWTs.
Supabase's API key documentation separates two concepts that are often blurred in application code:
API key
→ Which application/component is calling Supabase?
Supabase Auth session JWT
→ Which signed-in user is making the request?That distinction matters because a public web or mobile application needs both ideas at different times.
A publishable key identifies the application as a public client. After a user signs in, Supabase Auth supplies the user's session token and PostgreSQL evaluates the request as the authenticated user.
A secret key identifies a trusted backend component and maps to privileged server access.
The newer key system also makes backend key rotation more practical because you can create multiple named secret keys rather than sharing one global __INLINE_CODE_0__ credential across every service.
Is the new Supabase publishable key safe to expose?
Yes, publishable keys are designed to be used in public client code. They are not secrets.
That includes:
- web applications
- Flutter/iOS/Android applications
- desktop applications
- public-source clients
- CLIs distributed to users
But "safe to expose" does not mean "the database is safe regardless of configuration".
A publishable key receives low-privilege access and relies on PostgreSQL grants and Row Level Security to decide which data an anonymous or authenticated user can access.
The important architecture is:
Public app
↓
Publishable API key
↓
Supabase Auth user session, when signed in
↓
anon / authenticated Postgres role
↓
grants
↓
RLS policies
↓
allowed rowsIf a table containing sensitive data is exposed through the Data API without appropriate RLS, changing __INLINE_CODE_0__ to __INLINE_CODE_1__ does not repair that security model.
For a deeper RLS diagnosis workflow, Softotic already has a guide on fixing Supabase Storage 403 and Row Level Security failures.
Does a publishable key change existing RLS policies?
Normally, no. Supabase says publishable keys preserve the same low-privilege client model as the legacy __INLINE_CODE_0__ key.
Supabase's API-key documentation explains the effective Postgres roles:
| Request context | Effective role |
|---|---|
| Publishable key, no signed-in user | __INLINE_CODE_0__ |
| Publishable key + valid Supabase Auth user | __INLINE_CODE_0__ |
| Secret key | __INLINE_CODE_0__ |
So policies such as:
create policy "Users read own profile"
on public.profiles
for select
to authenticated
using ((select auth.uid()) = user_id);do not need to be rewritten merely because the public API credential changes.
What does need testing is the real application request path:
- public unauthenticated screens
- sign-up and sign-in
- authenticated CRUD
- Storage uploads
- Realtime subscriptions
- SSR requests
- background refresh/token handling
A key migration is a good time to catch accidental assumptions around user identity, but it should not require weakening RLS.
Is the new Supabase secret key equivalent to service_role?
It fills the same trusted-backend role, and it still bypasses Row Level Security.
A basic server migration looks like:
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!
)with:
SUPABASE_SECRET_KEY=sb_secret_...rather than a legacy __INLINE_CODE_0__ JWT.
The security rule remains unchanged:
A Supabase secret key must never be shipped to an untrusted client.
Supabase adds an extra protection that returns HTTP __INLINE_CODE_0__ when a secret key is used from a browser-like request with a __INLINE_CODE_1__, but Supabase explicitly warns that this is not a substitute for keeping the key server-side. A leaked key can still be used from other HTTP clients.
A secret key should be treated with the same seriousness as direct privileged database access.
Why separate secret keys by backend component?
The new key model lets you create multiple named secret keys.
That is operationally useful.
Instead of:
One service_role JWT
├── billing worker
├── admin portal
├── cron jobs
├── email worker
└── data pipelineyou can move toward:
secret:billing
secret:admin
secret:cron
secret:email
secret:data-pipelineIf one backend key leaks, you can rotate or delete that key without changing every other production service at the same time.
A practical inventory can include:
| Component | Key | Environment | RLS bypass needed? | Owner |
|---|---|---|---|---|
| Billing worker | __INLINE_CODE_0__ | Production | Yes | Payments |
| Admin API | __INLINE_CODE_0__ | Production | Yes | Backend |
| Reporting job | __INLINE_CODE_0__ | Production | Maybe | Data |
| Staging worker | separate staging key | Staging | Yes | Platform |
Do not create privileged secret keys by default for code that can safely operate under user-scoped RLS.
Least privilege still beats convenient privilege.
Why new Supabase API keys can break Authorization headers
The new key format is not a JWT.
That means code written around this old pattern deserves inspection:
Authorization: Bearer <service_role JWT>With the legacy system, __INLINE_CODE_0__ looked like a JWT and was often treated like one in integrations.
The new architecture separates headers:
apikey: sb_secret_...
Authorization: Bearer <actual user/session JWT when applicable>Supabase's current API key documentation recommends sending publishable and secret keys using the __INLINE_CODE_0__ header.
This is especially important when downstream code tries to parse __INLINE_CODE_0__ as a JWT. An opaque __INLINE_CODE_1__ string is not a user JWT and should not be treated as one.
The conceptual rule is:
apikey
→ application/service credential
Authorization
→ user JWT or another authentication token defined by the endpointThat separation makes the trust model easier to reason about.
Database Webhooks and pg_net need special attention
Supabase explicitly calls out Database Webhooks and direct __INLINE_CODE_0__ calls in its migration guide.
A legacy setup may look like:
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || service_role_key
)The new secret key should instead be sent on __INLINE_CODE_0__:
headers := jsonb_build_object(
'Content-Type', 'application/json',
'apikey', secret_key
)Do not hard-code the new secret into migration files or SQL functions.
Supabase recommends keeping sensitive values in Vault or another appropriate secret store and reading them at runtime.
This class of integration is easy to miss because the key may not exist anywhere in the web application repository. It can live inside:
- a Database Webhook
- SQL created months ago
- __INLINE_CODE_0__
- cron-triggered SQL
- a database migration
- a stored function
Your migration inventory must include the database itself.
What changes inside Supabase Edge Functions?
Supabase now provides new environment variables for Edge Functions alongside the legacy variables:
SUPABASE_PUBLISHABLE_KEYS
SUPABASE_SECRET_KEYSThe new variables are not plain strings. They contain JSON objects keyed by the key name.
A minimal read can look like:
const secretKeys = JSON.parse(
Deno.env.get('SUPABASE_SECRET_KEYS')!
)
const secretKey = secretKeys['default']If you created a separate key:
const billingKey = secretKeys['billing']Supabase still exposes the legacy variables during migration, so an Edge Function can be moved without forcing every other caller to change at the same moment.
Why verify_jwt is not enough for the new key model
This is one of the most important migration details.
Supabase's current authorization-header guidance explains that publishable and secret API keys are not user JWTs.
For migration compatibility, the Edge Functions platform can accept the newer API-key format through its platform verification path in some configurations, but Supabase warns that passing that check does not authenticate the caller as a user.
So do not design authorization like this:
request passed platform verify_jwt
→ therefore caller is authenticatedFor new Edge Functions, Supabase recommends explicit authorization in the handler, and its current Securing Edge Functions documentation uses __INLINE_CODE_0__ to make the intended caller type explicit.
The useful question is:
Who is allowed to call this function?
not:
Does this request contain something that the platform accepted as a key?
A cleaner Edge Function authorization model
Supabase's current server tooling supports authorization modes that distinguish user calls from backend calls.
Conceptually:
Function called by signed-in app user
→ require user authentication
→ use user-scoped Supabase client
→ RLS applies
Function called by trusted backend service
→ require secret API key
→ use privileged/admin client
Signed third-party webhook
→ verify provider signature
→ do not pretend Supabase user auth appliesThat is a much stronger boundary than one generic function with __INLINE_CODE_0__ and no replacement authorization.
A user-called function should continue to derive database access from the user's authenticated session wherever practical.
A service-to-service function can use a named secret key and can restrict which backend component is accepted.
An unauthenticated webhook should verify the webhook provider's signature or secret using that provider's scheme.
Do not confuse API-key migration with Supabase Auth JWT signing-key migration
These are separate migrations.
The new publishable and secret API keys are opaque application credentials. They are no longer JWTs tied to the project's legacy shared JWT secret.
Supabase Auth still issues user access tokens. Those user JWTs have their own signing-key lifecycle.
Supabase's migration guide explicitly recommends considering the newer JWT signing-key system after API-key migration, but one change does not automatically complete the other.
Think of the two projects separately:
Project A: API key migration
anon → publishable
service_role → secret
Project B: Auth token signing migration
legacy shared JWT secret
→ rotatable JWT signing keysKeeping them separate is useful for testing and rollback.
How should a web app migrate from anon to a publishable key?
A conventional browser app can migrate in small steps.
1. Create the publishable key
Keep __INLINE_CODE_0__ active.
2. Change the application environment
For example:
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...Then:
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)3. Test unauthenticated behaviour
Verify:
- public pages
- public reads that are intentionally allowed
- sign-up
- sign-in
- password reset
- OAuth callback
- anonymous-user flows if used
4. Test authenticated RLS
Verify user-scoped reads and writes as the actual signed-in user.
5. Test Storage and Realtime
These are often where hidden auth assumptions surface.
6. Deploy while the legacy key remains active
The old key can stay valid during observation.
7. Only deactivate the legacy key after every shipped client is accounted for
That last step is especially important for mobile applications.
Mobile apps make key deactivation harder
A web deployment can replace the public key for most users as soon as the new bundle is live.
A mobile application is different.
Old Android and iOS builds can remain installed for weeks or months.
Supabase's migration guide specifically warns teams not to forget mobile and desktop app versions already in users' hands when checking legacy-key usage.
Before deactivating __INLINE_CODE_0__, ask:
Which app versions embed the old key?
How many active users still run them?
Can they update?
Is a minimum app version enforced?
Would deactivation break sign-in or core data access?A safer mobile migration can be:
Release app version with publishable key
↓
observe adoption
↓
notify / encourage upgrade
↓
enforce minimum supported version if product policy allows
↓
deactivate legacy anon keyDo not turn API-key cleanup into a surprise outage for users on a still-supported app build.
Flutter migration example
For Flutter, the client key remains public configuration.
For example:
await Supabase.initialize(
url: const String.fromEnvironment('SUPABASE_URL'),
anonKey: const String.fromEnvironment(
'SUPABASE_PUBLISHABLE_KEY',
),
);The SDK parameter may still use a historical property name such as __INLINE_CODE_0__ depending on the client API version. What matters is the value and trust model, not the local variable name.
Use:
sb_publishable_...for the application.
Never put:
sb_secret_...inside:
- Dart source
- Android resources
- iOS configuration bundled into the app
- remote config readable by clients
- compiled assets
- a client-side __INLINE_CODE_0__
A secret delivered to the client is no longer secret.
How should SSR applications handle the new keys?
Server-side rendering can blur public and privileged contexts.
Keep two explicit clients.
User-scoped SSR client
Uses the publishable key plus the current user's session.
Purpose:
render data the current user is allowed to access
→ RLS appliesAdmin/server client
Uses a secret key in trusted backend code only.
Purpose:
privileged maintenance or business workflow
→ bypasses RLSDo not create one global client and sometimes attach a user session to it.
That makes it easy for a privileged credential to leak into user-scoped request handling or for a user Authorization header to overwrite assumptions in an admin client.
Trust boundaries should be visible in code.
How do you find every remaining legacy key?
Supabase's migration guide says there is currently no automatic usage indicator that can prove nothing still uses the legacy keys. Even where dashboard last-used signals help with individual key objects, they cannot discover old mobile builds, hidden scripts or external integrations on their own.
So this is still a manual inventory problem.
Search:
Source repositories
grep -R "SUPABASE_ANON_KEY" .
grep -R "SUPABASE_SERVICE_ROLE_KEY" .
grep -R "eyJ" .Be careful with the last search because many legitimate JWTs start similarly.
CI/CD
Check:
- GitHub Actions secrets
- GitLab CI variables
- deployment environments
- Vercel/Netlify variables
- Docker/Kubernetes secrets
- Terraform variables
- cloud secret managers
Runtime services
Check:
- API servers
- workers
- cron jobs
- queue consumers
- serverless functions
- data pipelines
- ETL tools
- admin scripts
Supabase/database configuration
Check:
- Edge Functions
- Database Webhooks
- __INLINE_CODE_0__
- Vault
- scheduled SQL
- stored procedures
External integrations
Check:
- automation platforms
- internal BI
- webhooks
- no-code tools
- external scripts
- staging environments
Shipped applications
Check:
- current web build
- supported Android releases
- supported iOS releases
- desktop applications
- CLI releases
Do not deactivate the old pair until this inventory is complete.
A safe zero-downtime migration sequence
The coexistence period enables a low-risk rollout.
Phase 1: create and classify
Create:
- one publishable key for public clients
- separate secret keys for trusted backend components where useful
Record owner and purpose.
Phase 2: migrate public web clients
Deploy publishable keys and verify Auth/RLS/Storage/Realtime.
Phase 3: migrate backend services one at a time
For each service:
add new secret
→ deploy
→ verify requests
→ watch errors/logs
→ remove old service_role value from that servicePhase 4: migrate Edge Functions
Update environment-variable handling and explicit authorization.
Phase 5: migrate database-originated HTTP calls
Fix __INLINE_CODE_0__ and Database Webhook headers.
Phase 6: migrate mobile/desktop releases
Wait for an acceptable adoption threshold or enforce a supported minimum version if appropriate.
Phase 7: search again
Repeat the legacy-key inventory.
Phase 8: deactivate legacy keys
Supabase says legacy keys can be reactivated if you discover a missed client, making deactivation a reversible final test.
Phase 9: delete stale secrets from deployment systems
A migration is not complete if the old __INLINE_CODE_0__ JWT still exists in twelve secret stores.
What should you monitor during cutover?
Track errors that reveal trust-boundary mistakes.
Useful signals include:
- HTTP __INLINE_CODE_0__
- HTTP __INLINE_CODE_0__
- __INLINE_CODE_0__
- RLS denials
- Edge Function authorization failures
- Storage upload failures
- Realtime connection failures
- backend jobs failing after secret rotation
- webhook delivery failures
- old mobile-app support tickets
Segment by application version and deployment component where possible.
A spike in __INLINE_CODE_0__ only on an older mobile version is a different problem from a backend worker suddenly receiving __INLINE_CODE_1__.
Common migration mistakes
Mistake 1: putting sb_secret in a frontend because the browser blocks it anyway
The browser block is defense in depth, not a security boundary. A leaked secret can be replayed from non-browser clients.
Mistake 2: replacing anon with a secret key to "fix RLS"
Secret keys bypass RLS. That removes the policy boundary instead of fixing it.
Mistake 3: sending sb_secret as a user Bearer token
New API keys are not user JWTs. Use the __INLINE_CODE_0__ header for the API key and __INLINE_CODE_1__ for actual user/session authentication where required.
Mistake 4: leaving Edge Functions with verify_jwt disabled and no handler authorization
Disabling a platform check is only safe when you replace it with an explicit authorization model.
Mistake 5: forgetting Database Webhooks and pg_net
Those credentials may live inside database configuration rather than application code.
Mistake 6: rotating every backend at once
Use separate named secret keys and migrate one service at a time.
Mistake 7: deactivating anon as soon as the web app is migrated
Old mobile and desktop versions can still depend on it.
Mistake 8: treating API-key migration as the JWT signing-key migration
They are separate projects with separate test surfaces.
A migration test matrix
| Test | Publishable client | Secret backend |
|---|---|---|
| Public allowed read | Test | N/A |
| Public denied read | Test | N/A |
| User sign-in | Test | N/A |
| Authenticated user RLS | Test | N/A |
| Storage upload | Test | As required |
| Realtime session | Test | As required |
| Admin read/write | N/A | Test |
| RLS bypass expected | No | Yes |
| Browser exposure | Expected | Forbidden |
| Edge Function user auth | Test | N/A |
| Edge Function service auth | N/A | Test |
| Database Webhook | N/A | Test |
| Key rotation | App rollout | Per-service |
| Old client compatibility | Critical | N/A |
The negative tests matter as much as the success tests.
Confirm that:
public client cannot read protected rows
user A cannot read user B data
browser cannot use privileged server key
unauthorised Edge Function caller is rejectedA migration that only proves successful requests can accidentally ship a broader security regression.
A 2026 migration checklist
- □New publishable and secret keys have been created alongside legacy keys.
- □Public clients use __INLINE_CODE_0__.
- □No client bundle contains __INLINE_CODE_0__.
- □Existing RLS policies have been tested under the publishable key.
- □Signed-in user flows still use Auth session JWTs.
- □Each trusted backend has an appropriate secret-key strategy.
- □Backend secrets are stored in managed secret storage.
- □Edge Functions use explicit authorization appropriate to the caller.
- □New API keys are sent through the correct __INLINE_CODE_0__ path.
- □Database Webhooks and __INLINE_CODE_0__ calls have been audited.
- □Secret values are not hard-coded in SQL or migrations.
- □CI/CD and deployment environments have been searched.
- □Cron jobs, workers and external integrations have been searched.
- □Supported mobile and desktop versions have been accounted for.
- □Realtime behaviour has been tested if used.
- □Old legacy credentials remain active until every caller is verified.
- □Legacy keys are deactivated only after the final inventory.
- □Old secrets are removed from deployment systems after cutover.
- □JWT signing-key migration is tracked separately if still required.
FAQs
When are Supabase anon and service_role keys being deprecated?
Supabase's current documentation says the legacy __INLINE_CODE_0__ and __INLINE_CODE_1__ keys are being deprecated by the end of 2026. Existing projects can create and use the replacement keys now while the legacy keys remain active during migration.
What replaces the Supabase anon key?
Use a publishable key with a format such as __INLINE_CODE_0__. It is intended for browsers, mobile apps, desktop apps and other public clients. It still relies on grants and RLS for data security.
What replaces the Supabase service_role key?
Use a secret key such as __INLINE_CODE_0__ in trusted backend code. Secret keys map to privileged service access and bypass RLS, so they must never be shipped in frontend or mobile applications.
Are Supabase publishable and secret keys JWTs?
No. The newer key formats are opaque API keys rather than JWTs. Supabase Auth user access tokens are still JWTs, which is why application API keys and user authentication should be handled as separate concepts.
Do I need to rewrite my RLS policies when replacing anon with a publishable key?
Normally no. Supabase says the publishable key uses the same low-privilege public-client model. An unauthenticated request maps to __INLINE_CODE_0__, and a signed-in user maps to __INLINE_CODE_1__. You should still regression-test RLS before deactivating the legacy key.
Why did my Edge Function break after changing service_role to sb_secret?
Common causes include treating the new secret key as a JWT, putting it in __INLINE_CODE_0__, relying on platform __INLINE_CODE_1__ as caller authentication, or reading the new Edge Function environment variable as a plain string rather than a named JSON object. Use __INLINE_CODE_2__ for API keys and implement explicit function authorization.
Conclusion
Supabase's 2026 API-key migration is not difficult because the new key names are complicated. It is difficult because the old JWT-shaped credentials encouraged several trust boundaries to collapse into one token.
The cleaner model is:
Publishable API key
→ identifies public application
→ RLS still protects data
User Auth JWT
→ identifies signed-in person
→ authenticated RLS applies
Secret API key
→ identifies trusted backend component
→ privileged access / RLS bypass
Webhook provider signature
→ authenticates external webhookIf you preserve those boundaries, the migration can be gradual and low risk.
Create the new keys now, move public clients first, give backend services independent secret keys where practical, update Edge Function authorization, audit database-originated HTTP calls, and leave the legacy keys active until old mobile versions and hidden integrations have been accounted for.
If your Supabase-backed product needs help separating client, RLS, Edge Function and privileged backend access cleanly, Softotic's custom software development service can help redesign the integration boundary. For customer-facing SaaS and dashboards, web application development can cover the application layer, while mobile app development can handle safe Flutter, Android and iOS key migration.