Authentication & Authorization
Authentication Stack
Section titled “Authentication Stack”ArchiSpark uses Better Auth for session management, with the username, admin, organization (teams enabled) and optional OAuth/OIDC plugins. A session is an httpOnly cookie (better-auth.session_token) — no JWT is ever exposed to browser JavaScript.
apps/control-api is the only service that talks to Better Auth. apps/tenant-api and apps/mcp-server never see the session cookie — control-api signs a short-lived inter-service JWT for every request it forwards to them (see Control-api / tenant-api split).
Sign In
Section titled “Sign In”curl -X POST http://localhost:3000/auth/sign-in/username \ -H "Content-Type: application/json" \ -d '{"username": "admin", "password": "admin"}' \ -c cookies.txtThe session cookie is set automatically. All subsequent requests must include it:
curl http://localhost:3000/elements -b cookies.txtDemo accounts
Section titled “Demo accounts”Four accounts are seeded on first startup, all members of the same default organization:
| Account | Password | Platform role | Org role |
|---|---|---|---|
admin | admin | platform_admin | owner |
user | user | user | member (read-only) |
contrib | contrib | user | admin |
archi | archi | user | owner |
contrib/archi exist so you can exercise the organization admin/owner roles without the platform-wide super admin. Override the passwords via SEED_ADMIN_PASSWORD, SEED_USER_PASSWORD, SEED_CONTRIB_PASSWORD, SEED_ARCHI_PASSWORD — required in production.
Two role systems
Section titled “Two role systems”ArchiSpark has two independent, orthogonal role systems.
Platform role (global, users.role)
Section titled “Platform role (global, users.role)”| Role | Meaning |
|---|---|
user | Default. Subject to organization roles below. |
platform_admin | Bypasses every organization role check. Can create organizations, manage all platform users, suspend/reactivate organizations, configure OAuth providers and site messages — via the admin web. |
Organization role (per-organization, Better Auth organization plugin)
Section titled “Organization role (per-organization, Better Auth organization plugin)”| Role | Permissions |
|---|---|
owner | Full control: workspace content (read/write), members, invitations, teams, workspace management. |
admin | Same as owner except cannot remove the organization itself. |
member | Read-only access to workspace content. |
A user’s organization role only applies within that organization — the same user can be owner of one organization and member of another.
Organizations & Teams
Section titled “Organizations & Teams”ArchiSpark is multi-tenant: each organization has its own members, teams, and workspaces (ArchiMate models). A workspace with one or more assigned teams is visible only to members of those teams (plus org owners/admins); a workspace with no teams is visible to the whole organization.
Org owners/admins (and platform super admins) get an Organization section in the web UI (/organization) to manage workspaces, members, invitations and teams.
Active organization resolution
Section titled “Active organization resolution”Every authenticated request resolves an active organization, in this order:
- The API token’s
organization_id(Bearer token requests). - The session’s
activeOrganizationId(set byorganization.setActive). - Otherwise, the user’s first organization membership.
The resolved role and team memberships are attached to the request as req.workspace ({ organizationId, orgRole, teamIds }).
Auto-activation in the web UI
Section titled “Auto-activation in the web UI”Better Auth’s org-scoped client hooks (useActiveOrganization, useActiveMemberRole) only return data once organization.setActive has been called at least once for the session — and the org switcher only calls it when a user belongs to 2+ organizations. useAutoActivateOrganization() (apps/web/hooks/use-organization.ts) activates the user’s first organization on mount if none is active yet, so single-org admin/owner users (e.g. contrib, archi) correctly see the read-write UI instead of silently falling back to read-only.
Workspace access control
Section titled “Workspace access control”Write operations (POST, PUT, DELETE) on workspace content require org role owner or admin (or platform role platform_admin); members get 403 Forbidden. /auth/*, /users*, and /settings/api-tokens* are exempt from this check.
A platform super admin can suspend an organization (organizations.enabled = false). While suspended, members (other than platform super admins) get 403 Forbidden on every request resolved against it as their active organization — data is untouched and access resumes on reactivation.
API Tokens
Section titled “API Tokens”Personal API tokens (api_tokens table) authenticate REST and MCP requests via Authorization: Bearer <token>, in place of the session cookie. Each token is scoped to one user, one organization, and optionally one workspace (workspace_id), and may have an expiry. Generate one from Mon profil → Tokens API → Nouveau token in the web UI.
curl http://localhost:3000/elements -H "Authorization: Bearer <token>"Cross-subdomain sessions (SaaS topology)
Section titled “Cross-subdomain sessions (SaaS topology)”By default the session cookie is scoped to whichever origin served the request (works for self-hosted single-domain deployments). When apps/web and apps/admin-web are deployed on subdomains of the same root domain (e.g. app.example.com / admin.example.com), set COOKIE_DOMAIN=.example.com on apps/control-api — Better Auth then issues the session cookie for that root domain, so signing in on either subdomain authenticates both. Also list every subdomain origin in TRUSTED_ORIGINS (comma-separated).
OAuth / OIDC providers (optional)
Section titled “OAuth / OIDC providers (optional)”Better Auth supports Google, GitHub, Microsoft Entra ID and generic OIDC providers. Configure them from the admin web Authentication page (stored in the oauth_providers table) or via environment variables. Enabled providers are exposed via GET /auth/providers.
Environment Variables
Section titled “Environment Variables”SEED_ADMIN_PASSWORD=<strong-password> # ⚠ Required in productionSEED_USER_PASSWORD=<strong-password> # ⚠ Required in productionSEED_CONTRIB_PASSWORD=<strong-password> # demo "org admin" accountSEED_ARCHI_PASSWORD=<strong-password> # demo "org owner" accountBETTER_AUTH_SECRET=<random-secret> # ⚠ Required in production — session signingTENANT_JWT_SECRET=<random-secret> # ⚠ Required — control-api ↔ tenant-api inter-service JWTCOOKIE_DOMAIN=.example.com # optional — shared cookie across subdomainsTRUSTED_ORIGINS=https://app.example.com,https://admin.example.com