Database Authentication Flow - Complete Guide
How authentication works in client-side (browser) vs server-side (Lambda) scenarios.
How authentication works in client-side (browser) vs server-side (Lambda) 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 localStorageSession 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 (Lambda) - Event Context
How It Works
In Lambda functions, the auth context comes in event.__volcano_auth and you pass it to the database!
Using Standard pg Library (Recommended)
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'
};
// 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_authWith 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();
}
}
if (!user) {
return <LoginForm onLogin={handleLogin} />;
}
return (
<div>
<h1>Welcome {user.email}</h1>
<button onClick={() => volcano.auth.signOut()}>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 Lambda Function
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: 20 });
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 Lambda
──────── ─────── ──────
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_idComparison: 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 usedHow auth is passed:
POST /databases/DB_ID/query/select
Authorization: Bearer {access_token_from_localStorage}
↑ Automatically added by SDKServer-Side (Lambda)
Session Storage: event.__volcano_auth (per-request)
Code:
// Lambda receives auth in 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_idCommon 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:
const { Pool } = require('pg');
// One pool per auth user, reused across invocations while the container is
// warm. In production, bound this cache (e.g. LRU with idle eviction).
const poolsByUser = new Map();
function poolForUser(userId) {
let pool = poolsByUser.get(userId);
if (!pool) {
// 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:${userId}`);
pool = new Pool({ connectionString: url.toString(), max: 5 });
poolsByUser.set(userId, pool);
}
return 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}
const client = await poolForUser(auth.user_id).connect();
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
// 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,
}
}
// 3. Invoke Lambda with augmented payload
lambda.Invoke(functionName, payload)Lambda 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'
// }
// }
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 (Lambda)
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
↓
Lambda receives it
↓
SDK passes to databaseConnection 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
anonrole - Row Level Security is enforced
auth.uid()returns NULL- Use for public, unauthenticated queries
See Also
- Direct Connection - PostgreSQL connection with auth impersonation
- Authentication & Impersonation - Complete auth guide
- Query Builder API - Query builder reference
- SDK Documentation - Complete SDK guide
- Row-Level Security - Using auth.uid() in policies