Volcano/ Docs

Accessing User Information in Functions

Learn how to access authenticated user information in your serverless functions.

Learn how to access authenticated user information in your serverless functions.

Overview

When users invoke your functions, Volcano automatically injects their authentication context into the event payload. This lets you:

  • Identify which user is making the request
  • Differentiate between guest (anonymous) and registered users
  • Apply user-specific logic and permissions
  • Track user data across sessions

The __volcano_auth Object

Functions invoked with an auth user token receive a special __volcano_auth object:

exports.handler = async (event) => {
  console.log(event.__volcano_auth);
  // {
  //   user_id: "550e8400-e29b-41d4-a716-446655440000",
  //   email: "user@example.com",
  //   project_id: "123e4567-e89b-12d3-a456-426614174000",
  //   role: "authenticated"
  // }
};

Context Fields

FieldTypeDescription
user_idstring (UUID)Unique identifier for the user. Persists across anonymous → authenticated conversion
emailstringUser's email (or internal format for anonymous users)
project_idstring (UUID)The project this user belongs to
rolestringUser type: "authenticated" or "anonymous"

Checking if User is Authenticated

Basic Authentication Check

exports.handler = async (event) => {
  if (!event.__volcano_auth) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'Authentication required' })
    };
  }

  // User is authenticated (either anonymous or registered)
  const { user_id, role } = event.__volcano_auth;
  console.log(`User ${user_id} with role ${role}`);
};

Public Endpoints (Optional Auth)

exports.handler = async (event) => {
  if (event.__volcano_auth) {
    // Authenticated request - personalize response
    const { user_id } = event.__volcano_auth;
    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'Welcome back!',
        user_id: user_id
      })
    };
  }

  // Unauthenticated - show public content
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Welcome, guest!'
    })
  };
};

User Types and Roles

Authenticated Users

Users who signed up with email/password or OAuth:

{
  user_id: "550e8400-e29b-41d4-a716-446655440000",
  email: "user@example.com",
  project_id: "123e4567-e89b-12d3-a456-426614174000",
  role: "authenticated"
}

Anonymous Users

Guest users created via signUpAnonymous():

{
  user_id: "660f9511-f39c-52e5-b827-557766551111",
  email: "anon-1234567890@anonymous.volcano.internal",
  project_id: "123e4567-e89b-12d3-a456-426614174000",
  role: "anonymous"
}

Note: Anonymous users have a real user_id for tracking their data. When they convert to authenticated users, this ID stays the same - preserving all their data.

Differentiating User Types

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

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

  if (auth.role === 'anonymous') {
    // Guest user
    return {
      statusCode: 200,
      body: JSON.stringify({
        data: { /* limited features */ },
        message: 'Create an account to unlock all features!',
        is_guest: true
      })
    };
  }

  if (auth.role === 'authenticated') {
    // Full registered user
    return {
      statusCode: 200,
      body: JSON.stringify({
        data: { /* full features */ },
        is_guest: false
      })
    };
  }
};

Common Patterns

User-Specific Data Queries

const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

exports.handler = async (event) => {
  if (!event.__volcano_auth) {
    return { statusCode: 401, body: 'Unauthorized' };
  }

  const { user_id } = event.__volcano_auth;
  const client = await pool.connect();

  try {
    // Query data for this specific user
    const result = await client.query(
      'SELECT * FROM todos WHERE user_id = $1',
      [user_id]
    );

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

Row-Level Security (RLS)

Set PostgreSQL session variables to enable automatic RLS filtering:

const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

exports.handler = async (event) => {
  if (!event.__volcano_auth) {
    return { statusCode: 401, body: 'Unauthorized' };
  }

  const { user_id, email, role } = event.__volcano_auth;
  const client = await pool.connect();

  try {
    // Set session variables for RLS
    await client.query('SET request.jwt.claim.sub = $1', [user_id]);
    await client.query('SET request.jwt.claim.email = $1', [email]);
    await client.query('SET request.jwt.claim.role = $1', [role]);

    // Query automatically filtered by RLS policies
    const result = await client.query('SELECT * FROM posts');

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

Feature Gating by User Type

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

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

  const { action } = event;

  // Premium features only for registered users
  const premiumFeatures = ['export', 'analytics', 'team_sharing'];
  
  if (premiumFeatures.includes(action) && auth.role === 'anonymous') {
    return {
      statusCode: 403,
      body: JSON.stringify({
        error: 'This feature requires a full account',
        upgrade_url: '/signup'
      })
    };
  }

  // Process the action
  // ...
};

Audit Logging

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

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

  const { user_id, email, role } = event.__volcano_auth;
  const { action, resource_id } = event;

  // Log the action with user context
  await logAction({
    user_id,
    email,
    role,
    action,
    resource_id,
    timestamp: new Date(),
    ip_address: event.headers?.['x-forwarded-for']
  });

  // Continue with operation
  // ...
};

When __volcano_auth is NOT Present

The context is undefined when invoked with:

  • service key (service-to-service calls)
  • No Authorization header
  • Invalid token
exports.handler = async (event) => {
  if (!event.__volcano_auth) {
    // Could be:
    // 1. Service-to-service call (service key)
    // 2. Unauthenticated request
    // 3. Invalid/expired token
    
    // Check if this is a valid use case
    // or return 401 Unauthorized
  }
};

Security Best Practices

Always Validate User Context

// DON'T: Assume context exists
const userId = event.__volcano_auth.user_id; // May crash!

// DO: Always check first
if (!event.__volcano_auth) {
  return { statusCode: 401, body: 'Unauthorized' };
}
const userId = event.__volcano_auth.user_id;

Don't Trust Client Input for User ID

// DON'T: Use user_id from request body
const userId = event.user_id; // Could be forged!

// DO: Use authenticated context
const userId = event.__volcano_auth.user_id; // Verified by Volcano

Validate Project Isolation

The project_id in context ensures users can only access their project's resources:

const { project_id, user_id } = event.__volcano_auth;

// Volcano guarantees:
// - user_id belongs to project_id
// - Token is valid for this project
// - User is active and not banned

Complete Example

const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

exports.handler = async (event) => {
  // 1. Validate authentication
  if (!event.__volcano_auth) {
    return {
      statusCode: 401,
      body: JSON.stringify({ error: 'Authentication required' })
    };
  }

  const { user_id, email, role } = event.__volcano_auth;
  console.log(`Request from user ${user_id} (${role})`);

  // 2. Connect to database
  const client = await pool.connect();

  try {
    // 3. Set RLS context
    await client.query('SET request.jwt.claim.sub = $1', [user_id]);
    await client.query('SET request.jwt.claim.role = $1', [role]);

    // 4. Process request based on action
    const { action, data } = event;

    switch (action) {
      case 'create_item':
        // Create item for this user
        const result = await client.query(
          'INSERT INTO items (user_id, name, data) VALUES ($1, $2, $3) RETURNING *',
          [user_id, data.name, data]
        );
        
        return {
          statusCode: 201,
          body: JSON.stringify(result.rows[0])
        };

      case 'list_items':
        // RLS automatically filters to user's items
        const items = await client.query('SELECT * FROM items ORDER BY created_at DESC');
        
        return {
          statusCode: 200,
          body: JSON.stringify({
            items: items.rows,
            user_type: role
          })
        };

      case 'upgrade_prompt':
        // Show upgrade message for anonymous users
        if (role === 'anonymous') {
          return {
            statusCode: 200,
            body: JSON.stringify({
              message: 'Create an account to save your items permanently',
              is_guest: true
            })
          };
        }
        
        return {
          statusCode: 200,
          body: JSON.stringify({
            message: 'You have full access!',
            is_guest: false
          })
        };

      default:
        return {
          statusCode: 400,
          body: JSON.stringify({ error: 'Unknown action' })
        };
    }
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' })
    };
  } finally {
    client.release();
  }
};

See Also

On this page