Database Authentication Flow - Complete Guide

How authentication works in client-side (browser) vs server-side (function) scenarios.

How authentication works in client-side (browser) vs server-side (function) scenarios.


Client-Side (Browser) - Automatic Session Persistence

How It Works

When users sign in, their session is automatically persisted across page reloads using localStorage!

The Flow

// ═══════════════════════════════════════
// PAGE 1: Login page
// ═══════════════════════════════════════

import { VolcanoAuth } from '@volcano.dev/sdk';

const volcano = new VolcanoAuth({
  apiUrl: 'https://api.yourapp.com',
  anonKey: 'your-anon-key',
});

// User signs in
await volcano.auth.signIn({
  email: 'user@example.com',
  password: 'password123',
});

// SDK automatically saves to localStorage:
// - volcano_access_token
// - volcano_refresh_token

// Navigate to another page
window.location.href = '/dashboard';
// ═══════════════════════════════════════
// PAGE 2: Dashboard (different page/reload)
// ═══════════════════════════════════════

import { VolcanoAuth } from '@volcano.dev/sdk';

// Initialize SDK again (new page)
const volcano = new VolcanoAuth({
  apiUrl: 'https://api.yourapp.com',
  anonKey: 'your-anon-key',
});

// Restore session from localStorage
await volcano.initialize();

// Check if user is logged in
const { user } = await volcano.auth.getUser();

if (user) {
  console.log('Still logged in as:', user.email);

  // Query database - works immediately!
  volcano.database('notes_db');
  const { data } = await volcano.from('posts').select('*');
  console.log('Your posts:', data);
} else {
  console.log('Not logged in - redirect to login');
  window.location.href = '/login';
}

Session Lifecycle

Page 1 (Login)          localStorage              Page 2 (Dashboard)
──────────────          ────────────              ──────────────────

signIn()          →     Save tokens         →     initialize()
                        - access_token             Restore tokens
                        - refresh_token            from localStorage
                                                        ↓
                                                   Query database
                                                   (tokens loaded!)

Automatic Token Management

// Initial page load
const volcano = new VolcanoAuth({ ... });

// SDK automatically:
// 1. Checks localStorage for saved tokens
// 2. Loads access_token and refresh_token
// 3. Ready to query!

// You can query immediately if session exists
await volcano.initialize();  // Restores session

volcano.database('notes_db');
const { data } = await volcano.from('posts').select('*');
// Works! Tokens loaded from localStorage

Session Expiration & Refresh

// When access token expires, SDK auto-refreshes
const { data, error } = await volcano.from('posts').select('*');

// If error is "expired token":
// 1. SDK automatically calls refreshSession()
// 2. Gets new access_token using refresh_token
// 3. Saves new token to localStorage
// 4. Retries the query
// 5. Returns data

// You don't need to handle this - it's automatic!

Server-Side (Function) - Event Context

How It Works

In functions, the auth context comes in event.__volcano_auth and you pass it to the database!

const { Client } = require('pg');

exports.handler = async (event) => {
  const auth = event.__volcano_auth;

  if (!auth) {
    return { statusCode: 401, body: 'Unauthorized' };
  }

  console.log('Querying as user:', auth.user_id);

  // Replace application_name in the connection string. DATABASE_URL already
  // carries application_name=volcano_full_access, so set (don't append) it to
  // reliably switch this connection to user access.
  const url = new URL(process.env.DATABASE_URL);
  url.searchParams.set('application_name', `volcano_user_access:${auth.user_id}`);

  const client = new Client({ connectionString: url.toString() });
  await client.connect();

  // Query - auth.uid() = auth.user_id from event
  const { rows } = await client.query('SELECT * FROM posts');

  await client.end();

  return { statusCode: 200, body: JSON.stringify({ posts: rows }) };
};

How it works:

// 1. User calls function with their access token
fetch('/functions/func-id/invoke', {
  headers: { Authorization: 'Bearer {user_access_token}' },
});

// 2. Volcano validates token and injects into event
event.__volcano_auth = {
  user_id: 'abc-123...', // ← From user's JWT!
  email: 'user@example.com', // ← From user's JWT!
  role: 'authenticated', // ← From user's JWT!
  project_id: 'project-uuid',
  access_token: 'eyJhbGci...', // The caller's access token
};

// 3. Build application_name with auth from event (simplified format)
const appName = `volcano_user_access:${auth.user_id}`;

// 4. pgproxy receives connection and sets:
// SET LOCAL request.jwt_sub = 'user_id'
// SET LOCAL request.jwt_email = 'email'
// SET LOCAL request.jwt_role = 'role'

// 5. Query executes
// auth.uid() = user_id from event.__volcano_auth

With Connection Pooling (Production)


Complete Examples

Client-Side App (React)

import { useEffect, useState } from 'react';
import { VolcanoAuth } from '@volcano.dev/sdk';

// Initialize SDK once
const volcano = new VolcanoAuth({
  apiUrl: 'https://api.yourapp.com',
  anonKey: 'your-anon-key',
});

volcano.database('your_notes_db');

function App() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // On app load, restore session from localStorage
    async function initAuth() {
      const { user } = await volcano.initialize();
      setUser(user);

      if (user) {
        console.log('Session restored for:', user.email);
        // Load user's data
        loadPosts();
      }
    }

    initAuth();

    // Listen for auth changes
    volcano.auth.onAuthStateChange((user) => {
      setUser(user);
      if (user) {
        loadPosts();
      } else {
        setPosts([]);
      }
    });
  }, []);

  async function handleLogin(email, password) {
    await volcano.auth.signIn({ email, password });
    // Tokens automatically saved to localStorage
    // onAuthStateChange callback fires
    // Posts automatically loaded
  }

  async function loadPosts() {
    // Query database - tokens automatically used
    const { data } = await volcano.from('posts').select('*').order('created_at', { ascending: false });

    setPosts(data);
  }

  async function createPost(title, content) {
    // Insert - access token automatically sent
    const { data, error } = await volcano.insert('posts', { title, content });

    if (!error) {
      // Reload posts
      loadPosts();
    }
  }

  async function handleSignOut() {
    const { error } = await volcano.auth.signOut();
    if (error) console.error('Sign-out request failed:', error);
  }

  if (!user) {
    return <LoginForm onLogin={handleLogin} />;
  }

  return (
    <div>
      <h1>Welcome {user.email}</h1>
      <button onClick={handleSignOut}>Sign Out</button>
      {/* Tokens automatically cleared from localStorage on signOut */}

      <PostsList posts={posts} />
      <CreatePostForm onSubmit={createPost} />
    </div>
  );
}

Key Points:

  • Tokens saved to localStorage on sign in
  • Tokens restored from localStorage on page reload
  • volcano.initialize() restores session automatically
  • All queries use restored tokens
  • Works across page navigations

Server-Side Function

Size pools across all function instances within the shared connection limits; this example uses two connections per instance.

const { VolcanoAuth } = require('@volcano.dev/sdk');

const volcano = new VolcanoAuth({
  projectId: process.env.VOLCANO_PROJECT_ID,
  anonKey: process.env.VOLCANO_ANON_KEY,
  apiUrl: process.env.VOLCANO_API_URL,
});

// Connection pool outside handler (reused)
const pool = volcano.database.createPool({ max: 2 });

exports.handler = async (event) => {
  console.log('Event auth context:', event.__volcano_auth);

  // Auth context comes from the user's access token
  // When user invokes function:
  //   fetch('/invoke', { headers: { 'Authorization': 'Bearer {user_token}' } })
  // Volcano validates token and injects:
  //   event.__volcano_auth = { user_id, email, role, project_id }

  const auth = event.__volcano_auth;

  if (!auth) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'Unauthorized - no auth context' }),
    };
  }

  console.log('Querying as user:', auth.user_id, '(' + auth.email + ')');

  const client = await pool.connect();

  try {
    // Pass event.__volcano_auth to database
    await volcano.database.setAuthContext(client, auth);

    // Now query - auth.uid() = auth.user_id
    const { rows } = await client.query(`
      SELECT id, title, content, created_at 
      FROM posts 
      ORDER BY created_at DESC 
      LIMIT 10
    `);

    return {
      statusCode: 200,
      body: JSON.stringify({
        posts: rows,
        count: rows.length,
        queried_as: {
          user_id: auth.user_id,
          email: auth.email,
          role: auth.role,
        },
      }),
    };
  } finally {
    client.release();
  }
};

How user identity flows to database:

Browser                    Volcano                    Function
────────                   ───────                    ────────

User signs in         →    Validates credentials →    N/A
Gets access token          Creates JWT with user_id

User calls function   →    Validates token        →   event.__volcano_auth = {
with token                 Extracts user_id              user_id, email, role
                                                      }

                                                   →   Build application_name with
                                                       event.__volcano_auth

                                                   →   application_name=
                                                       volcano_user_access:user_id

                                                   →   pgproxy parses auth context

                                                   →   SET LOCAL request.jwt_sub
                                                       = user_id

                                                   →   auth.uid() returns user_id

Comparison: Client vs Server

Client-Side (Browser)

Session Storage: localStorage (automatic)

Code:

// Page 1: Sign in
await volcano.auth.signIn({ email, password });
// Tokens saved to localStorage automatically

// Page 2: Query (after reload/navigation)
await volcano.initialize(); // Restores from localStorage
const { data } = await volcano.from('posts').select('*');
// Tokens automatically used

How auth is passed:

POST /databases/DB_ID/query/select
Authorization: Bearer {access_token_from_localStorage}
                      ↑ Automatically added by SDK

Server-Side (Function)

Session Storage: event.__volcano_auth (per-request)

Code:

// The function receives auth in the event
const { Client } = require('pg');

const auth = event.__volcano_auth;

// Replace application_name (DATABASE_URL already has volcano_full_access)
const url = new URL(process.env.DATABASE_URL);
url.searchParams.set('application_name', `volcano_user_access:${auth.user_id}`);

const client = new Client({ connectionString: url.toString() });
await client.connect();
const { rows } = await client.query('SELECT * FROM posts');
await client.end();

How auth is passed:

event.__volcano_auth { user_id, project_id }
         ↓
Build: application_name=volcano_user_access:user_id
         ↓
pgproxy looks up user and sets: request.jwt_sub = user_id
         ↓
auth.uid() returns user_id

Common Patterns

Client-Side: Protect Routes

import { VolcanoAuth } from '@volcano.dev/sdk';

const volcano = new VolcanoAuth({ ... });

// On app initialization
async function initApp() {
  // Try to restore session
  const { user, error } = await volcano.initialize();

  if (user) {
    // User is logged in
    console.log('Logged in as:', user.email);
    showDashboard();
  } else {
    // Not logged in
    showLoginPage();
  }
}

// Call on every page load
initApp();

Client-Side: Auto-Refresh Tokens

// SDK handles this automatically!
const { data, error } = await volcano.from('posts').select('*');

// If access_token expired:
// 1. SDK detects 401 error
// 2. Calls refreshSession() automatically
// 3. Uses refresh_token to get new access_token
// 4. Saves new token to localStorage
// 5. Retries the query
// 6. Returns data

// You just see the result - no error handling needed!

Server-Side: Simple Pattern

const { Client } = require('pg');

exports.handler = async (event) => {
  const auth = event.__volcano_auth;

  if (!auth) {
    return { statusCode: 401, body: 'Unauthorized' };
  }

  // auth contains the user's identity (from their JWT):
  console.log('User ID:', auth.user_id);
  console.log('Email:', auth.email);
  console.log('Role:', auth.role);

  // Replace application_name (DATABASE_URL already has volcano_full_access)
  const url = new URL(process.env.DATABASE_URL);
  url.searchParams.set('application_name', `volcano_user_access:${auth.user_id}`);

  const client = new Client({ connectionString: url.toString() });
  await client.connect();

  const { rows } = await client.query('SELECT * FROM posts WHERE user_id = auth.uid()');

  await client.end();

  return { statusCode: 200, body: JSON.stringify({ posts: rows }) };
};

Server-Side: Production Pattern (Connection Pool)

pgproxy fixes the auth identity from application_name at connection startup, so a pool serves exactly one user. Keep one pool per auth user:

Save the bounded per-user pool helper as user-db-pool.js alongside your function. It retains at most four user pools with one connection each, reclaims idle pools for new users, removes empty pools after failed startup, and preserves active work. Handle its capacity error with backpressure and size all instances within the shared connection limits.

const { connectForUser } = require('./user-db-pool');

exports.handler = async (event) => {
  const auth = event.__volcano_auth;

  if (!auth) {
    return { statusCode: 401, body: 'Unauthorized' };
  }

  // Every connection in this pool started as volcano_user_access:{userId}
  let client;
  try {
    client = await connectForUser(auth.user_id);
  } catch (error) {
    // Log diagnostic codes, never connection strings or error messages.
    console.error('Database connection failed', { name: error?.name, code: error?.code ?? 'UNKNOWN' });
    return {
      statusCode: 503,
      headers: { 'Retry-After': '1' },
      body: JSON.stringify({ error: 'Database temporarily unavailable' }),
    };
  }

  try {
    // Query - auth.uid() = auth.user_id from event
    const { rows } = await client.query('SELECT * FROM posts');

    return { statusCode: 200, body: JSON.stringify({ posts: rows }) };
  } finally {
    client.release();
  }
};

How event.__volcano_auth is Created

User Invokes Function

The function's visibility must be authenticated or public for a signed-in user to invoke it.

// Browser code
import { VolcanoAuth } from '@volcano.dev/sdk';

const volcano = new VolcanoAuth({ ... });

// User signs in
await volcano.auth.signIn({ email: 'user@example.com', password: 'pass' });

// Invoke function with user's access token
const result = await volcano.functions.invoke('get-posts', {
  action: 'get_posts'
});

// Behind the scenes:
// fetch('/functions/FUNC_ID/invoke', {
//   headers: { 'Authorization': 'Bearer {user_access_token}' },
//   body: JSON.stringify({ action: 'get_posts' })
// });

Volcano Creates event.__volcano_auth

// Volcano backend (internal/handlers/functions.go)

// 1. Validate token
claims := validateAccessToken(token)

// 2. Inject into event payload
if claims.Type == "auth_user" {
    payload["__volcano_auth"] = map[string]interface{}{
        "user_id":      claims.UserID,   // ← From JWT!
        "email":        claims.Email,    // ← From JWT!
        "role":         claims.Role,     // ← From JWT!
        "project_id":   projectID,
        "access_token": tokenString,     // The caller's access token
    }
}

// 3. Invoke the function with the augmented payload
invoke(functionName, payload)

The Function Receives It

exports.handler = async (event) => {
  console.log('Received event:', event);

  // Event contains:
  // {
  //   action: 'get_posts',
  //   __volcano_auth: {
  //     user_id: 'abc-123...',
  //     email: 'user@example.com',
  //     role: 'authenticated',
  //     project_id: 'project-uuid',
  //     access_token: 'eyJhbGci...'
  //   }
  // }

  const auth = event.__volcano_auth;
  // This is the user's identity from their JWT token
};

Summary

Client-Side (Browser)

Q: How is auth persisted across pages?
A: localStorage (automatic)

Q: How do I use it?
A: Just call volcano.initialize() on page load

// Tokens automatically saved on sign in
await volcano.auth.signIn({ email, password });

// Tokens automatically restored on page load
await volcano.initialize();

// Queries automatically use restored tokens
const { data } = await volcano.from('posts').select('*');

Server-Side (Function)

Q: How do I get user's identity?
A: From event.__volcano_auth

Q: How do I pass it to database?
A: Build application_name parameter with auth context

// Extract auth from event
const auth = event.__volcano_auth;

// Replace application_name in the connection string (DATABASE_URL already
// carries volcano_full_access — set, don't append)
const url = new URL(process.env.DATABASE_URL);
url.searchParams.set('application_name', `volcano_user_access:${auth.user_id}`);

// Connect
const client = new Client({ connectionString: url.toString() });

Where event.__volcano_auth Comes From

A: Volcano creates it from the user's access token!

User calls function with their token
  ↓
Volcano validates token
  ↓
Volcano extracts user_id, email, role from token
  ↓
Volcano creates event.__volcano_auth
  ↓
The function receives it
  ↓
SDK passes to database

Connection Format Reference

Volcano uses the application_name parameter to control access levels:

Full Admin Access

volcano_full_access
  • For DDL operations: CREATE TABLE, ALTER TABLE, migrations
  • Bypasses Row Level Security
  • Use for admin/maintenance scripts only

User Impersonation (RLS Enforced)

volcano_user_access:{user_id}
  • Queries run as the specified user
  • Row Level Security is enforced
  • auth.uid() returns the user_id
  • Use for authenticated user operations

Anonymous Access (RLS Enforced)

volcano_user_access
  • Queries run with anon role
  • Row Level Security is enforced
  • auth.uid() returns NULL
  • Use for public, unauthenticated queries

See Also

On this page