Database Roles and Security
When you create a database in Volcano, it comes with built-in roles for secure multi-tenant access.
When you create a database in Volcano, it comes with built-in roles for secure multi-tenant access.
Default grants
Volcano grants every table you create in the public schema to two roles:
| Role | Default grant on new tables in public | Default grant on new sequences |
|---|---|---|
anon | SELECT | none |
authenticated | ALL | ALL |
These grants decide what each role may touch. Row-level security decides which rows, and only once you enable it on the table. Until you do, any user session, including an anonymous user's, can read, change, and delete every row of the table, and connections that run as anon can read every row.
Make this the first step for every table you create in public:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;Then add policies for the rows each role may use. With RLS enabled and no policies, anon and authenticated see no rows. To keep a table away from both roles entirely, revoke the grants or create it in a schema of your own:
REVOKE ALL ON internal_notes FROM anon, authenticated;Available Roles
No User Session (anon)
Used when: No user is signed in
Permissions:
SELECTon new tables inpublicby default- Sees only the rows your policies allow, on tables with RLS enabled
- Reads every row of a table without RLS, on connections that run as
anon
Example:
// Unauthenticated request - automatically uses 'anon' role
// On a table with RLS enabled, sees only rows a policy allowsUser Sessions (authenticated)
Used when: A registered or anonymous user is signed in. This is the PostgreSQL role; auth.role() returns authenticated for registered users and anonymous for anonymous users.
Permissions:
ALLon new tables and sequences inpublicby default- Reads and writes only the rows your policies allow, on tables with RLS enabled
- Reads, changes, and deletes every row of a table without RLS
- Cannot modify database schema
Example:
// Authenticated request - automatically uses 'authenticated' role
// Can perform CRUD operations on own data
const result = await client.query('SELECT * FROM posts');
// Returns only current user's posts, once posts has RLS and a policyVolcano Platform (volcano_platform)
Used when: Volcano reads your database's size and query statistics, when realtime looks up a table's primary key, and when a migration run takes its lock and keeps its history
Permissions:
- Cannot log in
- Reads query statistics for every role (
pg_monitor) - Reads and writes the migration history in the
_volcano_migrationsschema, once a migration run creates it - No access to your tables beyond what you grant to
PUBLIC
Statements run as volcano_platform never appear in database queries, including your own after SET ROLE volcano_platform. Don't drop, alter, or grant the role:
- If you drop it, Volcano recreates it the next time it needs it. Statements recorded before the drop appear under
unknownuntil statistics reset. - Volcano checks the role when it creates or repairs it. If the role can log in or other roles are members of it at that point, Volcano refuses to use it: database queries return an error, and Volcano runs its size reads, primary-key lookups, and migration bookkeeping as the database owner, where they appear in database queries.
SELECT rolname, rolcanlogin
FROM pg_roles
WHERE rolname = 'volcano_platform'; rolname | rolcanlogin
------------------+-------------
volcano_platform | fHow It Works
Automatic Role Selection
Volcano automatically selects the correct role based on authentication:
// In your function
exports.handler = async (event) => {
// If event.__volcano_auth exists → uses 'authenticated' role
// If no auth → uses 'anon' role
const client = await pool.connect();
const result = await client.query('SELECT * FROM posts');
// Automatically filtered by RLS!
};You don't need to:
- Manually set roles
- Add WHERE clauses for user_id
- Manage permissions yourself
Volcano handles:
- Role selection
- User context injection
- RLS enforcement
Multi-Tenant Isolation
Each user sees only their own data:
-- User 1 connects
SELECT * FROM posts;
-- Returns: User 1's posts only
-- User 2 connects
SELECT * FROM posts;
-- Returns: User 2's posts onlySecurity guarantees:
- Users cannot see each other's data
- Users cannot modify each other's data
- Enforced at database level (not application code)
Setting Up Tables
1. Enable RLS on Table
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
title TEXT,
content TEXT
);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;2. Create Policies
For any user session, including an anonymous user:
CREATE POLICY "authenticated_users_own_data" ON posts
FOR ALL
TO authenticated
USING (user_id = auth.uid());For public read:
CREATE POLICY "public_read" ON posts
FOR SELECT
TO anon
USING (is_public = true);3. Grant Permissions
Tables you create in public already carry the default grants. Grant explicitly for tables in other schemas, or after you revoke. A table in another schema also needs USAGE on its schema:
-- Only for a schema other than public
GRANT USAGE ON SCHEMA app TO anon, authenticated;
-- Grant to authenticated users
GRANT ALL ON posts TO authenticated;
-- Grant to requests without a user session
GRANT SELECT ON posts TO anon;Best Practices
DO:
- Use RLS policies for all user data
- Use
auth.uid()in policies - Set
user_idtoauth.uid()on insert - Grant minimal permissions to
anonrole
DON'T:
- Don't bypass RLS in application code
- Don't manually filter by user_id (let RLS do it)
- Don't grant write permissions to
anon - Don't leave a table in
publicwithout ENABLE ROW LEVEL SECURITY
Examples
User Posts
CREATE TABLE user_posts (
id SERIAL PRIMARY KEY,
user_id UUID DEFAULT auth.uid(),
title TEXT NOT NULL,
content TEXT
);
ALTER TABLE user_posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_posts_policy ON user_posts
FOR ALL
TO authenticated
USING (user_id = auth.uid());
GRANT ALL ON user_posts TO authenticated;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO authenticated;Public Content with Private Edits
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
author_id UUID NOT NULL,
title TEXT,
content TEXT,
published BOOLEAN DEFAULT false
);
ALTER TABLE articles ENABLE ROW LEVEL SECURITY;
-- Everyone can read published articles
CREATE POLICY articles_public_read ON articles
FOR SELECT
TO anon, authenticated
USING (published = true);
-- Authors can manage their own articles
CREATE POLICY articles_author_manage ON articles
FOR ALL
TO authenticated
USING (author_id = auth.uid());
GRANT SELECT ON articles TO anon;
GRANT ALL ON articles TO authenticated;See Also
- Row-Level Security - Complete RLS guide
- Auth Helpers - Helper functions reference
- Creating Databases - Database setup