Functional Specification: SuperTokens Authentication with Multi-Tenancy Integration
1. Introduction & Goal
Problem Statement:
- Current system uses Firebase Auth, creating vendor lock-in
- Need to transition to self-hosted solution for full control
- Multi-tenant architecture requires tenant-aware authentication
- Demo user management needed for v0.1 with multiple tenant instances
- Infrastructure transitioning from Firebase+GCP to Hetzner+Supabase+Cloudflare
Goal: Implement a self-hosted SuperTokens authentication system with complete multi-tenancy support, enabling:
- User registration and login across multiple tenant organizations
- Tenant-specific access control and data isolation
- Demo user setup for v0.1 showcase
- Seamless token management for web, CLI, and real-time clients
2. Current State
- Authentication: Firebase Auth (temporary solution)
- Multi-tenancy: Partially implemented (tenant_id in core tables, but auth layer is global)
- Infrastructure: Transitioning from GCP → Hetzner, Firebase → SuperTokens
- Database: PostgreSQL (multi-tenant ready)
- Frontend: React (Firebase SDK integrated)
- CLI: Basic auth with temporary tokens
3. Proposed Solution
Deploy a self-hosted SuperTokens Core service that integrates with our multi-tenant architecture:
Architecture Overview:
┌─────────────────────────────────────────────────────────┐
│ SuperTokens Core (Docker Container) │
│ (Handles: signup, login, sessions, token rotation) │
└──────────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ PostgreSQL (Tenant-Scoped User Association) │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ SuperTokens Tables │ │ Tenant-Users Table │ │
│ │ (auto-generated) │ │ (custom mapping) │ │
│ └──────────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Key Principles:
- ✅ SuperTokens handles identity verification (authentication)
- ✅ Custom
tenant_userstable tracks tenant associations - ✅ JWT claims include tenant context for isolation
- ✅ GraphQL middleware enforces tenant-scoped access
- ✅ RBAC roles managed per-tenant
4. Technical Design
4.1 Multi-Tenancy Architecture
User-Tenant Relationship:
SuperTokens provides global user management. We layer tenant association via custom metadata and a junction table:
-- Tracks SuperTokens user → Tenant mapping
CREATE TABLE tenant_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
supertokens_user_id VARCHAR(128) NOT NULL,
user_email VARCHAR(254) NOT NULL,
roles TEXT[] DEFAULT '{}', -- admin, s&op_orchestrator, analyst, viewer
organizational_unit_id UUID,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_user_per_tenant UNIQUE(tenant_id, supertokens_user_id),
CONSTRAINT valid_roles CHECK (roles <@ ARRAY['admin', 's&op_orchestrator', 'analyst', 'viewer'])
);
CREATE INDEX idx_tenant_users_supertokens_id ON tenant_users(supertokens_user_id);
CREATE INDEX idx_tenant_users_email ON tenant_users(user_email);
Multi-Tenant User Setup:
When a user signs up:
- SuperTokens creates global user record
- Custom hook creates
tenant_usersentry with tenant_id - JWT claim includes tenant_id for all subsequent requests
- All GraphQL queries filtered by tenant_id
4.2 Token Lifecycle
Web Clients (React):
- Access Token: JWT, 15 minutes, stored in memory
- Refresh Token: Opaque, 7 days, stored in HttpOnly cookie (secure!)
- Flow: SuperTokens SDK handles automatic refresh in background
- User Impact: No visible token management needed
CLI Client:
- Token Storage:
~/.chainalign/tokens.json(mode 0600 for security) - Management: CLI requests new access token when expired
- Flow:
chainalign login→ enter credentials → tokens stored locally
# CLI Token File Structure
{
"accessToken": "eyJhbGc...",
"refreshToken": "opaque-token-here",
"tenantId": "tenant-uuid",
"expiresAt": 1730644800000
}
WebSocket Connections (Real-Time):
- Bearer Token: Access token passed in handshake
- Verification: Checked on connect and renewed on refresh
- Isolation: Tenant-scoped socket.io rooms
4.3 Authorization & Access Control
Role-Based Access Control (RBAC):
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name VARCHAR(50) NOT NULL,
description TEXT,
CONSTRAINT unique_role_per_tenant UNIQUE(tenant_id, name)
);
-- Predefined roles available in all tenants
INSERT INTO roles (tenant_id, name, description) VALUES
('*', 'admin', 'Full access to all features'),
('*', 's&op_orchestrator', 'Manage scenarios and constraints'),
('*', 'analyst', 'View and create reports'),
('*', 'viewer', 'Read-only access');
Roles stored per user:
- Users have multiple roles:
roles: ['admin', 's&op_orchestrator'] - Roles are tenant-specific
- Enforced at GraphQL resolver level via directives
GraphQL Authorization Example:
// Directive-based enforcement
const resolvers = {
Mutation: {
// Only admins and s&op_orchestrators can create scenarios
createScenario: requireRole(['admin', 's&op_orchestrator'])(
async (_, { input }, context) => {
// Automatic tenant_id filtering in context
return await ScenarioService.create(input, context.tenantId, context.userId);
}
),
// Only admins can delete
deleteScenario: requireRole(['admin'])(
async (_, { id }, context) => {
return await ScenarioService.delete(id, context.tenantId);
}
),
},
};
4.4 Tenant Context in Requests
Middleware Flow:
SuperTokens Middleware (verify session)
↓
Tenant Middleware (extract tenant from JWT)
↓
GraphQL Resolver (tenant_id enforced in queries)
Implementation:
// middleware/tenantMiddleware.js
export const tenantMiddleware = async (req, res, next) => {
const userId = req.session.getUserId();
const tenantId = req.session.getAccessTokenPayload().tenantId;
if (!tenantId) {
return res.status(403).json({ error: "User not associated with a tenant" });
}
req.tenantId = tenantId;
req.userId = userId;
next();
};
// All GraphQL queries automatically have tenant context
4.5 Real-Time Authentication (WebSocket)
Socket.io Auth Middleware:
// Verify token on connect
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token;
const session = await verifySession(token);
socket.userId = session.getUserId();
socket.tenantId = session.getAccessTokenPayload().tenantId;
next();
} catch (error) {
next(new Error('Invalid token'));
}
});
// Tenant-scoped rooms
io.on('connection', (socket) => {
socket.join(`tenant:${socket.tenantId}`);
// Broadcast only to users in same tenant
socket.on('scenario:update', (data) => {
io.to(`tenant:${socket.tenantId}`).emit('scenario:updated', data);
});
});
4.6 Demo User Setup for v0.1
Demo Tenant Structure:
TechElectronics Inc (tech-electronics-uuid)
├── admin@techemfg.com (admin, s&op_orchestrator)
├── analyst@techemfg.com (analyst)
└── viewer@techemfg.com (viewer)
PharmaCo (pharma-co-uuid)
├── admin@pharma.com (admin, s&op_orchestrator)
├── analyst@pharma.com (analyst)
└── viewer@pharma.com (viewer)
AgroScience (agro-science-uuid)
├── admin@agro.com (admin, s&op_orchestrator)
├── analyst@agro.com (analyst)
└── viewer@agro.com (viewer)
Seed Script:
#!/bin/bash
# Create users in SuperTokens and associate with tenants
DEMO_PASSWORD=${DEMO_PASSWORD:-"DemoPassword123!"}
# Create users via SuperTokens API
for email in admin@techemfg.com analyst@techemfg.com viewer@techemfg.com; do
curl -X POST "http://localhost:8007/recipe/user" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$email\", \"password\": \"$DEMO_PASSWORD\"}"
done
# Associate with tenants via backend API
curl -X POST "http://api.chainalign.local/auth/assign-tenant" \
-H "Content-Type: application/json" \
-d "{
\"email\": \"admin@techemfg.com\",
\"tenantId\": \"tech-electronics-uuid\",
\"roles\": [\"admin\", \"s&op_orchestrator\"]
}"
4.7 Firebase → SuperTokens Migration
Migration Flow:
- Export Firebase users (with custom claims containing tenantId and roles)
- Create SuperTokens users
- Create tenant_users entries (preserving tenant and role mappings)
- Verify users can login with new system
- Decommission Firebase
Migration Script:
// Pseudo-code for migration
async function migrateFirebaseUsers() {
const firebaseUsers = await exportFirebaseUsers();
for (const fUser of firebaseUsers) {
// Create in SuperTokens
const stUser = await EmailPassword.signUpByEmail(fUser.email, tempPassword);
// Map tenant association
await tenantUsersRepo.create({
supertokensUserId: stUser.user.id,
tenantId: fUser.customClaims.tenantId,
roles: fUser.customClaims.roles || ['user'],
});
}
}
5. Implementation Phases
Phase 1: Infrastructure Setup
Deliverable: SuperTokens Core running in Docker, connected to PostgreSQL
Tasks:
- Deploy SuperTokens Core Docker image
- Configure PostgreSQL connection
- Generate JWT signing keys (→ Infisical Vault)
- Test basic signup/login
Blocking: None (can start immediately)
Phase 2: Backend Integration
Deliverable: GraphQL API with SuperTokens authentication and tenant context
Tasks:
- Add supertokens-node SDK
- Create tenant_users table and migrations
- Implement signup hook for tenant association
- Add session verification middleware
- Add tenant context to GraphQL resolvers
- Implement RBAC role enforcement
Blocking: Phase 1
Phase 3: Frontend Integration
Deliverable: React app with login/signup pages and protected routes
Tasks:
- Add supertokens-auth-react SDK
- Create auth routes (/auth/login, /auth/signup, /auth/reset)
- Implement session authentication
- Add route guards for protected pages
- Test automatic token refresh
Blocking: Phase 1
Phase 4: CLI Token Management
Deliverable: CLI can login and maintain session without browser
Tasks:
- Implement CLI login command (email/password → tokens)
- Add token storage to ~/.chainalign/tokens.json
- Implement automatic token refresh
- Add logout command
Blocking: Phase 2 (needs backend auth endpoints)
Phase 5: WebSocket Authentication
Deliverable: Real-time features with per-tenant isolation
Tasks:
- Implement socket.io auth middleware
- Add token verification on connect
- Set up tenant-scoped socket rooms
- Test real-time updates with auth
Blocking: Phase 2 (needs token verification)
Phase 6: Demo User Setup
Deliverable: 3 demo tenants with 3 users each, ready for v0.1 showcase
Tasks:
- Create demo tenants in database
- Seed demo users via SuperTokens
- Assign roles and tenant associations
- Document demo credentials
- Test all demo user flows
Blocking: Phase 2, Phase 3
Phase 7: Firebase Migration
Deliverable: All existing users migrated, Firebase decommissioned
Tasks:
- Export Firebase users
- Migrate to SuperTokens users
- Create tenant_users mappings
- Verify all users can login
- Remove Firebase dependencies
- Update secrets (remove FIREBASE_* from vault)
Blocking: Phase 2, Phase 3, Phase 6
6. Database Schema Summary
-- SuperTokens auto-generates (internal):
-- auth_user, auth_session, auth_emailpassword_users, etc.
-- Custom tables for multi-tenancy:
tenant_users (user-tenant mapping)
roles (role definitions per tenant)
role_permissions (role capabilities)
organizational_units (tenant structure)
7. Secrets Management Integration (M58)
SuperTokens Credentials for Infisical Vault:
SUPERTOKENS_CORE_URL=http://supertokens-core:3567
SUPERTOKENS_API_KEY=<random-secure-key>
SUPERTOKENS_JWT_SIGNING_KEY=<random-secure-key>
# OAuth (optional, for social login)
GOOGLE_OAUTH_CLIENT_ID=<from-google-cloud>
GOOGLE_OAUTH_CLIENT_SECRET=<from-google-cloud>
# Token Configuration
JWT_EXPIRY_SECONDS=900 # 15 minutes
REFRESH_TOKEN_EXPIRY=604800 # 7 days
Fallback Secrets Classification:
- Required: SUPERTOKENS_CORE_URL, SUPERTOKENS_API_KEY, SUPERTOKENS_JWT_SIGNING_KEY
- Optional: GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET
8. Security Considerations
Email Verification
- Required before accessing application features
- One-time verification links sent to user email
- Prevents spam/fake accounts
Password Reset
- Secure token-based flow
- One-time use links with expiry
- Rate-limited to prevent abuse
Rate Limiting
- Auth endpoints protected: 5 attempts per 15 minutes per email
- WebSocket connections: Token validation required
- API endpoints: General rate limiting per tenant
Cookie Security
- HttpOnly: Prevents JavaScript access (XSS protection)
- SameSite=Strict: CSRF protection
- Secure: HTTPS only in production
Tenant Isolation
- Critical: All queries must filter by tenant_id
- JWT claim serves as "proof" of tenant membership
- BaseRepository enforces tenant filter in all queries
- GraphQL middleware validates tenant context
Protected Secrets
Never log these:
- SUPERTOKENS_JWT_SIGNING_KEY
- SUPERTOKENS_API_KEY
- User passwords
- Refresh tokens
8.5 User-Facing Error Handling
This section outlines the strategy for communicating authentication and authorization errors to the end-user. The goal is to provide a clear, secure, and helpful user experience without exposing internal system details that could be exploited.
Guiding Principles:
- Be Generic: Avoid revealing specific reasons for failure (e.g., use "Invalid credentials" instead of "User not found" or "Incorrect password").
- Be Actionable: When possible, guide the user on the next steps (e.g., "Need help? Contact support.").
- Log Details: While user messages are generic, detailed contextual information must be logged internally for debugging.
- Use HTTP Status Codes: Adhere to standard HTTP status codes to allow clients to react programmatically.
Error Response Structure:
All error responses from the API should follow a consistent JSON structure:
{
"error": {
"code": "AUTH_INVALID_CREDENTIALS",
"message": "The email or password you entered is incorrect. Please try again."
}
}
Common Error Scenarios:
| Scenario | HTTP Status | Error Code | User-Facing Message | Internal Log Details |
|---|---|---|---|---|
| Login Failure | 401 Unauthorized | AUTH_INVALID_CREDENTIALS | The email or password you entered is incorrect. Please try again. | Login attempt failed for email: [user_email]. Reason: [Invalid Password/User Not Found] |
| Email Already Exists | 409 Conflict | AUTH_EMAIL_EXISTS | An account with this email address already exists. Please log in. | Signup attempt failed for existing email: [user_email] |
| Forbidden Access | 403 Forbidden | AUTH_INSUFFICIENT_PERMISSIONS | You do not have permission to perform this action. Please contact your administrator if you believe this is an error. | Access denied for user [userId] in tenant [tenantId]. Required roles: [required_roles], User roles: [user_roles]. Action: [action_name] |
| Session Expired | 401 Unauthorized | AUTH_SESSION_EXPIRED | Your session has expired. Please log in again to continue. | Session expired for user [userId]. Token expired at [expiry_time]. |
| Tenant Mismatch | 403 Forbidden | AUTH_TENANT_ACCESS_DENIED | Access to this resource is not allowed. | User [userId] attempted to access a resource outside of their tenant [tenantId]. |
| Rate Limit Exceeded | 429 Too Many Requests | AUTH_RATE_LIMIT_EXCEEDED | You have made too many attempts. Please wait a few minutes and try again. | Rate limit exceeded for IP [ip_address] on endpoint [endpoint]. |
| Email Not Verified | 403 Forbidden | AUTH_EMAIL_NOT_VERIFIED | Your email address has not been verified. Please check your inbox for a verification link. | Action blocked for unverified user [userId]. |
| Generic Server Error | 500 Internal Server Error | INTERNAL_SERVER_ERROR | An unexpected error occurred. Our team has been notified. Please try again later. | [Full stack trace and error details]. Request ID: [request_id] |
This structured approach ensures that users receive helpful feedback while the development team gets the detailed information needed to diagnose and resolve issues quickly.
9. Out of Scope
This FSD covers authentication (verifying identity). The following are explicitly out of scope:
- Authorization Details: While RBAC structure is defined, specific permission implementation is per-feature
- Infisical Vault Integration: Covered separately in M58 Secrets Management FSD
- Advanced Features: MFA/2FA, SSO, SAML (can be added in future versions)
- Infrastructure Deployment: Server/network setup is operations concern
10. Success Criteria
- ✅ Users can signup, login, logout with email/password
- ✅ SuperTokens session tokens are valid and verified
- ✅ Multiple tenants can exist with isolated user bases
- ✅ Same email can exist in different tenants
- ✅ JWT claims include tenant_id for tenant context
- ✅ GraphQL resolvers enforce tenant isolation
- ✅ Demo users exist for 3 tenants (9 users total)
- ✅ CLI can login and maintain session without browser
- ✅ WebSocket connections require valid auth token
- ✅ All protected secrets are masked in logs
- ✅ Firebase users successfully migrated or decommissioned
- ✅ No vendor lock-in: SuperTokens can be self-hosted on Hetzner
11. Dependencies & Prerequisites
External:
- Hetzner VM with Docker support
- PostgreSQL 13+ (shared with main application)
- Google OAuth credentials (for social login, optional)
- SMTP/Email service (SendGrid, AWS SES, etc.)
Internal (from other FSDSs):
- M58 Secrets Management: Vault infrastructure and credentials
- Database: Existing multi-tenant schema with tenant_id support
- GraphQL API: Express+Apollo setup ready
Data:
- Firebase user export (for migration)
- Existing tenant definitions
12. Related Documentation
- M58 FSD: Secrets Management (vault credentials)
- Database Schema: Multi-tenant table design
- API Documentation: GraphQL authentication directives
- Deployment Guide: SuperTokens Docker configuration
Version: 1.0 Status: Ready for Implementation Last Updated: 2025-11-03