User Accounts & Authentication (E1)¶
The leader has a built-in user account system: register, login, browser
sessions, and per-user API tokens. Users live in the leader's state DB
(SWARM_JOIN_STATE_DB, default .swarm/join_state.sqlite3).
First run¶
The first registered user automatically becomes an active admin — no manual
DB setup needed. Every later registrant is a plain user whose starting status
depends on the flags below.
Configuration¶
| Env var | Default | Effect |
|---|---|---|
SWARM_REQUIRE_EMAIL_VERIFICATION |
off | New users start unverified and must confirm their email before login (requires the mailer, E11). |
SWARM_REQUIRE_ADMIN_APPROVAL |
off | New users start (or move to) pending_approval; an admin must approve them before they can log in. Works without SMTP. |
SWARM_SESSION_TTL_HOURS |
336 (14 days) |
Browser session lifetime. |
SWARM_COOKIE_SECURE |
1 |
Session cookies are Secure (HTTPS-only). Set 0 only for plain-HTTP local development. |
SWARM_REQUIRE_NODE_AUTH |
off | Enforce node credentials on fellow → leader calls (E5). |
SWARM_REQUIRE_OPERATOR_AUTH |
off | Enforce the authorization sweep (E7): API routes require a session/token/admin per their class. |
SWARM_AUTH_RATE_MAX / SWARM_AUTH_RATE_WINDOW_S |
10 / 60 |
Auth-endpoint rate limit (attempts per window, per IP). |
Account states: unverified → pending_approval → active → disabled.
Login is only possible in active; the login error explains why otherwise.
Pages¶
| Route | Access | Purpose |
|---|---|---|
/ |
public | Landing page (register/login CTAs); logged-in users are redirected to /dashboard. |
/login, /register |
public | Auth forms (redirect to /dashboard when already logged in). |
/dashboard |
session | Post-login home: live pools (with plan quota), create-pool flow, tiles for Training Console, Inference, Profile. |
/pools/{id} |
session + pool visibility | Pool detail: live node status, per-pool join command, jobs, models, members; edit/delete for the owner. Links to the pool-scoped Training Console (/ops/join-v2?pool={id}). |
/profile |
session | Account details, password change, personal API tokens. |
/legal/imprint, /legal/privacy |
public | Legal pages; content from SWARM_IMPRINT_FILE / SWARM_PRIVACY_FILE (HTML fragments), placeholder otherwise. |
The ops pages (/ops/join-v2, /ops/inference) show a logged-in chip
(name + Dashboard link + logout) when a session exists, and a "Log in" link
otherwise. They stay reachable anonymously until the E7 authorization sweep.
Endpoints¶
| Route | Method | Purpose |
|---|---|---|
/api/v1/auth/register |
POST | {email, password, display_name?} — creates the account; logs it straight in when it starts active. |
/api/v1/auth/login |
POST | {email, password} — sets the session cookie. 401 bad credentials, 403 not-yet-active (with reason). |
/api/v1/auth/logout |
POST | Revokes the session, clears the cookie. |
/api/v1/auth/me |
GET | The logged-in user. |
/api/v1/auth/password |
POST | {current_password, new_password} — changes the password and revokes all other sessions. |
Passwords are hashed with scrypt (per-user salt); session and API tokens are
stored only as SHA-256 hashes. State-changing endpoints accept JSON bodies only,
which together with SameSite=Lax cookies blocks cross-site form POSTs.
API tokens (/api/v1/api-tokens) minted while logged in belong to that user;
plain users see and revoke only their own tokens, admins see all.
Pools (admin management, E3)¶
Pools are the spaces where nodes land and connect (e.g. a public pool
thuringia for a localized deployment). Admins manage them without
restrictions at /admin/pools (admin-only page; plain users are redirected
to the dashboard):
- Create with name, slug, description, visibility (
public= listed to all users,private= members only) and an optional node limit (empty = unlimited). - Edit name/description/visibility/limit; the slug is fixed at creation.
- Delete refuses while nodes are attached unless forced (force detaches them) — no silent orphaning.
API (admin session required): GET/POST /api/v1/admin/pools,
GET/PATCH/DELETE /api/v1/admin/pools/{id} (DELETE ?force=true to detach
nodes). The pool owner is automatically its first member. Node↔pool binding is
enforced from E5 on.
User pools & quotas (E4)¶
Any logged-in user can create pools through /api/v1/pools (same verbs as
the admin surface plus GET /api/v1/pools/limits), enforced against the
role_limits table in the state DB:
| Role | Max pools | Max nodes/pool | Visibilities |
|---|---|---|---|
user (default seed) |
1 | 2 | private only |
admin |
unlimited | unlimited | public + private |
Quotas are data, not code — edit the role_limits rows to change a
deployment's plans; enforcement lives in the pool service layer and error
messages state the plan ("Your plan allows 1 pool(s) with up to 2 node(s)
each."). A user pool snapshots the plan's node limit at creation; private
pools are visible only to their members (and admins), public pools are listed
to every logged-in user. GET /api/v1/pools annotates each pool with
my_role (owner / member / null).
Pool-scoped nodes & node credentials (E5)¶
Nodes belong to pools, and everything they do is scoped to their pool:
- Join tokens are pool-scoped.
POST /api/v1/bootstrap/join-sessionsaccepts apool_id; the generated "Add Fellow" command carries that pool's token. A fellow registering with it lands in the pool (pool_nodes), subject to the pool's node limit (409 when full). No token → the deployment default pool (created lazily; migration target for pre-pool fellows). - Node credentials. Registration returns a one-time
node_token(stored hashed; re-registration re-mints it). The fellow presents it as a Bearer token on heartbeat, relay push/pop, log shipping, artifact/servable uploads, job status posts and the inference transport — including the torch subprocesses (training relay via the transport spec, inference relay via theSWARM_RELAY_TOKENenv). - Enforcement:
SWARM_REQUIRE_NODE_AUTH=1rejects calls without a valid credential. Off (default) = migration grace: missing tokens pass with a debug log, but a wrong/revoked/foreign token is always rejected. - Scheduling isolation. A job is bound to a pool
(
JobSubmitRequest.pool_id, default pool otherwise) and only ever combines that pool's nodes; logged-in non-admins can only schedule into pools they belong to and only see their pools' jobs. - Catalog scoping. Servable manifests are stamped with the training job's pool; logged-in non-admins see only their pools' models (plus pre-E5 unstamped ones), and a pool's model is only ever served on that pool's nodes.
Authorization sweep (E7)¶
Every leader route has an entry in
route_policy.py — the single
source of truth — classifying it as public, page (self-gating HTML),
node (fellow credential), chat (API token), session, operator
(session or API token — UI and CLI), or admin. A route with no entry is
unknown = default-deny: the middleware locks it to admin and
test_route_policy.py fails, so an unauthenticated route can't ship by
accident.
Enforcement of session/operator/admin on /api/* routes activates with
SWARM_REQUIRE_OPERATOR_AUTH=1 (grace-off by default, mirroring node auth).
HTML pages keep their own in-handler redirects. Auth events (login, register,
failures) and admin actions (worker release) are written to an audit log,
readable by admins at GET /api/v1/admin/audit. Login/register are rate-limited
per IP.
Roll-out order for a live deployment: enable SWARM_REQUIRE_NODE_AUTH once
fellows have re-registered, then SWARM_REQUIRE_OPERATOR_AUTH once operators
use sessions or API tokens (the CLI already sends its token).
Pool collaboration (E8)¶
Public pools can be shared; private pools are invite-only:
- Invites — a pool owner (or admin) invites by email:
POST /api/v1/pools/{id}/invites {email}. The invitee sees pending invites atGET /api/v1/pools/invites/mineand accepts withPOST /api/v1/pools/invites/{invite_id}/accept(the email must match). - Join requests — for a public pool,
POST /api/v1/pools/{id}/joineither joins immediately (when the pool has open-join enabled) or files a request the owner approves atPOST /api/v1/pools/{id}/join-requests/{user_id}/approve. - Roles & leaving — members have the
memberrole (owner keepsowner); a member can leave and an owner/admin can remove others viaDELETE /api/v1/pools/{id}/members/{user_id}. The owner can't be removed — delete the pool instead. Invite mail is sent through the E11 mailer when configured (best-effort until then).
Admin user management (E10)¶
Admins manage every account at /admin/users (admin-only page). The table is
server-side sorted / filtered / searched / paginated via
GET /api/v1/admin/users?q=&status=&role=&sort=&order=&page=&page_size=, with
columns for status, role, pool/node counts, registration and last-login. A
pending-approval badge appears on the dashboard tile
(GET /api/v1/admin/users/pending-count).
Row actions (all audit-logged):
- Approve — the manual registration confirmation:
pending_approval/unverified→active. - Edit —
PATCH …/{id}display name, email, role — with last-admin protection (can't demote/deactivate/delete the last active admin). - Deactivate / reactivate —
POST …/{id}/status; deactivation revokes the user's sessions and API tokens. - Reset password —
POST …/{id}/reset-passwordissues a single-use, 24h-expiring reset token. With SMTP it is mailed; without, the response carries a one-time/reset-password?token=…link the admin hands over. - Resend verification — re-issues the email-verification token.
- Delete — the E9 GDPR cascade (below).
Account deletion & hardening (E9)¶
GDPR deletion. Deleting a user removes everything that grants access, in a guarded order (last-admin refused before anything is touched): their owned pools are deleted (nodes detached), their memberships in other pools dropped, their API tokens revoked, then the user row, sessions and action tokens are removed. The response summarizes what was deleted.
Brute-force lockout. After SWARM_LOGIN_LOCKOUT_THRESHOLD (default 5)
failed logins for an email, that account is locked for
SWARM_LOGIN_LOCKOUT_SECONDS (default 300s); a success clears the counter. This
is per-account and complements the per-IP auth rate limit.
Session hygiene. Sessions are hash-stored and TTL-bounded
(SWARM_SESSION_TTL_HOURS); UserStore.purge_expired_sessions() drops
revoked/expired rows. Action tokens (verify/reset) are single-use and expire in
24h.
Backup & restore. All account, pool, job, and audit state lives in the one
SQLite file (SWARM_JOIN_STATE_DB, default /data/join_state.sqlite3 in Docker
— a bind-mounted volume). To back up, stop the leader (or use
sqlite3 <db> ".backup '<dest>'" for a hot copy) and archive the file; to
restore, put the file back before starting the leader. Users, pools and node
bindings all survive the round-trip. Keep SWARM_SECRET_KEY and any SMTP
credentials in .env (see .env.example), never in the image or the repo.
Transactional email (E11)¶
When SMTP is configured the leader sends lifecycle mail; without it the mailer is disabled and every feature degrades gracefully (verification skipped, approval via the admin UI, password reset falls back to a one-time link).
Configure via env (all optional): SWARM_SMTP_HOST, SWARM_SMTP_PORT,
SWARM_SMTP_USER, SWARM_SMTP_PASSWORD, SWARM_SMTP_FROM, SWARM_SMTP_TLS
(starttls | ssl | none).
- Queued, non-blocking delivery. Mails are written to a persistent outbox in the state DB and delivered by a background worker with retry/backoff (up to 5 attempts), so a slow or down SMTP server never blocks the triggering request.
- Templates: verify-email, pending-approval, account-approved, account-deactivated, password-reset, password-changed, and pool-invite — tokenized links are single-use and expire in 24h.
- Admin surface: the
/admin/userspage shows an Email card (configured / disabled, pending count) with a Send test email button (GET /api/v1/admin/mail,POST /api/v1/admin/mail/test). From the leader host,swarm admin send-test-mail you@example.comvalidates SMTP directly against the host's env.
Locked out? CLI escape hatch¶
On the leader host (no HTTP, operates directly on the state DB):
# Create an active admin account
swarm admin create-user you@example.com # prompts for password
# Reset a password (also revokes that user's sessions)
swarm admin set-password you@example.com
Both accept --db /path/to/join_state.sqlite3 when the DB isn't at the default
location (in Docker: run inside the leader container, DB at /data/join_state.sqlite3).