Supabase deep dive · Video 3
This deck turns the narration into a fuller visual walkthrough: who connects, what objects they may touch, and which rows count as theirs once row-level security is enabled. The pacing matches the full narration, and each concept gets a clear picture so you can present the entire video without compressing the details.
LOGIN roles, passwords, connection strings, and how requests arrive.
Schemas, tables, views, functions, sequences, and reusable group roles.
RLS narrows data to the current user, token, or request-local session value.
Video 1 was about connection choices, video 2 was about IPv4 and IPv6 network behavior, and this one is about authorization inside the database.
Section 1 · Core concept
In Postgres, user and group are not separate SQL primitives. They are both roles with different attributes. That is the foundation for everything else in the video.
Has LOGIN, usually has a password, can open a session, and can be named in a connection string.
Cannot connect directly, but can hold grants and membership so many child roles inherit the same access.
Section 1 · Group role picture
Group-style roles exist so you do not repeat the same GRANT statements for every service account. Attach permissions to the parent role, then attach members to that parent.
The visual emphasis is now the direction of permission flow: grants live on the left, member accounts consume them on the right.
Section 2 · Creating roles
The simplest role has no login and no password. That is ideal for group roles or roles you intend PostgREST to switch into later through JWT claims.
CREATE ROLE role_name;
Section 2 · Login roles
When something connects directly to Postgres with a URI, the role needs LOGIN. Backends, scripts, BI tools, and admin clients all fit that pattern.
CREATE ROLE app_backend WITH LOGIN PASSWORD 'your-strong-password';
The connection string is not connecting to "a user type". It is connecting to a role that happens to be allowed to log in.
Section 2 · Rotation
Provisioning and rotation are different moments in the same lifecycle. A role can stay the same while the secret changes underneath it.
ALTER ROLE app_backend WITH PASSWORD 'new-strong-password';
Issue the role with LOGIN and an initial secret.
Keep the credential in your vault or deployment environment.
Change the password without changing the role name.
Roll the new secret to the applications that use it.
Section 2 · URI encoding
Special characters are allowed in the password itself, but connection strings are URLs. Reserved characters need percent-encoding or the parser may split the URI incorrectly.
A good rule of thumb for demos is to avoid ending the password with @, since that often confuses people reading the string aloud.
Section 2 · Supabase studio
The default postgres role is your setup and migration account. Supabase lets you reset that password in Studio, and it is independent from the custom roles you create afterward.
postgres for setup, migrations, and emergencies. Do not use it as the everyday app credential.Section 3 · Fresh role behavior
A newly created role can still inspect system catalogs, so commands like \d+ can list tables. But that does not mean the role can read or write the table rows themselves.
The narration makes an important distinction here: schema and catalog visibility are not the same thing as row privileges.
Section 3 · Explicit vs inherited grant
If you explicitly grant schema USAGE to a role that already inherits it via PUBLIC, the effective access does not change much, but the ACL becomes easier to read because the role now gets its own visible entry.
Section 3 · Why public resolves
Supabase grants USAGE on the public schema to the special PUBLIC role by default. Every role inherits that baseline, so custom roles can resolve object names there even when they do not appear explicitly in the ACL list.
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'public';
The empty string before =U is Postgres notation for the PUBLIC role. That line applies to every role automatically.
Section 3 · Important exception
Supabase's built-in API roles such as anon and authenticated already come with grants configured by the platform. The "fresh role with almost no data access" story applies to roles you create yourself, not to these pre-wired roles.
Usually begins with catalog visibility and inherited schema basics, but no meaningful table access until you grant it.
Already participates in Supabase's API permission model, so object grants and RLS behavior are intentionally set up ahead of time.
anon or authenticated, they are observing Supabase's platform defaults. To understand true blank-slate behavior, create a brand-new custom role and test that instead.Section 3 · GRANT grammar
Once a role exists, object-level permissions are attached with a sentence you can almost say out loud: grant a permission on an object to a role.
GRANT permission ON object TO role;
Section 3 · Object examples
Grants are not only for tables. In a real app, the role may need read access to a table, execute on a function, and maybe access to a sequence if inserts generate IDs automatically.
GRANT SELECT ON public.products TO app_backend;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.orders TO app_backend;
GRANT SELECT ON public.order_summary TO app_backend;
GRANT EXECUTE ON FUNCTION public.calculate_total(uuid) TO app_backend;
Read-only access for browsing catalog data.
Read and write transactional rows.
Read a safe projection without widening table access.
Allow a specific routine instead of broader object rights.
Section 3 · Sequence foot-gun
A common surprise is granting INSERT on the table but forgetting the sequence behind the auto-generated ID. The table permission is not always enough by itself.
Section 3 · Schema access
A role can have table-level grants and still fail if it does not have USAGE on the schema that contains the table. In Supabase this is easy to miss because the default public schema already has a permissive baseline.
GRANT USAGE ON SCHEMA public TO app_backend;
The narration emphasizes custom schemas here because the public defaults can hide the pattern until your design grows beyond public.
Section 3 · Future objects
GRANT statements affect objects that exist now. If a migration creates a new table next week, the role does not magically inherit the same privileges unless you define default privileges for the role that creates the future objects.
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
GRANT SELECT ON TABLES TO app_reader;
postgres, so FOR ROLE postgres is usually the part that makes the statement actually useful.Section 4 · Membership
Membership lets you express hierarchy. One role can inherit the privileges of another so you do not repeat the full permission set everywhere.
GRANT parent_role TO child_role;
CREATE ROLE child_role_name IN ROLE parent_role_name;
Own the common read access once and keep that baseline in a single place.
These roles inherit the shared baseline instead of repeating the same grant list manually.
Section 4 · PostgreSQL 16+
PostgreSQL 16 and later let you control inheritance at the membership level. That gives you a choice between automatic privilege flow and explicit activation with SET ROLE.
GRANT parent_role TO child_role WITH INHERIT TRUE;
GRANT parent_role TO child_role WITH INHERIT FALSE, SET TRUE;
Child sessions immediately use the parent's privileges. This is the default and the easiest mental model.
Membership exists, but the child must activate the parent's power deliberately inside the session.
Section 4 · Why teams use this
If multiple services share the same database, inheritance keeps the base permission model centralized. Update the parent role once and every child role receives the new behavior.
That is the operational win the narration is pointing at: one place to update, multiple downstream roles benefit.
Section 5 · Supabase roles
Supabase starts a project with a ready-made role system. You do not create these from scratch each time; the platform provisions them because the API, auth, storage, and admin services rely on them.
Used when the request is unauthenticated.
PostgREST connects as this role and switches to others.
Used when the JWT proves a signed-in user.
Bypasses RLS. Never ship this to the client.
Used by the Supabase dashboard tooling.
Top-level management role used internally by the platform.
Section 5 · Internal service roles
Some Supabase roles exist mainly so platform services can own schemas or run internal jobs. You may not grant against them often, but it helps to recognize their purpose when inspecting permissions.
Managed by Supabase Auth.
Managed by Supabase Storage.
Used internally for data movement and related jobs.
Section 5 · Day-to-day RLS targets
When you are writing row-level security for an application, these are usually the two roles you care about most. anon represents unauthenticated requests, and authenticated represents signed-in users.
Policies here answer what someone can do before they log in.
Policies here answer what a logged-in user can see or change.
anon and authenticated, the two roles your app uses day to day.Section 6 · Example app need
The example app only needs to read from products and create or update rows in orders. That is a perfect least-privilege case: define a role that can do exactly that and nothing more.
Read products, create and update orders, stay away from admin powers.
Too broad for routine application traffic and too dangerous if leaked.
Section 6 · Approach 1
If the backend or tool connects straight to Postgres, the role needs LOGIN because the connection string authenticates directly as that role.
CREATE ROLE app_service LOGIN PASSWORD 'your-strong-password';
GRANT USAGE ON SCHEMA public TO app_service;
GRANT SELECT ON public.products TO app_service;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.orders TO app_service;
app_service.Section 6 · Least privilege payoff
This is the real value of creating a narrower role. The application still works, but the credential cannot act like an admin account if it leaks or gets misused.
Section 5 · API request path
With the Data API, clients do not connect as your custom direct-login role. PostgREST connects as authenticator, then switches into the role named by the JWT, typically anon or authenticated.
anon and authenticated, because those are the roles the app uses day to day.Section 6 · Approach 2
If requests go through the Supabase client libraries or REST endpoints, the custom role does not need LOGIN. PostgREST will switch into that role instead.
CREATE ROLE custom_user NOLOGIN;
GRANT USAGE ON SCHEMA public TO custom_user;
GRANT SELECT ON public.products TO custom_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.orders TO custom_user;
GRANT custom_user TO authenticator;
The last GRANT is the permission for authenticator to switch into custom_user. Without it, the role exists but cannot be assumed by the API layer.
Section 6 · JWT role claim
Creating the role is not enough. The incoming JWT has to include role: "custom_user" or PostgREST will keep using the default role like anon or authenticated.
Section 6 · Direction matters
If you want the custom role to inherit whatever anon already has, the grant direction is important. The child goes on the right.
GRANT anon TO custom_user;
custom_user gets anon's permissions. Not the other way around.Section 7 · Mental model
Object grants answer "can this role touch this table at all?" Row-level security answers "which subset of rows should be visible or writable once table access exists?"
GRANT opens the object-level door.
RLS decides which records pass through that door.
Section 7 · Enable the policy layer
Once RLS is enabled, row access is denied by default until a policy allows it. That is why viewers should think of RLS as a second authorization layer after grants.
ALTER TABLE public.orders ENABLE ROW LEVEL SECURITY;
Section 7 · Supabase Auth policy
With Supabase Auth, the common pattern is to compare a row's owner column to the current user's UUID from the JWT. That is what auth.uid() gives you.
CREATE POLICY "users can read their own orders"
ON public.orders
FOR SELECT
TO authenticated
USING (user_id = auth.uid());
Section 7 · Direct Postgres policy
If the caller connects directly to Postgres instead of through Supabase Auth, you can still do row-based filtering. The backend sets a request-specific value, and the policy reads it with current_setting(..., true).
CREATE POLICY "users can read their own orders"
ON public.orders
FOR SELECT
USING (user_id = current_setting('app.current_user_id', true)::uuid);
Section 7 · Transaction-local state
The backend can populate the setting just before the query. Using the transaction-local form avoids user identity leaking into later requests when pooled connections are reused.
SELECT set_config(
'app.current_user_id',
'550e8400-e29b-41d4-a716-446655440000',
true
);
current_setting(..., true) returns NULL instead of throwing if the setting is missing, and set_config(..., true) keeps the value local to the current transaction.Section 8 · Separate audience
Reporting tools often need summaries, not raw operational tables. A separate schema is a clean way to expose the transformed data while keeping the original tables and sensitive columns elsewhere.
The trust boundary becomes the ETL or transformation job that decides what crosses from operational data into reporting data.
Section 8 · Reporting schema SQL
Once you decide the reporting area exists, create its schema and the tables or views that the analytics audience should actually use.
CREATE SCHEMA reporting;
CREATE TABLE reporting.monthly_summary (
month date,
revenue numeric,
order_count int
);
reporting or exports, rather than by one individual tool or person.Section 8 · BI login role
If Power BI or another BI tool connects directly to Postgres, give it its own login role and point it only at the reporting schema. That keeps raw application tables out of scope.
CREATE ROLE powerbi_reader LOGIN PASSWORD 'strong-password';
GRANT USAGE ON SCHEMA reporting TO powerbi_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO powerbi_reader;
As with the earlier default-privileges slide, future reporting tables may need default grants too if you do not want refresh jobs to break later.
Section 8 · Direct DB vs Data API
Supabase can expose extra schemas through the Data API, but that is optional. BI tools commonly work better over a direct connection or the session pooler, especially for long-lived reporting sessions.
reporting to exposed schemas if needed.Outro · Final model
This is the full story from the video in one picture: use postgres for setup and migrations, use narrow roles for production tools and services, use RLS when access depends on the current user, and carve out schemas when one audience should only see one slice of data.
Use for migrations and break-glass admin work.
Give apps and tools only the privileges they need.
Filter rows based on auth context or request state.
Isolate reporting or exports from raw app tables.
How clients connect to Supabase Postgres.
What changes on legacy networks and hosted paths.
Who can do what inside the database itself.