The answer. The Data API serves every table, view and function in the schemas listed under "Exposed schemas" (public, storage and graphql_public by default), and it publishes an OpenAPI description of all of it to anyone who presents the publishable key at /rest/v1/. Three consequences. Helper functions and internal tables belong in schemas that are not exposed; they are then unreachable even with the secret key. New tables should not become readable by accident: revoke the default privileges so a table needs an explicit grant before anon or authenticated can touch it, and remember a table created with raw SQL has RLS off. Views are not protected by the base table's policies: a view owned by postgres behaves like a definer function unless created with (security_invoker = on), and materialized views cannot enforce RLS at all.
The pattern.
alter default privileges for role postgres in schema public
revoke select, insert, update, delete on tables from anon, authenticated, service_role; -- opt-in from now on
create schema api; -- exposed: the surface you designed
create schema internal; -- not exposed: helpers, ledgers, queues
create view api.open_tickets with (security_invoker = on) as
select id, title, tenant from public.tickets where status = 'open';
grant usage on schema api to authenticated; grant select on api.open_tickets to authenticated;
-- hide the OpenAPI document without disabling OpenAPI (which breaks the platform health check)
create or replace function public.mock_openapi() returns json language sql as $$ select '{"swagger":"2.0","info":{"title":"private"}}'::json $$;
alter role authenticator set pgrst.db_root_spec = 'public.mock_openapi'; notify pgrst, 'reload config';
Watch out.
- Removing a schema from the exposed list cuts off the secret key too; a Worker that queried it will start failing with 404s.
- A client selects a non-default schema with
createClient(url, key, { db: { schema: "api" } }); forgetting that yields "relation does not exist". - The advisor names the offenders:
rls_disabled_in_public(0013),security_definer_view(0010),materialized_view_in_api(0016),extension_in_public(0014).
Related: security-advisor-lints-to-fix-before-launch · security-definer-rpc-is-a-controlled-hole-in-rls · postgrest-pre-request-function-is-your-api-middleware