Auth & Users
Inhalt
Authentication and authorization are handled by Better Auth (apps/api/src/auth.ts). Sessions are managed server-side; all role checks are enforced at the API layer.
Authentication Providers
| Provider | Status | Notes |
|---|---|---|
| Email / password | Available | Default — enabled on all instances |
| OIDC (Authentik, Keycloak, etc.) | Planned v1.0 | OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET appear in .env.example but are not read by the code yet |
| Passkey / WebAuthn | Planned v1.0 | Passwordless, device-bound credentials |
| Social OAuth (GitHub, Google, etc.) | Not planned | Out of scope for self-hosted use case |
User Roles
| Role | Description | Permissions |
|---|---|---|
| Admin | Full instance control | Everything — settings, users, content, theme, legal |
| Moderator | Content moderation | Role exists in the schema but currently has no rights of its own in the code. Reports and takedowns are admin-only |
| Creator | Content creation | Upload videos, manage own channels and videos, edit Studio |
| Viewer | Consumption | Watch public, unlisted and logged_in videos, comment where enabled. The code currently has no role check on upload |
Role assignment is managed in /admin/users. A user can hold one role at a time. Admins can promote or demote any account. New accounts get the Creator role by default.
Admin Bootstrap
On a fresh instance, the first-run wizard at /setup normally creates the admin account and sets the Admin role directly. The INITIAL_ADMIN_EMAIL environment variable is an optional fallback and also pre-fills and locks the wizard's email field.
How it works:
- Set
INITIAL_ADMIN_EMAIL=you@example.comin your environment or compose file - Register a new account using that exact email address
- The Better Auth hook
databaseHooks.user.create.afterchecks the email and automatically promotes the account to Admin
The hook runs on every sign-up, and this address may register even when registration is closed. Since email addresses are unique, leaving the variable set is harmless once the account exists. Further promotions are done through the admin panel.
Session Management
- Cookie: HTTP-only session cookie, set on successful login
- SameSite:
Lax— protects against CSRF while allowing top-level navigation - Secure flag: Better Auth default, not configured separately in
apps/api/src/auth.ts - Session lifetime: Better Auth default, not configured separately in
apps/api/src/auth.ts - Storage: Sessions are stored server-side in the Postgres database (via Better Auth's built-in session table)
Clients never receive the raw session secret. All session validation happens on the API.
Visibility Enforcement
Video visibility is enforced centrally via canViewVideo() at every read endpoint. No client-side gating — the check happens in the tRPC procedure before any data is returned.
| Visibility level | Who can watch |
|---|---|
public |
Everyone, including unauthenticated visitors |
unlisted |
Anyone with the direct link — not shown in feeds or search |
logged_in |
Authenticated users only (any role) |
private |
Video owner and Admins only |
There are no signed playback URLs. HLS files are served directly by the container via /s3/. The visibility check covers the video data returned by the API.
Rate Limiting
Rate limits are enforced at the Fastify layer before requests reach tRPC.
| Endpoint group | Limit |
|---|---|
| Global | none, only individual routes are limited |
Auth routes /api/auth/* (login, sign-up, reset) |
20 requests / minute / IP |
Video upload /api/upload |
10 requests / minute / IP |
Setup logo /api/setup/logo |
6 requests / minute / IP |
Routes without their own values use RATE_LIMIT_MAX (default 120) and RATE_LIMIT_WINDOW_MS (default 60000). Counters are kept in Redis via @fastify/rate-limit (key prefix play-rl:). In the all-in-one container, Redis is internal and always available.
Registration Control
/admin/settings and the first-run wizard offer three modes (Open, Invite only, Closed). The setting lives in the database (SiteSettings.registrationMode). When registration is not open:
- Sign-ups are rejected with
403, error codeREGISTRATION_CLOSEDorINVITE_REQUIRED - Existing accounts are unaffected
- The address from
INITIAL_ADMIN_EMAILcan still register
Invite tokens are not implemented yet, so "Invite only" currently blocks all new sign-ups. There is no way to create accounts from /admin/users.