Skip to main content

Overview

DelightBridge uses NextAuth.js with Google OAuth for authentication. All API routes are protected by session-based authentication, meaning users must be logged in via the web interface before making API requests.
DelightBridge does not currently support API keys or bearer tokens. All requests must include a valid session cookie.

Authentication Flow

1

User logs in with Google

Users authenticate via Google OAuth through the /login page. NextAuth handles the OAuth flow and creates a session.
2

Session cookie is set

After successful login, NextAuth sets a session cookie named authjs.session-token (or __Secure-authjs.session-token in production with HTTPS).
3

Cookie is sent with requests

The browser automatically includes this cookie with all subsequent API requests to the same domain.
4

Session is verified

Each API route calls requireSession() or requirePermission() to verify the session and check permissions before processing the request.

Session Validation

The authentication logic is implemented in /src/lib/session.ts:
session.ts
import { auth } from '@/../auth';
import { NextResponse } from 'next/server';

export async function requireSession() {
  const session = await auth();

  if (!session?.user) {
    return {
      session: null,
      unauthorized: NextResponse.json(
        { error: 'Unauthorized' }, 
        { status: 401 }
      ),
    };
  }

  return { session, unauthorized: null };
}

Usage in API Routes

Every protected endpoint uses this pattern:
route.ts
import { requireSession } from '@/lib/session';

export async function GET(req: NextRequest) {
  const { unauthorized } = await requireSession();
  if (unauthorized) return unauthorized;

  // Authenticated logic here
  const data = await fetchData();
  return NextResponse.json(data);
}

Permission Levels

DelightBridge implements a hierarchical permission system with four levels:

view

Can view threads and services (read-only)

edit

Can modify drafts and service configurations

send

Can send email responses to customers

admin

Can manage members and all workspace settings

Permission Hierarchy

Permissions follow a hierarchy where higher levels inherit lower level permissions:
admin > send > edit > view
  • admin can do everything (view + edit + send + manage members)
  • send can view, edit, and send emails
  • edit can view and modify drafts/services
  • view can only read data

Permission Checking

The requirePermission() function enforces permission requirements:
session.ts
export async function requirePermission(allowed: PermissionLevel[]) {
  const { session, unauthorized } = await requireSession();
  if (unauthorized || !session) {
    return { session: null, unauthorized, forbidden: null };
  }

  const permission = getSessionPermission(session);
  if (!permission || !allowed.includes(permission)) {
    return {
      session,
      unauthorized: null,
      forbidden: NextResponse.json(
        { error: 'Forbidden' }, 
        { status: 403 }
      ),
    };
  }

  return { session, unauthorized: null, forbidden: null };
}

Permission-Restricted Endpoints

Some endpoints require specific permissions:
Send Email (requires or 'admin')
export async function POST(_req: NextRequest, { params }) {
  const { unauthorized, forbidden } = await requirePermission(['send', 'admin']);
  if (unauthorized) return unauthorized;
  if (forbidden) return forbidden;

  // Send email logic
}
Manage Members (requires only)
export async function GET() {
  const { unauthorized, forbidden } = await requireAdminSession();
  if (unauthorized) return unauthorized;
  if (forbidden) return forbidden;

  // List workspace members
}

Making Authenticated Requests

From the Browser

When making requests from the DelightBridge frontend, the session cookie is automatically included:
Frontend Example
const response = await fetch('/api/threads?serviceId=service-123', {
  credentials: 'include', // Ensures cookies are sent
});

if (!response.ok) {
  if (response.status === 401) {
    // User is not logged in - redirect to login
    window.location.href = '/login';
  } else if (response.status === 403) {
    // User lacks permissions
    console.error('Insufficient permissions');
  }
}

const threads = await response.json();

From External Clients

To make API requests from external tools (e.g., curl, Postman), you need to:
  1. Log in via the web interface to obtain a session cookie
  2. Extract the session cookie from your browser’s developer tools
  3. Include the cookie in your requests
Session tokens are sensitive credentials. Never commit them to version control or share them publicly.
cURL Example
curl -X GET 'https://your-domain.com/api/services' \
  -H 'Cookie: authjs.session-token=YOUR_SESSION_TOKEN_HERE' \
  -H 'Content-Type: application/json'
cURL with Multiple Cookies
curl -X POST 'https://your-domain.com/api/threads/thread-123/send' \
  -H 'Cookie: authjs.session-token=TOKEN; authjs.callback-url=https://your-domain.com' \
  -H 'Content-Type: application/json'

Admin Emails

Users can be designated as admins via environment variables. These users automatically receive admin permissions regardless of database settings:
.env.local
Admin emails defined in environment variables cannot be downgraded or removed through the API. This provides a safeguard for permanent admin access.
The logic is implemented in /src/lib/admin-emails.ts and enforced in both authentication and permission checks:
auth.ts
const ADMIN_EMAILS = getAdminEmails();

const isAllowed = ADMIN_EMAILS.includes(email) || !!member;

Session Object Structure

The session object available in API routes contains:
Session Structure
{
  user: {
    id: string,              // User ID from database
    email: string,           // Google account email
    name: string,            // User's display name
    picture: string | null,  // Google profile picture URL
    permission: PermissionLevel  // 'admin' | 'send' | 'edit' | 'view'
  }
}

Error Responses

401 Unauthorized

Returned when no valid session exists:
{
  "error": "Unauthorized"
}
Cause: User is not logged in or session has expired. Solution: Redirect to /login to authenticate.

403 Forbidden

Returned when the user lacks required permissions:
{
  "error": "Forbidden"
}
Cause: User’s permission level is insufficient for the requested operation. Example: A user with view permission trying to send an email.

Workspace Members

Workspace access is controlled through the workspace_members table. Only users whose email addresses are in this table (or in ADMIN_EMAILS) can log in:
Sign-In Check
const [member] = await db
  .select({ permission: workspaceMembers.permission })
  .from(workspaceMembers)
  .where(eq(workspaceMembers.email, email));

const isAllowed = ADMIN_EMAILS.includes(email) || !!member;

if (!isAllowed) return '/login?error=unauthorized';
Unauthorized users are redirected to the login page with an error.

Security Best Practices

Always deploy DelightBridge with HTTPS enabled. NextAuth automatically uses secure cookies (__Secure-* prefix) when served over HTTPS.
Set a strong AUTH_SECRET in your environment variables:
AUTH_SECRET="your-random-32-char-secret-here"
Generate a new secret with:
openssl rand -base64 32
NextAuth sessions expire after 30 days by default. Users must re-authenticate after this period.
Track permission changes in your workspace members. Consider logging who modifies permissions and when.

Next Steps

Services API

Learn how to manage email service accounts

Threads API

Fetch and manage email threads

Members API

Manage workspace members (admin only)

Permissions Guide

Detailed guide on permission management

Build docs developers (and LLMs) love