Fair Supply LogoFair Supply - Docs

Auth0 Infrastructure

Auth0 handles authentication and organisation-based SSO for the FSA Platform.

Key Files

TypeLocation
Auth0 Clientpackages/core/src/infrastructure/auth0/client.ts
Auth Utilitiesapps/web/src/lib/auth.ts
Repositoriespackages/core/src/infrastructure/repositories/auth0-*-repository.ts

Authentication Flow

Authentication is handled by Next.js middleware (proxy.ts). Unauthenticated users are redirected automatically.

JWT Claims

interface Auth0User {
  sub: string;           // Auth0 user ID (auth0|abc123)
  email: string;
  email_verified: boolean;
  name?: string;
  org_id?: string;       // Auth0 organisation ID
}

Organisation-Based SSO

Each Account links to an Auth0 Organisation for SSO:

Linking Account to Auth0 Org

const useCase = new CreateAndLinkAuth0Organisation();
await useCase.execute({ accountId, orgName, auth0UserId });

Management API

// packages/core/src/infrastructure/auth0/client.ts
import { ManagementClient } from 'auth0';

const management = new ManagementClient({
  domain: process.env.AUTH0_DOMAIN!,
  clientId: process.env.AUTH0_MGMT_CLIENT_ID!,
  clientSecret: process.env.AUTH0_MGMT_CLIENT_SECRET!,
});

Common Operations

// Create organisation
await management.organizations.create({ name: 'slug', display_name: 'Name' });

// Invite member
await management.organizations.createInvitation(
  { id: orgId },
  { inviter: { name }, invitee: { email }, client_id, send_invitation_email: true }
);

// Get members
await management.organizations.getMembers({ id: orgId });

// Check user exists
const users = await management.users.getByEmail(email);

Environment Variables

AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_CLIENT_ID=xxx
AUTH0_CLIENT_SECRET=xxx
AUTH0_MGMT_CLIENT_ID=xxx
AUTH0_MGMT_CLIENT_SECRET=xxx

Server Action Authorization

Server actions must verify the caller has a platform account before executing account-scoped operations.

Auth Guards

GuardLocationUse
requireAuth()lib/auth.tsVerifies Auth0 session, returns userId
requireAccount()lib/auth.tsVerifies session + selected account (Redis), returns { userId, accountId }
requireAdmin()lib/auth.tsVerifies FSAdmin role, redirects if not
getCurrentUserId()lib/auth.tsOptional auth — returns userId or null without redirecting

Which guard to use

ContextGuard
Account-scoped server actions, pages, and layoutsrequireAccount()
Admin server actions (under /admin)requireAuth() (proxy enforces FSAdmin role)
Respondent-only actions (/engage)requireAuth() (no account needed)
Optional auth (e.g., admin pages)getCurrentUserId()

Example

'use server';
import { requireAccount } from '@/lib/auth';

export const myAction = createServerAction()
  .input(z.object({ name: z.string() }))
  .handler(async ({ input }) => {
    const { userId, accountId } = await requireAccount();
    // userId and accountId are guaranteed to exist
  });

Organisation Security

Auth0 organisations are created with assign_membership_on_login: false and is_signup_enabled: false (defaults). This prevents users from self-joining organisations by crafting direct Auth0 /authorize URLs. Membership is managed exclusively through the invitation flow.

Signup is enabled at the tenant database connection level to allow invitees to create accounts. Users who sign up without an invitation have no org membership and are blocked by the requireAccount() guard.

Best Practices

  1. Use requireAccount() in server actions - fast Redis check, blocks users without an account
  2. Rely on middleware for admin routes - proxy.ts checks FSAdmin role for /admin paths
  3. Sync with local data - keep User entity updated with Auth0
  4. Never trust client-provided user IDs - use userId from requireAccount()
  5. Secure Management API - keep credentials safe
TypeLocation
Clientpackages/core/src/infrastructure/auth0/client.ts
Proxyapps/web/src/proxy.ts
Auth Utilitiesapps/web/src/lib/auth.ts
Repositoriespackages/core/src/infrastructure/repositories/auth0-*-repository.ts

On this page