Skip to content

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: unverifiedpending_approvalactivedisabled. 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-sessions accepts a pool_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 the SWARM_RELAY_TOKEN env).
  • Enforcement: SWARM_REQUIRE_NODE_AUTH=1 rejects 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 at GET /api/v1/pools/invites/mine and accepts with POST /api/v1/pools/invites/{invite_id}/accept (the email must match).
  • Join requests — for a public pool, POST /api/v1/pools/{id}/join either joins immediately (when the pool has open-join enabled) or files a request the owner approves at POST /api/v1/pools/{id}/join-requests/{user_id}/approve.
  • Roles & leaving — members have the member role (owner keeps owner); a member can leave and an owner/admin can remove others via DELETE /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 / unverifiedactive.
  • EditPATCH …/{id} display name, email, role — with last-admin protection (can't demote/deactivate/delete the last active admin).
  • Deactivate / reactivatePOST …/{id}/status; deactivation revokes the user's sessions and API tokens.
  • Reset passwordPOST …/{id}/reset-password issues 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/users page 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.com validates 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).