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 | Standard OpenID Connect, configurable via env |
| 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 | View all content, hide/delete videos/comments, manage report queue |
| Creator | Content creation | Upload videos, manage own channels and videos, edit Studio |
| Viewer | Consumption only | Watch public and unlisted videos, submit comments (if enabled by admin) |
Role assignment is managed in /admin/users. A user can hold one role at a time. Admins can promote or demote any account.
Admin Bootstrap
On a fresh instance, the first admin is bootstrapped via the INITIAL_ADMIN_EMAIL environment variable.
How it works:
- Set
INITIAL_ADMIN_EMAIL=you@example.comin your environment or compose file - Register a new account using that exact email address
- Better Auth's
onAfterSignuphook checks the email and automatically promotes the account to Admin
This is a one-time operation. Once an Admin exists, further promotions are done through the admin panel. The env var can be left set without side effects — it only fires for the matching email once.
Session Management
- Cookie: HTTP-only session cookie, set on successful login
- SameSite:
Lax— protects against CSRF while allowing top-level navigation - Secure flag: Set automatically when
BASE_URLuseshttps:// - Session lifetime: 30 days (sliding expiry on activity)
- 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 |
Signed playback URLs for logged_in and private videos expire after 1 hour. Attempting to play an expired URL returns a 403.
Rate Limiting
Rate limits are enforced at the Fastify layer before requests reach tRPC.
| Endpoint group | Limit |
|---|---|
| Global | 60 requests / minute / IP |
| File upload (tus) | 5 parallel uploads per user |
| Account registration | 3 new accounts / hour / IP |
| Login attempts | 10 attempts / 15 minutes / IP (then 15 min lockout) |
Limits are tracked in Redis with a sliding window. In the all-in-one container, Redis is internal and always available.
Registration Control
Admins can disable public registration in /admin/settings (toggle: Allow new registrations). When disabled:
- The
/registerpage shows a "Registration is closed" message - The registration API endpoint returns
403 REGISTRATION_DISABLED - Existing accounts are unaffected
This enables invite-only mode. Admins can still create accounts manually from /admin/users.
© Benjamin Weber · ITSWEBER — play.itsweber.net · GitHub