{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"supabase-in-2026-the-production-playbook-for-grants-rls-keys-connections-realtime-and-cost-m6rnj","url":"https://zyvop.com/supabase-in-2026-the-production-playbook-for-grants-rls-keys-connections-realtime-and-cost-m6rnj","title":"Supabase in 2026: The Production Playbook for Grants, RLS, Keys, Connections, Realtime and Cost","subtitle":"A practical production guide to Supabase grants, row-level security, API keys, Postgres connections, Realtime, Edge Functions and pricing.","tldr":"Prepare Supabase for production with this 2026 playbook: configure Data API grants, secure tables with Row Level Security, manage API keys and JWTs, choose the right Postgres connection pool, scale Realtime safely, understand Edge Function limits, and estimate hosting and usage costs.","keywords":["Backend Security","postgresql","row-level security","supabase","database performance","Tutorial"],"entities":["Shobit Singh","Backend Security","postgresql","row-level security","supabase","database performance","Tutorial","ZyVOP"],"keyTakeaways":["Supabase combines Postgres with services for authentication, generated APIs, realtime messaging, object storage and Edge Functions.","This guide skips the product tour and focuses on what can break deployments, expose data or raise costs.","The most urgent item has a deadline: on October 30, 2026, Supabase will stop automatically granting Data API access to newly created tables in existing projects."],"headings":["1. The October 30 change: explicit grants for the Data API","2. What you are actually renting: the architecture","3. Build one table the secure way","Test the policies with pgTAP","4. RLS patterns that bite in production","Unauthenticated requests make auth.uid() null","Do not authorize from user_metadata","Views can bypass underlying RLS","Security-definer helpers and policy recursion","Two smaller traps","5. Make RLS fast","6. Keys and tokens: the 2026 model","7. Connections: pick the method by where the code runs","8. Realtime: broadcast changes deliberately","9. Edge Functions: what the limits mean for design","10. What it costs","11. Dated changes to put on your calendar","12. Where Supabase is the wrong tool","13. Pre-launch checklist"],"outboundLinks":["https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically","https://supabase.com/docs/guides/api/securing-your-api","https://supabase.com/docs/guides/database/postgres/row-level-security","https://supabase.com/docs/guides/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c","https://supabase.com/docs/guides/getting-started/architecture","https://supabase.com/changelog/48048-self-hosted-supabase-envoy-becomes-the-default-api-gateway-b","https://supabase.com/blog/supabase-series-f","https://supabase.com/docs/guides/getting-started/api-keys","https://supabase.com/docs/guides/database/views","https://supabase.com/docs/guides/database/functions","https://supabase.com/docs/guides/database/postgres/row-level-security-performance","https://supabase.com/docs/guides/functions/auth","https://supabase.com/docs/guides/auth/signing-keys","https://supabase.com/docs/guides/auth/jwts","https://supabase.com/docs/guides/database/connecting-to-postgres","https://supabase.com/docs/guides/database/postgres-js","https://supabase.com/docs/guides/realtime/subscribing-to-database-changes","https://supabase.com/docs/guides/realtime/postgres-changes","https://supabase.com/docs/guides/realtime/authorization","https://supabase.com/docs/guides/functions/limits","https://supabase.com/pricing","https://supabase.com/changelog","https://supabase.com/changelog/supabase-server-adapters-deprecated","https://supabase.com/changelog?types=deprecation","https://supabase.com/changelog/multigres-private-alpha","https://supabase.com/changelog/orioledb-public-beta"],"contentText":"Supabase combines Postgres with services for authentication, generated APIs, realtime messaging, object storage and Edge Functions. This guide skips the product tour and focuses on what can break deployments, expose data or raise costs. The most urgent item has a deadline: on October 30, 2026, Supabase will stop automatically granting Data API access to newly created tables in existing projects. Explicit permissions must become part of your migration workflow. This guide uses Supabase documentation, changelog entries and pricing pages checked on October 18, 2026. Recheck linked sources before applying time-sensitive settings because platform documentation and limits can change. Start with the problem you need to solve: Section 1 covers the grants deadline; Sections 3–5 cover RLS and policy performance; Section 6 covers keys and JWTs; Sections 7–8 cover connections and Realtime; Sections 9–10 cover Edge Functions and costs. 1. The October 30 change: explicit grants for the Data API Supabase announced in its April 28 changelog entry that new tables in the public schema will no longer be exposed to the Data API automatically. The behavior became the default for new projects on May 30, 2026, and is scheduled to apply to existing projects on October 30, 2026. Existing tables keep their current grants. New tables need explicit grants before PostgREST, GraphQL or supabase-js can access them. This affects apps that use Supabase client libraries or call /rest/v1/ or /graphql/v1. If your app connects to Postgres only through a connection string, ORM or server-side driver, this particular Data API change does not directly affect that connection. If a Data API request lacks the required grant, PostgREST can return SQLSTATE 42501 with a hint about the missing GRANT. Date Milestone What to do 2026-04-28 Opt-in behavior announced Test the change on a branch or staging project 2026-05-30 New-project default begins rolling out Update provisioning and migration templates 2026-10-23 Final email notice scheduled for owners and admins Complete the access audit 2026-10-30 Applies to existing projects Ensure every exposed object has intentional grants Source: Supabase changelog: tables are no longer exposed automatically. You can adopt the new behavior before October 30. Supabase's API security guide documents the statements below. Test them on a branch or staging project first. The final statement is not schema-scoped: a schema-specific revoke cannot remove a privilege inherited from PostgreSQL's global default for functions. -- Tables created by postgres in public do not get automatic Data API table grants. alter default privileges for role postgres in schema public revoke select, insert, update, delete on tables from anon, authenticated, service_role; -- New sequences do not get automatic grants to the Data API roles. alter default privileges for role postgres in schema public revoke usage, select on sequences from anon, authenticated, service_role; -- New functions created by postgres in public do not get automatic EXECUTE grants -- to these API roles. alter default privileges for role postgres in schema public revoke execute on functions from anon, authenticated, service_role; -- This global default revoke prevents new postgres-owned functions from inheriting -- EXECUTE from PUBLIC. It is deliberately not limited to the public schema. alter default privileges for role postgres revoke execute on functions from public;These statements affect only future objects created by the postgres role. They do not remove grants from existing objects, so audit those separately and grant only the access each one needs. PostgreSQL default privileges depend on the role and schema. Check your project's current defaults before applying the changes, especially if objects are created by other roles or you have customized grants. Before the deadline, audit ordinary and partitioned tables in public. The first query checks each privilege separately instead of passing a comma-separated list to has_table_privilege. select c.relname as table_name, c.relrowsecurity as rls_enabled, ( has_table_privilege('anon', c.oid, 'select') or has_table_privilege('anon', c.oid, 'insert') or has_table_privilege('anon', c.oid, 'update') or has_table_privilege('anon', c.oid, 'delete') ) as anon_has_any, ( has_table_privilege('authenticated', c.oid, 'select') or has_table_privilege('authenticated', c.oid, 'insert') or has_table_privilege('authenticated', c.oid, 'update') or has_table_privilege('authenticated', c.oid, 'delete') ) as authenticated_has_any from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'public' and c.relkind in ('r', 'p') order by c.relrowsecurity, c.relname; -- List RLS-enabled tables in public, including those with zero policies. select c.relname as table_name, count(p.policyname) as policy_count from pg_class c join pg_namespace n on n.oid = c.relnamespace left join pg_policies p on p.schemaname = n.nspname and p.tablename = c.relname where n.nspname = 'public' and c.relkind in ('r', 'p') and c.relrowsecurity group by c.relname order by policy_count, c.relname;The LEFT JOIN matters. A query that starts from pg_policies cannot show tables with zero policies because those tables have no policy rows. This query starts from the table catalog, so it includes them. This is a table-focused audit, not a complete exposure audit. Review views, functions, sequences, other API-exposed schemas, and grants inherited through roles separately. A table's grants and its RLS policies answer different questions: grants decide whether a role can access the table at all; RLS decides which rows it can access. Without RLS, a table in an exposed schema is accessible for every operation granted to the caller. With RLS enabled but no matching policy, ordinary Data API requests are denied by default. See Supabase's RLS guide and troubleshooting note about inherited privileges. 2. What you are actually renting: the architecture Every Supabase project has a Postgres database with services around it. The architecture guide shows Envoy routing requests to GoTrue, PostgREST, Realtime, Storage, postgres-meta, Edge Functions and pg_graphql. These services use the same database. Supabase does not replace Postgres with a proprietary data layer, so many security and performance choices are standard PostgreSQL choices. Component Role Implementation language noted in the architecture guide Postgres Database and query engine C GoTrue (Auth) Users, sessions and JWTs Go PostgREST Generates a REST API from the database schema Haskell Realtime WebSocket service for Broadcast, Presence and database changes Elixir Storage API Object storage API with metadata in Postgres Node.js / TypeScript Edge Functions Deno-based runtime for TypeScript and JavaScript TypeScript / Rust Supavisor Multi-tenant Postgres connection pooler Elixir Envoy API gateway C++ Source: Supabase architecture guide. Supabase says portability is a core principle. Its architecture guide describes compatibility between cloud and self-hosted deployments, with tools such as pg_dump and CSV for moving data. Self-hosted deployments are changing too. A July 17 changelog entry says Envoy is replacing Kong as the default API gateway, with rollout beginning the week of August 9, 2026. In its Series F announcement on June 4, 2026, Supabase reported a $500 million round led by GIC at a $10 billion pre-money valuation. The grants change addresses a risk that comes with faster development: scripts and AI tools can create tables faster than people review their permissions. A Data API request passes through four distinct checks: API key: identifies the project and key type. A publishable key does not identify a specific user. JWT: when present, identifies the user and provides claims, including the Postgres role for the request. Without a signed-in user, the request normally uses the anon role. SQL privileges: determine whether that role can access a table, view, sequence or function. RLS policies: determine which rows the role can read or change. Supabase's API keys guide explains that PostgreSQL checks privileges before RLS. A missing grant therefore causes a different result from a policy that filters out all rows. sequenceDiagram participant App as Browser app participant GW as API gateway participant PR as PostgREST participant PG as Postgres App-&gt;&gt;GW: Request with apikey header and, when signed in, user JWT GW-&gt;&gt;GW: Validate the API key GW-&gt;&gt;PR: Forward the request PR-&gt;&gt;PG: Execute as anon or the role named in the user JWT PG-&gt;&gt;PG: Check object privileges; fail if missing PG-&gt;&gt;PG: Apply RLS policies to candidate rows PG--&gt;&gt;PR: Authorized rows PR--&gt;&gt;App: JSON response3. Build one table the secure way Use one migration for each table. In it, create the table and indexes, enable RLS, grant only the required privileges, and add policies for the operations the app needs. Supabase recommends separate policies for SELECT, INSERT, UPDATE and DELETE, with the intended roles named in each TO clause. -- supabase/migrations/20261018120000_create_notes.sql create table public.notes ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users (id) on delete cascade, body text not null, created_at timestamptz not null default now() ); -- RLS filters by user_id, so index it. create index notes_user_id_idx on public.notes using btree (user_id); alter table public.notes enable row level security; -- Start from no direct table privileges for these roles, then add only what's needed. revoke all on table public.notes from anon, authenticated, service_role; grant select, insert, update, delete on table public.notes to authenticated; grant select, insert, update, delete on table public.notes to service_role; create policy \"notes_select_own\" on public.notes for select to authenticated using ((select auth.uid()) = user_id); create policy \"notes_insert_own\" on public.notes for insert to authenticated with check ((select auth.uid()) = user_id); create policy \"notes_update_own\" on public.notes for update to authenticated using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id); create policy \"notes_delete_own\" on public.notes for delete to authenticated using ((select auth.uid()) = user_id);Keep three details in mind. The user_id index helps queries and policies that filter by owner. The update policy uses both USING and WITH CHECK: USING limits which existing rows can be changed, while WITH CHECK prevents the caller from assigning a row to someone else. Every policy targets authenticated, so the anon role cannot use these policies to access rows. The service_role is privileged and bypasses RLS. Give it access only where trusted server-side code needs it, and never expose its key in browser or mobile code. RLS bypass does not replace SQL privileges: the role must still have the required object access. Test the policies with pgTAP Supabase's RLS guide documents database testing in supabase/tests, run with supabase test db. Create a test file with supabase test new notes_rls.test, then test both allowed and denied actions. The example below assumes the notes table and policies above are present in the test database. -- supabase/tests/notes_rls.test.sql begin; select plan(9); insert into auth.users (id, email) values ('11111111-1111-1111-1111-111111111111', 'owner@example.com'), ('22222222-2222-2222-2222-222222222222', 'other@example.com'); -- No table grant for anon: this fails before an RLS policy can permit access. set local role anon; select throws_ok( $$select * from public.notes$$, '42501', null, 'anon cannot read notes' ); -- The owner creates a row belonging to themselves. set local role authenticated; set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111'; select results_eq( $$insert into public.notes (user_id, body) values ('11111111-1111-1111-1111-111111111111', 'hello') returning body$$, array['hello'], 'owner inserts a note' ); -- The owner can update their own note. select results_eq( $$update public.notes set body = 'updated' where user_id = '11111111-1111-1111-1111-111111111111' and body = 'hello' returning body$$, array['updated'], 'owner updates their note' ); -- The owner cannot reassign that row to someone else. select throws_ok( $$update public.notes set user_id = '22222222-2222-2222-2222-222222222222' where user_id = '11111111-1111-1111-1111-111111111111'$$, '42501', null, 'owner cannot transfer ownership by updating user_id' ); -- A different signed-in user cannot insert a note for the owner. set local request.jwt.claim.sub = '22222222-2222-2222-2222-222222222222'; select throws_ok( $$insert into public.notes (user_id, body) values ('11111111-1111-1111-1111-111111111111', 'forged')$$, '42501', null, 'another user cannot insert a note for the owner' ); select is_empty( $$select * from public.notes$$, 'another user reads no notes' ); select is_empty( $$update public.notes set body = 'hacked' returning id$$, 'another user updates no notes' ); select is_empty( $$delete from public.notes returning id$$, 'another user deletes no notes' ); -- Confirm the owner's row remains intact. set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111'; select results_eq( $$select body from public.notes where user_id = '11111111-1111-1111-1111-111111111111'$$, array['updated'], 'owner note remains intact' ); select * from finish(); rollback;The nine assertions separate permission errors from operations that affect zero rows. A missing grant or a WITH CHECK violation can raise 42501, so the test uses throws_ok for those cases. A USING policy usually hides rows without raising an error; an update or delete then affects zero rows, so the test checks RETURNING and confirms the owner's row still exists. Run this test in your database-test environment. Adjust the auth.users fixture if your local schema or auth triggers require extra fields. A passing database test does not replace an end-to-end test of the actual Data API request with a user JWT. 4. RLS patterns that bite in production Unauthenticated requests make auth.uid() null Supabase says auth.uid() returns null when no user is signed in. Because null does not equal a user ID, an ownership condition will not match. A policy using TO authenticated already excludes anon. If a policy must serve both anonymous and signed-in users, add an explicit non-null check where needed. using ( (select auth.uid()) is not null and (select auth.uid()) = user_id )Do not authorize from user_metadata Do not use user-editable metadata such as raw_user_meta_data to decide access, because users may be able to change it from the client. For suitable authorization data, use trusted claims such as raw_app_meta_data. If access must be revoked immediately, check a database membership table instead. JWT claims can be stale, so a membership change stored only in a claim may not take effect until the token is refreshed or expires. Views can bypass underlying RLS By default, PostgreSQL checks access to a view's underlying tables using the view owner's privileges. If that owner is privileged, the view may reveal rows the caller could not read directly. On PostgreSQL 15 and later, set security_invoker = true when the view should follow the caller's permissions and RLS policies. On older versions, revoke API-role access to the view or keep it in an unexposed schema. Review the view's own grants separately. create view public.my_notes with (security_invoker = true) as select id, body, created_at from public.notes; grant select on table public.my_notes to authenticated;Source: Supabase views guide. Security-definer helpers and policy recursion If policies on two tables query each other, PostgreSQL can raise 42P17 for infinite recursion. One solution is a carefully designed SECURITY DEFINER helper in a schema that the Data API does not expose. Set search_path to an empty string and qualify every object name. Restrict who can execute the helper: it runs with its owner's privileges, so careless access can create a privilege-escalation risk. create schema if not exists private; create function private.team_ids_for_current_user() returns setof uuid language sql security definer set search_path = '' stable as $$ select team_id from public.team_members where user_id = (select auth.uid()) $$; revoke execute on function private.team_ids_for_current_user() from public; grant usage on schema private to authenticated; grant execute on function private.team_ids_for_current_user() to authenticated; create policy \"members read team notes\" on public.team_notes for select to authenticated using ( team_id in (select private.team_ids_for_current_user()) );This example assumes public.team_members uses the intended membership model and that the function owner can read it. A SECURITY DEFINER function does not make a policy safe by itself. Check the caller's identity and access inside the function, restrict EXECUTE, and test with real roles. See Supabase's database functions guide. Two smaller traps An UPDATE usually also needs a matching SELECT policy to behave as expected. A secret key also does not guarantee RLS bypass when the request includes a user's access token. With a user JWT attached, the request may run under that user's role and policies, even if the client was created with a secret key. This can explain why some server-side admin calls return only a subset of rows. 5. Make RLS fast RLS policies run as part of each query. Their cost depends on how many candidate rows the query scans and how much work each policy does. Start with Supabase's RLS performance guide and RLS guide. First, index columns used in row filters. A composite primary key on (team_id, user_id) does not provide an index that starts with user_id. Add one if your queries filter by that column. create index team_members_user_id_idx on public.team_members using btree (user_id);Second, wrap helper calls that do not depend on the current row in a scalar subquery. For example, (select auth.uid()) can let PostgreSQL evaluate the function once as an initPlan and reuse the result within the statement. Third, limit each policy to the intended role with TO authenticated or another role. Fourth, repeat the relevant filter in the application query when appropriate. This can help the planner narrow the rows it checks, while RLS remains the security boundary. // Repeat the ownership condition intentionally; RLS remains the security boundary. const { data, error } = await supabase .from('notes') .select() .eq('user_id', userId) if (error) throw errorAvoid expensive joins inside policy expressions when a set-based membership check can do the job. Selecting the allowed team IDs may be easier for the planner than joining membership rows to every candidate row. Test the pattern for your schema with EXPLAIN ANALYZE; a set containing thousands of IDs is not automatically faster. -- One possible set-based pattern for a membership policy. using ( team_id in ( select team_id from public.team_user where user_id = (select auth.uid()) ) )Measure instead of guessing. In a test environment, use realistic data and compare the same query under the role your app uses. Check Rows Removed by Filter when a policy rejects many scanned rows. begin; set local role authenticated; set local request.jwt.claims = '{\"role\":\"authenticated\",\"sub\":\"5950b438-b07c-4012-8190-6ce79e4bd8e5\"}'; explain analyze select count(*) from public.notes; rollback;Use a real test user ID and run this only in a safe environment. Testing as an administrator does not show how the query behaves under your application's role and JWT claims. 6. Keys and tokens: the 2026 model Supabase API keys identify the project or calling application; authentication identifies the individual user. These are separate jobs. The API keys guide describes four key types: Key Format Intended use Typical Postgres role Publishable sb_publishable_... Browser, mobile app and other public client code anon, or authenticated when a user JWT is present Secret sb_secret_... Trusted servers, Edge Functions and controlled jobs service_role, which bypasses RLS anon (legacy) Long-lived JWT Legacy equivalent of the publishable key anon service_role (legacy) Long-lived JWT Legacy equivalent of the secret key service_role Supabase plans to deprecate the legacy anon and service_role keys by the end of 2026. Both key systems work side by side during the transition. Create the new keys, update your clients, and check Settings → API Keys for last-used activity. Deactivate old keys only after confirming that nothing depends on them, and test the change before applying it in production. Secret keys have extra safeguards compared with the older JWT-based service-role key. Supabase can return HTTP 401 when the User-Agent header identifies a browser request using a secret key. Treat this as an extra safeguard, not a security boundary. Never place a secret key in frontend code, a mobile app, a public repository or client-side configuration. Where practical, use a separate secret key for each backend component so you can rotate one without affecting the others. Never log keys; store a cryptographic hash if you need to identify a key without retaining the key itself. Publishable and secret keys are not JWTs, so send them in the apikey header, not as a Bearer user token. The verify_jwt setting does not turn an API key into a user identity. Your function must still authorize its caller. Supabase's @supabase/server guide shows how to protect functions that require a secret key. import { withSupabase } from 'npm:@supabase/server@1' export default { fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) =&gt; { // This client is privileged and bypasses RLS. // Only use it after the SDK has authorized the secret-key caller. const { count, error } = await ctx.supabaseAdmin .from('notes') .select('id', { count: 'exact', head: true }) if (error) return Response.json({ error: 'Query failed' }, { status: 500 }) return Response.json({ notes: count }) }), }For a service-to-service function using withSupabase({ auth: 'secret' }), Supabase's current guide says to set verify_jwt = false because the wrapper validates the apikey credential. Keep verify_jwt = true for the usual user-JWT flow. Never disable platform verification without adding the intended protection. For external webhooks, verify the provider's signature yourself. # supabase/config.toml — only for a function whose withSupabase auth mode # validates the secret key, such as nightly-report. [functions.nightly-report] verify_jwt = falseSupabase supports a legacy shared JWT secret and a newer signing-keys system. The signing keys guide recommends asymmetric keys for production and covers ES256 and RS256. A backend can verify these tokens with public keys, without holding the signing secret. Rotation does not automatically sign out every user, but custom backends must handle cached keys correctly. Supabase's JWT guide explains that JWKS responses are cached, so key changes may take time to reach every verifier. Follow the documented waiting period before deleting a key that might still be cached. Supabase's services and your own backend may not see a change at the same time. For a standalone backend, use the jose library to verify a Supabase JWT against the project's JWKS endpoint if the project uses asymmetric signing keys. Check the issuer against a real token; a custom domain can change the issuer URL. import { createRemoteJWKSet, jwtVerify } from 'jose' const issuer = `${process.env.SUPABASE_URL}/auth/v1` const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)) export async function verifyAccessToken(token: string) { const { payload } = await jwtVerify(token, jwks, { issuer }) return { userId: payload.sub, role: payload.role, claims: payload, } }JWKS works with asymmetric signing keys. If your project still uses an HS256 shared secret, follow Supabase's current token-validation guidance instead; the JWKS endpoint cannot verify a token signed with that shared secret. 7. Connections: pick the method by where the code runs Supabase's connection guide matches connection methods to different runtimes. The shared Supavisor pooler supports IPv4 on every plan. Direct connections generally require IPv6 unless you have the paid IPv4 add-on. Check these network requirements first when troubleshooting connection failures. Where the code runs Typical choice Port Notes Browser or mobile frontend Data API None Use RLS; never embed database credentials Serverless or edge function using a Postgres driver Shared pooler, transaction mode 6543 Suited to many short-lived connections; transaction-mode limitations apply Persistent backend with IPv6 or paid IPv4 add-on Direct connection 5432 Appropriate for long-lived sessions and native database tools Persistent backend on IPv4-only network Shared pooler, session mode 5432 Keeps a session pinned while connected BI tool or database GUI Shared pooler, session mode 5432 Useful when the client expects session features or prepared statements High-throughput workload on a paid plan Dedicated pooler, where appropriate 6543 Lower network latency than the shared pooler; still account for database limits Migrations, pg_dump and replication tasks Direct connection, when reachable 5432 Prefer a connection mode that supports the required native operation Transaction pooling returns a connection to the pool after each transaction, so session state does not persist. Session-level SET and RESET, advisory locks, LISTEN and NOTIFY, temporary tables that outlive a transaction, and WITH HOLD cursors may not work as expected. Prepared statements and query pipelining also need driver-specific treatment. Postgres.js has an important caveat. Supabase's Postgres.js guide warns that its default query pipelining can hang queries or return mismatched rows with the shared transaction pooler. The guide says there is no working direct setting to disable pipelining; max_pipeline: 0 can break sql.begin() transactions. prepare: false disables prepared statements, but it does not fix pipelining. Do not treat it as a complete Postgres.js transaction-pooling fix. If serverless code can use the Data API, supabase-js is often simpler: it uses HTTPS, and Supabase manages the API's database connections. If you need direct SQL, choose a driver and configuration documented for your runtime and pooler. Check the current connection guide before using shared transaction mode with any driver. For a persistent Node.js backend using a direct connection or session-mode pooler, create one client at module scope to reuse connections across requests. Set the pool size based on the project's connection budget rather than copying a sample value: import postgres from 'postgres' // Example for a long-lived backend using a direct connection or session pooler. // Do not reuse this as a generic transaction-pooler recipe. export const sql = postgres(process.env.DATABASE_URL!, { max: 5, ssl: 'require', })Keep the combined pool size across all app instances within the database's connection limit, with room for Supabase services and administration. Even five connections per instance can be too many if your platform scales to dozens of instances. Driver or ORM Common transaction-pooler consideration Postgres.js Review the warning above; prepare: false does not resolve query pipelining Drizzle Behavior depends on the underlying driver; apply that driver's pooling constraints Prisma Follow current Prisma/Supabase guidance for the deployed version and connection string asyncpg The Supabase connection guide lists statement_cache_size=0 where needed JDBC The connection guide lists prepareThreshold=0 for relevant driver configurations Sources: Supabase connection guide and Postgres.js guide. 8. Realtime: broadcast changes deliberately Supabase Realtime offers Broadcast, Presence and Postgres Changes. For database events, the subscribing guide recommends Broadcast for better scalability and security. Postgres Changes is simpler to start with but can become costly when many clients subscribe to the same events. With Postgres Changes, Supabase checks each event against every subscriber. One event sent to 100 subscribers can require 100 authorization checks. Processing also follows a single-threaded path to preserve order, so more database compute does not automatically solve the bottleneck. If many clients subscribe to the same changes, consider Broadcast early instead of relying on a compute upgrade. Two details are easy to miss. RLS policies are not applied to DELETE events in the same way as other row events because the deleted row is no longer available for a normal access check. Also, REPLICA IDENTITY FULL is not a guarantee that clients will receive every old column: when RLS is enabled, payload.old can still contain only the primary key. See the current Postgres Changes guide for the exact behavior and trade-offs. The example below broadcasts changes on a private topic and uses a policy to limit who can subscribe. Replace room_topic and the membership fields with names from your schema. The function is SECURITY DEFINER, so it pins search_path, qualifies database objects and revokes direct execution from API roles. Keep the private schema out of the Data API's exposed schemas. create schema if not exists private; create function private.broadcast_chat_message() returns trigger language plpgsql security definer set search_path = '' as $$ begin perform realtime.broadcast_changes( coalesce(new.room_topic, old.room_topic), tg_op, tg_op, tg_table_name, tg_table_schema, new, old ); return null; end; $$; -- The trigger does not require API roles to call this function directly. revoke execute on function private.broadcast_chat_message() from public, anon, authenticated, service_role; create trigger chat_messages_broadcast after insert or update or delete on public.chat_messages for each row execute function private.broadcast_chat_message(); -- Only members of a room may receive broadcasts for that room. create policy \"members receive room broadcasts\" on realtime.messages for select to authenticated using ( realtime.messages.extension = 'broadcast' and exists ( select 1 from public.room_members m where m.user_id = (select auth.uid()) and m.room_topic = (select realtime.topic()) ) );The membership table also needs the right grants and RLS policy for users to read their own membership rows, where required. Set the channel to private on the client and turn off Allow public access in Realtime settings. Before deploying, confirm the function signature and policy against the current Broadcast guide. await supabase.realtime.setAuth() const channel = supabase .channel(roomTopic, { config: { private: true } }) .on('broadcast', { event: 'INSERT' }, (payload) =&gt; render(payload)) .subscribe((status, err) =&gt; { if (status !== 'SUBSCRIBED') console.error(err) })The Realtime Authorization guide says authorization policies are cached for the life of a connection and refreshed when a new JWT arrives. A removed member may keep access until the token refreshes or expires, depending on the connection and token lifecycle. Set token expiry to match your risk level, and test member removal on an active connection. Complex policies can also slow connections. 9. Edge Functions: what the limits mean for design Supabase Edge Functions run on Deno. The official limits page lists 256 MB of memory and two seconds of CPU time per request, excluding asynchronous I/O. The wall-clock limit is 150 seconds on Free and 400 seconds on paid plans; the request idle timeout is 150 seconds. Bundles can be up to 20 MB when built locally with the CLI, or 5 MB when bundled server-side through the Dashboard or Management API. Recursive or nested calls are limited to 30 requests per trace in 60 seconds. Use Edge Functions for request handling and light orchestration, such as webhooks, auth hooks, short API calls and simple coordination. Move CPU-heavy image processing and large batch jobs to a more suitable runtime. Two restrictions affect common workloads. Outbound SMTP on ports 25 and 587 is blocked, so use an email provider's HTTPS API instead of direct SMTP. Libraries that need unsupported multithreading or native image-processing dependencies—such as some sharp/libvips setups—may need another runtime or packaging approach. HTML responses are served as text/html only on custom domains; otherwise, a GET response with that content type is changed to text/plain. The runtime also lacks the Web Worker and Node vm APIs. 10. What it costs The prices below come from the Supabase pricing page, checked on October 18, 2026. Plans are billed per organization, and every project has its own Postgres compute instance. Check the current pricing page before budgeting because quotas and overage rates can change. Item Free Pro Team Base price $0 $25/month $599/month Database disk 500 MB 8 GB per project, then $0.125/GB Same as Pro Monthly active users 50,000 100,000, then $0.00325 each Same as Pro Uncached egress 5 GB 250 GB, then $0.09/GB Same as Pro Cached egress 5 GB 250 GB, then $0.03/GB Same as Pro File storage 1 GB 100 GB, then $0.0213/GB Same as Pro Edge Function invocations 500,000 2 million, then $2/million Same as Pro Realtime peak connections 200 500, then $10 per 1,000 Same as Pro Realtime messages 2 million 5 million, then $2.50/million Same as Pro Daily backups None 7 days 14 days Pausing After one week of inactivity No inactivity pause No inactivity pause Support Community Email Priority email with SLAs Compliance SOC 2 and ISO 27001 not included Not included SOC 2 and ISO 27001 included; HIPAA available as a paid add-on Source: Supabase pricing. Confirm plan-specific compliance terms and availability before relying on them for procurement or a regulatory decision. Compute can make the final bill higher than the plan's headline price. Paid plans include $10 per month in compute credits, enough to cover one Micro instance. Compute itself is billed hourly. Compute size Approx. monthly price Memory CPU Direct connections Pooler connections Micro $10 1 GB Shared 60 200 Small $15 2 GB Shared 90 400 Medium $60 4 GB Shared 120 600 Large $110 8 GB 2 dedicated vCPUs 160 800 XL $210 16 GB 4 dedicated vCPUs 240 1,000 These are the listed connection figures for the compute sizes shown; check the current pricing page for available sizes and exact limits. Connection counts are not a target pool size: keep application pools comfortably below database limits and account for all instances. The following estimates use the listed prices: plan fee plus compute cost minus the $10 compute credit. They exclude overages, taxes and egress. Setup Calculation Estimated monthly base Pro with Micro $25 + $10 - $10 $25 Pro with Small $25 + $15 - $10 $30 Pro with Micro for production and a second Micro project for staging $25 + $10 + $10 - $10 $35 Pro with Medium $25 + $60 - $10 $75 Pro with Large $25 + $110 - $10 $125 Pro with Large plus point-in-time recovery $125 + $100 $225 Three more details affect your bill. The Pro spend cap is enabled by default, so check how your organization handles overages before relying on automatic scaling. Point-in-time recovery starts at $100 per month for seven days of retention, and custom domains add $10 per month per project. The Free plan suits prototypes and small experiments: projects may pause after a week of inactivity, only a limited number of projects can be active, and automatic backups are not included. Confirm these details on the current pricing page before launch. 11. Dated changes to put on your calendar These dates come from the Supabase changelog and API keys guide. Confirm each deadline in the linked announcement before acting. Date Change Action 2026-10-30 Data API default-grant change applies to existing projects Audit table access and include grants in migrations 2026-12-01 Hono, H3, Elysia and NestJS adapters in @supabase/server are removed Move to the supported framework bridges By the end of 2026 Legacy anon and service_role keys are deprecated Migrate to publishable and secret keys 2027-01-31 supabase-js requires TypeScript 5.0 or later Upgrade older TypeScript toolchains The framework-adapter date and TypeScript requirement are documented in the adapter deprecation notice and supabase-js deprecation notice. Keep Node.js requirements and supported framework integrations in view when upgrading client libraries. Two database projects are worth watching, but avoid relying on them in production too soon. Multigres, an open-source Postgres system with multi-node failover and pooling, entered private alpha on October 1. Its announcement says it is not production-ready and has no uptime SLA; check its current status before use. OrioleDB reached public beta around the same time. Evaluate both only when their support and limitations meet your needs. 12. Where Supabase is the wrong tool Postgres Changes may not suit workloads that send each database event to many subscribers; Section 8 explains when to use Broadcast instead. CPU-heavy or long-running jobs can also exceed Edge Functions' limits. Run those jobs on a worker or job runner designed for them. If your service requires automatic multi-node failover, do not build production around Multigres until it is generally available and offers guarantees that meet your needs. One advantage is portability. Supabase uses standard Postgres and familiar tools, so you can migrate if your needs change. Grants, RLS, SQL migrations and connection-pooling practices also apply to other Postgres hosts. 13. Pre-launch checklist Check How Reference RLS on every exposed table Run the catalog audit in Section 1 RLS guide Explicit grants for each role Grant only required operations; test them Securing your API Tables with zero policies reviewed Use the LEFT JOIN query in Section 1 RLS guide Views and functions audited separately Review invoker security, grants and execute privileges Views guide Policy tests pass Test both successful and denied actions with supabase test db RLS guide Security-definer functions constrained Pin search_path, qualify names and restrict EXECUTE Database functions guide No policy relies on user-editable metadata Use trusted claims or database membership checks RLS guide Publishable and secret keys used correctly Publishable keys in clients; secret keys only on trusted servers API keys guide Legacy keys deactivated safely Check last use and test before deactivation API keys guide JWT verification matches signing mode Validate issuer and use JWKS only for asymmetric keys JWT guide Connection mode matches runtime and driver Test transaction-mode limitations and driver-specific behavior Connection guide Realtime fan-out is designed deliberately Evaluate Broadcast and private-channel authorization Realtime authorization Costs are understood Estimate compute, storage, uncached/cached egress, overages and recovery Pricing page Security Advisor reviewed Resolve findings or document why a finding is acceptable Securing your API First, make explicit grants part of migrations before October 30, 2026. Then migrate legacy API keys before year-end and confirm that each database driver works with its connection mode. Once the security and reliability basics are in place, tune performance and costs.","contentHash":"sha256:8228f394d68c9ddc692ec36d0a7491adbcb73437d3dc069430bf1bd3d755ac3e","authorName":"Shobit Singh","authorUrl":"https://zyvop.com/author/shobit","authorSameAs":[],"category":"Tutorial","tags":["Backend Security","postgresql","row-level security","supabase","database performance"],"audience":"Senior software engineers, systems architects, and technical leads working with Tutorial","tone":"Instructional, practical, code-first","readingTimeMinutes":26,"wordCount":5814,"faqs":null,"primaryTopic":"Tutorial","publishedAt":"2026-10-18T11:47:23.361Z","updatedAt":"2026-10-18T11:47:23.361Z","canonicalUrl":"https://zyvop.com/supabase-in-2026-the-production-playbook-for-grants-rls-keys-connections-realtime-and-cost-m6rnj"}