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:

RoleDefault grant on new tables in publicDefault grant on new sequences
anonSELECTnone
authenticatedALLALL

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:

  • SELECT on new tables in public by 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 allows

User 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:

  • ALL on new tables and sequences in public by 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 policy

Volcano 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_migrations schema, 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 unknown until 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 | f

How 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 only

Security 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_id to auth.uid() on insert
  • Grant minimal permissions to anon role

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 public without 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

On this page