Skip to content

Authentication & Authorization

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).

Terminal window
curl -X POST http://localhost:3000/auth/sign-in/username \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin"}' \
-c cookies.txt

The session cookie is set automatically. All subsequent requests must include it:

Terminal window
curl http://localhost:3000/elements -b cookies.txt

Four accounts are seeded on first startup, all members of the same default organization:

AccountPasswordPlatform roleOrg role
adminadminplatform_adminowner
useruserusermember (read-only)
contribcontribuseradmin
archiarchiuserowner

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_PASSWORDrequired in production.

ArchiSpark has two independent, orthogonal role systems.

RoleMeaning
userDefault. Subject to organization roles below.
platform_adminBypasses 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)”
RolePermissions
ownerFull control: workspace content (read/write), members, invitations, teams, workspace management.
adminSame as owner except cannot remove the organization itself.
memberRead-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.

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.

Every authenticated request resolves an active organization, in this order:

  1. The API token’s organization_id (Bearer token requests).
  2. The session’s activeOrganizationId (set by organization.setActive).
  3. Otherwise, the user’s first organization membership.

The resolved role and team memberships are attached to the request as req.workspace ({ organizationId, orgRole, teamIds }).

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.

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.

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.

Terminal window
curl http://localhost:3000/elements -H "Authorization: Bearer <token>"

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).

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.

Terminal window
SEED_ADMIN_PASSWORD=<strong-password> # ⚠ Required in production
SEED_USER_PASSWORD=<strong-password> # ⚠ Required in production
SEED_CONTRIB_PASSWORD=<strong-password> # demo "org admin" account
SEED_ARCHI_PASSWORD=<strong-password> # demo "org owner" account
BETTER_AUTH_SECRET=<random-secret> # ⚠ Required in production — session signing
TENANT_JWT_SECRET=<random-secret> # ⚠ Required — control-api ↔ tenant-api inter-service JWT
COOKIE_DOMAIN=.example.com # optional — shared cookie across subdomains
TRUSTED_ORIGINS=https://app.example.com,https://admin.example.com