Oliver White
5 May 2026 · 7 min read
Row Level Security is PostgreSQL's mechanism for attaching access control policies directly to tables. When RLS is enabled on a table, every query — SELECT, INSERT, UPDATE, DELETE — is filtered through the defined policies before any data is returned or modified. Not in the application. Not in an ORM. In the database itself.
Application-layer access control relies on every code path correctly checking permissions before touching data. This is fine in theory and fragile in practice. A new route, a forgotten if-statement, a misconfigured middleware, a library vulnerability — any of these can bypass application checks. RLS has no such bypass. Even if your application code is broken, the database will not return data it shouldn't.
Application-layer only
RLS + Application layer
| Table | Policy | Rule |
|---|---|---|
| contests | Public read | status IN ('active', 'archived') |
| contests | Admin insert/update/delete | auth.jwt()->>'role' = 'admin' |
| artworks | Public read | true — all artworks visible |
| artworks | Admin insert | auth.jwt()->>'role' = 'admin' |
| artworks | Admin update/delete | auth.jwt()->>'role' = 'admin' |
| votes | Anyone can insert | true — anonymous voting allowed |
| votes | No updates | false — votes are immutable |
| votes | No deletes | false — votes are permanent |
| votes | User reads own votes | auth.uid() = user_id OR role = 'admin' |
| users | Public read (is_public=true) | is_public = true |
| users | User reads own profile | auth.uid() = id |
| users | User updates own profile | auth.uid() = id |
The submit_vote function is marked SECURITY DEFINER, which means it runs with the permissions of the function owner (postgres) rather than the calling user. This means it bypasses RLS. This is intentional — the function needs to read contests, artworks, and votes tables atomically, then insert and update. Doing this through RLS-filtered queries would require each table to have policies permitting the anon role to read them in the exact way the function needs.
SECURITY DEFINER functions are the right tool when you need to perform privileged operations on behalf of a less-privileged caller — provided the function itself implements the access control logic. The submit_vote function checks duplicate votes and contest status internally, so it doesn't need RLS to do that job.
Built with this methodology
RLS policies enforce access rules at the database layer. They are not a replacement for application-layer auth — they are the guarantee that application-layer bugs do not become data breaches.
From the build log