Authentication
How someone signs in
Authentication is handled by GoTrue, the Supabase auth service. Two methods are available: Google sign-in, and email with a password.
On success the browser receives a JSON Web Token signed with the project's JWT secret. That token carries the user's identifier and their PostgreSQL role, and is sent with every subsequent request. The session is persisted in browser storage, which is why the authenticated layout route disables server-side rendering: the server cannot see it.
From token to database identity
This is the part worth understanding, because it explains why the security model works.
When a request arrives at PostgREST carrying a token, PostgREST verifies the signature, then connects to PostgreSQL as a limited role and switches into the role named in the token: anon for an unauthenticated caller, authenticated for a signed-in one. It also makes the token's claims available to SQL, which is what auth.uid() reads inside a policy.
So a policy written as USING (user_id = auth.uid()) is comparing a column against the verified identity from the token. The application cannot lie about who it is, because it never asserts an identity directly; it presents a signed token, and the database derives the identity from it.
Neither anon nor authenticated can log in to PostgreSQL directly. They are reachable only through this switching mechanism.
Email verification is disabled
Sign-ups are auto-confirmed. Nobody has to click a link in an email, because the server cannot reliably send email: the hosting provider blocks outbound mail ports by default.
The consequence is that email addresses are not proven. Someone can register with an address they do not control.
Two things limit the damage. New profiles are forced to pending and are invisible until an administrator approves them, so impersonation is caught by moderation before anyone sees it. And the two founder addresses that receive owner access automatically are already registered and confirmed, so they cannot be claimed by registering them again.
This is a known compromise, recorded so that nobody mistakes it for an accident. If outbound email ever works, turning verification back on is a configuration change.
Roles in the application
Roles are stored in user_roles, not on profiles. Four levels exist:
| Role | Rank | What it can do |
|---|---|---|
user | 0 | Ordinary member. No row in user_roles |
editor | 10 | Publish and edit announcements, upload post images |
admin | 20 | Everything an editor can, plus approve and manage profiles and subscribers |
owner | 30 | Everything, plus change other people's roles and read the audit log |
Checks are hierarchical. has_role_at_least(user, 'admin') is true for an admin and for an owner. This matters: an earlier exact-match check meant an owner who held only the owner role failed every administrator check, which is the bug that prompted the hierarchy.
In React, useUserRole reads the current role and roleAtLeast compares it. As always, these calls shape the interface and enforce nothing.
Changing a role
user_roles cannot be written through the API at all. The only path is the set_user_role function, which:
- takes an advisory lock before reading anything, so two concurrent changes cannot each observe a state that the other is about to invalidate;
- refuses a caller who is not an owner;
- refuses a change that would leave the system with zero owners, checked after applying so that the failure rolls the change back;
- writes an audit row.
There is one deliberate gap. The function reads auth.uid(), which is null in Supabase Studio and for the service role, so it always refuses there. If every owner is ever locked out, recovery is to edit user_roles directly as a database superuser. That is the intended break-glass path.
The founder trigger
Two email addresses are hardcoded in a trigger that grants owner access when an account with one of those addresses is confirmed.
This was a bootstrapping mechanism from before a role management interface existed. It is now redundant, and it carries a small risk: if one of those accounts were ever deleted, the address would become claimable, and registering it would grant owner access immediately. Replacing it is worth doing.