Build the mental model
Supabase's shape: a standard Postgres database at the core, with Supabase generating APIs directly from your schema so you don't hand-write a backend layer for basic CRUD.
- Authentication as an integrated service
- File storage with policy-based access
- Realtime subscriptions to database changes
- Serverless functions for custom logic
Architecture: Frontend/Backend -> Supabase, branching into the Postgres database (with row-level security), Auth, and Storage.
Row-level security means the database itself enforces which rows a given request may see or modify, based on who's asking -- not just application code deciding what to display.
A genuinely critical distinction sits alongside this: client-accessible keys are restricted by row-level security, while server-only secret keys bypass it entirely.
- Row-Level Security
- A database-level security feature where access rules are enforced per row based on who is making the request, so the database itself -- not just application code -- controls which rows a given query can see or change.
SUPABASE ARCHITECTURE
---------------------
+----------------------------+
| Supabase |
Frontend / Backend ->| Postgres DB (Row-Level |
| Security) | Auth | Storage |
+----------------------------+Connect it to a real scenario
A client-accessible key is safe in frontend code specifically because row-level security policies constrain what it can see or change -- as long as policies are configured correctly.
A service-role or secret key bypasses those policies entirely and is intended only for trusted server-side code that has already done its own authorization checks.
Start closed
Treat every new table's default access as closed to any client-accessible key.
Write a specific policy
Add a row-level security policy describing exactly who can select, insert, update, or delete which rows.
Test the boundary
Deliberately try to access data you shouldn't be able to, confirming the policy actually blocks it.
Because dashboard specifics evolve, the durable practice is starting closed and opening deliberately, rather than starting open and hoping nothing risky gets exposed.
Client-accessible keys are not server secrets
A key meant for browser use (restricted by security policies like row-level security) and a server-only secret key (which bypasses those restrictions) are not interchangeable. Using a server-only secret in browser-exposed code is a real, serious security risk -- it can hand any visitor full access to your database.
Try the working example
function checkKeyUsage(usage) {
const { keyType, context } = usage;
const risky = keyType === "service-role" && context === "browser";
return risky ? "RISKY: service-role key exposed to browser" : "safe";
}
const scenarios = [
{ label: "anon key in frontend JS", keyType: "public-anon", context: "browser" },
{ label: "service-role key in frontend JS", keyType: "service-role", context: "browser" },
];
for (const s of scenarios) {
console.log(`${s.label} -> ${checkKeyUsage(s)}`);
}anon key in frontend JS -> safe
service-role key in frontend JS -> RISKY: service-role key exposed to browser5-minute try-it
For a table storing private user notes, describe in plain language what a row-level security policy would need to say so users can only see their own notes.
One important caution
Leaving a table's row-level security unconfigured or disabled while still exposing a client-accessible key, allowing any visitor to read or modify all rows.
Embedding a service-role or secret key in frontend code, bypassing all row-level security protections entirely.
PostgreSQL Documentation: Row Security Policies — Cloud Providers & Platforms