Another breaking change is coming to Supabase, and this one affects how your applications connect to the database. Starting October 30, 2026, new tables in the public schema will no longer be automatically exposed to the Data API. If you're running self-hosted Supabase or building applications on Supabase Cloud, you need to understand this change now.
We covered the June 2026 breaking changes (Postgres 17 default, Analytics opt-in, database role updates) last week. This October change is separate—and arguably more impactful for application developers.
What's Changing
Currently, when you create a table in the public schema, Supabase automatically grants SELECT, INSERT, UPDATE, and DELETE permissions to the anon, authenticated, and service_role roles. This makes tables immediately accessible via the Data API, GraphQL API, and client libraries like supabase-js.
After October 30, 2026, this automatic exposure stops. New tables require explicit GRANT statements before the Data API can see them.
The Timeline
- April 28, 2026: Opt-in available for new projects
- May 30, 2026: New behavior becomes default for all new projects
- October 30, 2026: Enforced on all existing projects
If you've created a project since May 30, you're already on the new behavior. If you're running an older project—including self-hosted deployments—October 30 is your deadline.
Why Supabase Made This Change
The convenience of automatic grants created security blind spots. Creating a table exposed it to the Data API immediately—before you could configure Row Level Security policies or review access patterns.
This became more dangerous as AI coding tools and automation entered the picture. An agent or CLI script creating tables expects them to be reachable via the API immediately—without a human reviewing the security implications.
Explicit grants make access a deliberate, code-level decision. You can't accidentally expose a table; you have to intentionally grant access.
What Breaks (And What Doesn't)
Existing tables are NOT affected. They keep their current grants and remain reachable through the Data API. The change only impacts tables created after the enforcement date.
New tables without explicit grants will break. PostgREST returns a clear error when a grant is missing:
{
"hint": "Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;",
"message": "permission denied for relation your_table"
}
The error includes the exact GRANT statement needed—helpful for debugging, but disruptive if it happens in production.
Who's Affected
You're affected if any of these apply:
- Your app reads or writes tables via the Data API (PostgREST, GraphQL,
supabase-js, direct HTTP to/rest/v1/or/graphql/v1/) - Your migrations create tables in
publicwithout explicitGRANTstatements - AI tools, CLI scripts, or the Management API create tables expecting immediate API access
- You use any ORM or database tool that creates tables on your behalf
For self-hosted deployments, this matters regardless of which Supabase version you're running. The Data API behavior change comes from PostgREST and applies universally.
How to Prepare Your Self-Hosted Instance
Step 1: Audit Your Current Tables
First, identify which tables currently have grants. Run this query to see what's exposed:
SELECT
grantee,
table_schema,
table_name,
privilege_type
FROM information_schema.table_privileges
WHERE table_schema = 'public'
AND grantee IN ('anon', 'authenticated', 'service_role')
ORDER BY table_name, grantee, privilege_type;
This shows your current state. Any table listed here remains accessible after October—no action needed for existing tables.
Step 2: Update Your Migration Workflow
Every migration that creates a table should now include explicit grants. Here's the pattern:
-- Create table
CREATE TABLE public.products (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name text NOT NULL,
price numeric(10,2) NOT NULL,
created_at timestamptz DEFAULT now()
);
-- Enable RLS (still required)
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
-- Explicit grants - choose what access you actually need
GRANT SELECT ON public.products TO anon;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.products TO authenticated;
GRANT ALL ON public.products TO service_role;
For tables that should NOT be accessible via the Data API—like internal audit logs or system tables—simply omit the grants. The table exists in the database but remains invisible to PostgREST.
Step 3: Review AI and Automation Workflows
If you use AI coding assistants, CLI scripts, or the Supabase Management API to create tables, those workflows need updating. Tools that worked yesterday will fail tomorrow if they don't include grants.
For MCP integrations, update your prompts or agent configurations to include grant statements when creating tables.
Step 4: Choose Your Default Strategy
You have two approaches:
Explicit per-table grants (recommended): Include grants in every migration. More verbose, but forces you to think about access for each table. This is the secure-by-default approach Supabase is pushing toward.
Blanket grants (convenience mode): Run a single statement to restore old behavior:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO anon, authenticated, service_role;
This opts you back into automatic exposure. Convenient, but you lose the security benefits of the change. Use this only if you have other controls in place (strict RLS, API gateway restrictions, etc.).
Grants vs. RLS: Understanding the Layers
This change highlights the difference between two security layers that often get conflated:
Grants control whether a role can access a table at all. Without a grant, the table doesn't exist as far as that role is concerned.
Row Level Security controls which rows a role can see or modify within tables they can access.
Both layers are necessary. A table with grants but no RLS is publicly readable. A table with RLS but no grants is invisible to the Data API entirely.
The recommended pattern:
-- Layer 1: Grants (table-level access) GRANT SELECT, INSERT ON public.messages TO authenticated; -- Layer 2: RLS (row-level access) ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can read own messages" ON public.messages FOR SELECT TO authenticated USING (user_id = auth.uid()); CREATE POLICY "Users can insert own messages" ON public.messages FOR INSERT TO authenticated WITH CHECK (user_id = auth.uid());
Migration Patterns for Self-Hosters
Pattern 1: Tables Exposed to Authenticated Users Only
Most application tables follow this pattern—only logged-in users should access them:
CREATE TABLE public.user_profiles (
id uuid PRIMARY KEY REFERENCES auth.users(id),
display_name text,
avatar_url text
);
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
-- Only authenticated users can access
GRANT SELECT, UPDATE ON public.user_profiles TO authenticated;
GRANT ALL ON public.user_profiles TO service_role;
-- Policy limits to own profile
CREATE POLICY "Users can view own profile"
ON public.user_profiles FOR SELECT
TO authenticated
USING (id = auth.uid());
Pattern 2: Public Read, Authenticated Write
For content that anyone can view but only logged-in users can create:
CREATE TABLE public.blog_posts (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
author_id uuid REFERENCES auth.users(id),
title text NOT NULL,
content text,
published boolean DEFAULT false
);
ALTER TABLE public.blog_posts ENABLE ROW LEVEL SECURITY;
-- Anonymous can read published posts
GRANT SELECT ON public.blog_posts TO anon;
-- Authenticated can read all and write
GRANT SELECT, INSERT, UPDATE, DELETE ON public.blog_posts TO authenticated;
GRANT ALL ON public.blog_posts TO service_role;
-- Anonymous sees only published
CREATE POLICY "Anyone can view published posts"
ON public.blog_posts FOR SELECT
TO anon
USING (published = true);
Pattern 3: Backend-Only Tables
Tables that should never be accessed via the Data API—internal operations, sensitive data, system state:
CREATE TABLE public.audit_log (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
action text NOT NULL,
user_id uuid,
details jsonb,
created_at timestamptz DEFAULT now()
);
-- No grants = invisible to Data API
-- Access only via service_role or direct database connections
GRANT ALL ON public.audit_log TO service_role;
The table exists and can be accessed by Edge Functions using the service role, but client-side code can never reach it.
Testing Before October
Don't wait until October 30 to discover problems. Test now:
Create a test project with the new behavior enabled (or spin up a fresh self-hosted instance)
Run your migrations against it
Test your application — every API call that reads or writes tables should work
Check for hidden dependencies — some ORMs or tools create tables automatically; make sure those paths include grants
For self-hosted deployments, you can simulate the new behavior by revoking default grants:
-- Simulate post-October behavior (careful in production!) REVOKE SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public FROM anon, authenticated;
Then test your application. Any broken flows need explicit grants added to your migrations.
Using Supascale Through the Transition
Managing grants across multiple self-hosted Supabase projects compounds the migration work. Each project needs migration updates, testing, and verification.
Supascale helps by:
- Providing a unified view across all your projects
- Backing up your databases before you make changes
- Enabling quick restore if a migration goes wrong
With one-time pricing, you're not paying extra during an intensive migration period. Set up proper backup procedures before making any grant changes.
Common Mistakes to Avoid
Granting everything to everyone: Don't just run a blanket GRANT ALL to avoid dealing with this. You'll lose the security improvement that Supabase is trying to achieve.
Forgetting service_role: Backend services and Edge Functions that bypass RLS still need grants. Include service_role in your grant statements for tables those services access.
Not updating seed scripts: If you use database seeding scripts, those need grants too. Tables created during seeding are new tables.
Assuming existing tables are safe: Existing tables keep their grants, but if you drop and recreate a table (common in some migration strategies), the new version won't have grants.
The Security Upside
This change makes Supabase more secure by default. Consider the pre-October scenario:
- You create a table for internal use
- You forget to configure RLS
- The table is immediately exposed to the Data API
- Anyone with your
anonkey can read it
Post-October:
- You create a table for internal use
- You don't add grants (correctly, since it's internal)
- The table is invisible to the Data API
- Even if you forget RLS, the table isn't exposed
The explicit grant requirement creates a deliberate step where you decide: should this table be accessible via the API?
