Skip to main content
This Quickstart is currently in Beta. We’d love to hear your feedback!

Use AI to integrate Auth0

If you use an AI coding assistant like Claude Code, Cursor, or GitHub Copilot, you can add Auth0 authentication automatically in minutes using agent skills.Install:
npx skills add auth0/agent-skills
Then ask your AI assistant:
Add Auth0 authentication to my Cap'n Web app
Your AI assistant will automatically create your Auth0 application, fetch credentials, install required packages, create secure WebSocket RPC authentication, and set up your configuration. Full agent skills documentation →

Get Started

This quickstart demonstrates how to add Auth0 authentication to a Cap’n Web application. You’ll build a modern RPC-based web application with secure login functionality using Cap’n Web’s JavaScript framework and the Auth0 SPA SDK.
1

Create a new project

Create a new Cap’n Web project and set up the basic structure:
mkdir capnweb-auth0-app && cd capnweb-auth0-app
Initialize the project and configure it for ES modules:
npm init -y && npm pkg set type="module"
Create the project folder structure:
mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
2

Install dependencies

Install Cap’n Web and core dependencies:
npm install capnweb ws dotenv
Install the Auth0 SDKs for authentication and token verification:
npm install @auth0/auth0-spa-js @auth0/auth0-api-js
@auth0/auth0-spa-js is used on the client side to handle user authentication, login flows, and token management in the browser.@auth0/auth0-api-js is used on the server side to verify access tokens and validate JWT signatures using Auth0’s JWKS.
Set up the start script in package.json:
npm pkg set scripts.start="node server/index.js"
3

Setup your Auth0 App

Next up, you need to create a new app on your Auth0 tenant and add the environment variables to your project.You can choose to do this automatically by running a CLI command or do it manually via the Dashboard:
Run the following shell command on your project’s root directory to create an Auth0 app and generate a .env file:
5

Create the server

Create the Cap’n Web server with Auth0 integration:
server/index.js
import { RpcTarget, newWebSocketRpcSession } from 'capnweb';
import { WebSocketServer } from 'ws';
import { ApiClient } from '@auth0/auth0-api-js';
import http from 'http';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';

dotenv.config();

const __dirname = dirname(fileURLToPath(import.meta.url));
const userProfiles = new Map();

// Auth0 configuration
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;
const PORT = process.env.PORT || 3000;

if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
  console.error('❌ Missing required Auth0 environment variables');
  if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN is required');
  if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID is required');
  if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE is required');
  process.exit(1);
}

// Initialize Auth0 API client for token verification
const auth0ApiClient = new ApiClient({
  domain: AUTH0_DOMAIN,
  audience: AUTH0_AUDIENCE
});

async function verifyToken(token) {
  try {
    const payload = await auth0ApiClient.verifyAccessToken({
      accessToken: token
    });
    return payload;
  } catch (error) {
    throw new Error(`Token verification failed: ${error.message}`);
  }
}

// Profile Service - Cap'n Web RPC Target
class ProfileService extends RpcTarget {
  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    const userId = decoded.sub;
    const profile = userProfiles.get(userId) || { bio: '' };
    
    return {
      id: userId,
      email: decoded.email || 'Unknown User',
      bio: profile.bio
    };
  }

  async updateProfile(accessToken, bio) {
    const decoded = await verifyToken(accessToken);
    const userId = decoded.sub;
    userProfiles.set(userId, { bio });
    
    return { success: true, message: 'Profile updated successfully' };
  }
}

// Create HTTP server
const server = http.createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  if (req.url === '/api/config') {
    const config = { 
      auth0: { 
        domain: AUTH0_DOMAIN, 
        clientId: AUTH0_CLIENT_ID,
        audience: AUTH0_AUDIENCE
      } 
    };
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200);
    res.end(JSON.stringify(config));
    return;
  }

  // Handle root path and Auth0 callback
  if (req.url === '/' || req.url === '/index.html' || req.url.startsWith('/?code=') || req.url.startsWith('/?error=')) {
    const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
    res.setHeader('Content-Type', 'text/html');
    res.writeHead(200);
    res.end(html);
    return;
  }

  if (req.url === '/app.js') {
    const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  // Serve Auth0 SPA JS SDK from node_modules
  if (req.url === '/@auth0/auth0-spa-js') {
    const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js');
    const js = readFileSync(modulePath, 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  // Serve capnweb from node_modules
  if (req.url === '/capnweb') {
    const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js');
    const js = readFileSync(modulePath, 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  res.writeHead(404);
  res.end('Not found');
});

// WebSocket server for Cap'n Web RPC
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
  // Only handle RPC connections on /api path
  if (req.url === '/api') {
    console.log('🔗 New Cap\'n Web RPC connection');
    
    // Create a new ProfileService instance for this connection
    const profileService = new ProfileService();
    
    // Use capnweb's newWebSocketRpcSession to handle the connection
    newWebSocketRpcSession(ws, profileService);
  }
});

// Start server
server.listen(PORT, () => {
  console.log(`🚀 Cap'n Web Auth0 Server Started`);
  console.log(`📍 Server running on http://localhost:${PORT}`);
  console.log(`🔐 Auth0 Domain: ${AUTH0_DOMAIN}`);
  console.log(`🆔 Client ID: ${AUTH0_CLIENT_ID.substring(0, 8)}...`);
  console.log(`🎯 API Audience: ${AUTH0_AUDIENCE}`);
});
6

Create the client interface

Create the frontend HTML and JavaScript files:
7

Run your app

npm run start
CheckpointYou should now have a fully functional Auth0 login page running on your localhost

Advanced Usage

Enhance security by adding additional validation and rate limiting to your RPC methods:
server/profile-service.js
import rateLimit from 'express-rate-limit';

class ProfileService extends RpcTarget {
  constructor() {
    super();
    this.rateLimiter = new Map(); // Simple in-memory rate limiting
  }

  async validateAndRateLimit(userId) {
    const now = Date.now();
    const userLimit = this.rateLimiter.get(userId) || { count: 0, resetTime: now + 60000 };
    
    if (now > userLimit.resetTime) {
      userLimit.count = 0;
      userLimit.resetTime = now + 60000;
    }
    
    if (userLimit.count >= 10) {
      throw new Error('Rate limit exceeded. Try again later.');
    }
    
    userLimit.count++;
    this.rateLimiter.set(userId, userLimit);
  }

  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    await this.validateAndRateLimit(decoded.sub);
    
    const userId = decoded.sub;
    const profile = userProfiles.get(userId) || { bio: '' };
    
    return {
      id: userId,
      email: decoded.email || 'Unknown User',
      bio: profile.bio,
      lastUpdated: profile.lastUpdated || null
    };
  }
}
Implement proper WebSocket connection handling with automatic reconnection:
client/connection-manager.js
import { newWebSocketRpcSession } from 'capnweb';

class RpcConnectionManager {
  constructor(wsUrl, options = {}) {
    this.wsUrl = wsUrl;
    this.api = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.isConnecting = false;
    this.onReconnect = options.onReconnect || (() => {});
  }

  async connect() {
    if (this.isConnecting) return this.api;
    this.isConnecting = true;

    try {
      // Dispose existing connection if any
      if (this.api) {
        this.api[Symbol.dispose]();
      }

      // Create new WebSocket RPC session
      this.api = newWebSocketRpcSession(this.wsUrl);
      this.reconnectAttempts = 0;
      this.isConnecting = false;
      
      console.log('✅ RPC connection established');
      this.onReconnect(this.api);
      
      return this.api;
    } catch (error) {
      this.isConnecting = false;
      
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        console.log(`🔄 Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
        
        await new Promise(resolve => 
          setTimeout(resolve, this.reconnectDelay * this.reconnectAttempts)
        );
        
        return this.connect();
      } else {
        throw new Error('Max reconnection attempts reached');
      }
    }
  }

  disconnect() {
    if (this.api) {
      this.api[Symbol.dispose]();
      this.api = null;
    }
    this.reconnectAttempts = 0;
  }

  getApi() {
    return this.api;
  }
}

// Usage in app.js
const connectionManager = new RpcConnectionManager(
  `${protocol}//${host}/api`,
  {
    maxReconnectAttempts: 5,
    reconnectDelay: 1000,
    onReconnect: async (api) => {
      // Refresh UI or reload data after reconnection
      await displayProfile(api);
    }
  }
);

// Connect when authenticated
if (isAuthenticated) {
  await connectionManager.connect();
  profileApi = connectionManager.getApi();
}
Replace in-memory storage with a database for production use:
server/database.js
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL || 'postgresql://localhost/capnweb_auth0'
});

// Initialize database schema
async function initializeDatabase() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS user_profiles (
      user_id VARCHAR(255) PRIMARY KEY,
      bio TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
  `);
}

class DatabaseProfileService extends RpcTarget {
  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    const result = await pool.query(
      'SELECT bio, updated_at FROM user_profiles WHERE user_id = $1',
      [decoded.sub]
    );
    
    return {
      id: decoded.sub,
      email: decoded.email,
      bio: result.rows[0]?.bio || '',
      lastUpdated: result.rows[0]?.updated_at || null
    };
  }
  
  async updateProfile(accessToken, bio) {
    const decoded = await verifyToken(accessToken);
    await pool.query(`
      INSERT INTO user_profiles (user_id, bio, updated_at) 
      VALUES ($1, $2, CURRENT_TIMESTAMP)
      ON CONFLICT (user_id) 
      DO UPDATE SET bio = $2, updated_at = CURRENT_TIMESTAMP
    `, [decoded.sub, bio]);
    
    return { success: true, message: 'Profile updated successfully' };
  }
}

export { initializeDatabase, DatabaseProfileService };