Klyvra Documentation
Everything you need to deploy, configure, and manage your self-hosted productivity suite.
Quick Start Guide
From zero to a running stack in 3 commands. Takes about 5 minutes.
BeginnerYour First Login
Create an admin account, log into every app, and understand the SSO flow.
BeginnerRBAC & Group Mapping
Define groups once, have permissions auto-propagate everywhere. The core feature.
BeginnerAdding Users & Groups
Step-by-step: create users, assign them to groups, watch access appear across apps.
BeginnerDeploying with Helm
Deploy on Kubernetes with the Klyvra Helm chart. Declarative RBAC in values.yaml.
IntermediateCustom Domain & TLS
Move from localhost to a real domain with automatic HTTPS via Let's Encrypt.
IntermediateArchitecture
Hub-and-spoke SSO, adapters, blueprints, and how the pieces fit together.
ReferenceTroubleshooting
Common issues and how to fix them: ports, DNS, SSO, and more.
ReferenceQuick Start Guide
From zero to a running productivity suite in under 5 minutes.
Prerequisites
- Docker Engine β₯ 24.0 with Compose v2 (Docker Desktop works too)
opensslandmake(pre-installed on most Linux/macOS systems)- 8 GB RAM minimum β all 10 apps plus their databases run simultaneously
- Port 80 free (or choose another port β see below)
Three Commands
1 Clone the repository
git clone https://github.com/klyvra-org/klyvra.git && cd klyvra
2 Generate configuration
make bootstrap
This creates a .env file with 30+ randomly generated secrets (database passwords, OIDC client secrets, API keys). You never need to touch these.
make bootstrap HTTP_PORT=8080 instead. All URLs will include the port automatically.
3 Start everything
make up
This builds custom images, starts 33 containers, waits for health checks, and runs init containers. Takes about 2β3 minutes on first run.
What You Get
When make up finishes, these URLs are live:
| App | URL | What it does |
|---|---|---|
| Authentik | auth.localhost | Identity & SSO admin |
| Outline | docs.localhost | Wiki & knowledge base |
| Mattermost | chat.localhost | Team chat & channels |
| Gitea | git.localhost | Git hosting & CI/CD |
| Seafile | files.localhost | File sync & share |
| Kimai | time.localhost | Time tracking |
| Vikunja | tasks.localhost | Task management |
| Vaultwarden | vault.localhost | Password manager |
| Stalwart | mail.localhost | Email, calendar, contacts |
| OnlyOffice | office.localhost | Document editing |
Your First Login
Create an admin account, understand the SSO flow, and log into every app.
1 Create the Authentik Admin
Open http://auth.localhost/if/flow/initial-setup/ in your browser.
You'll see a one-time setup form. Create your admin account with a strong password. This is the only manual step in the entire setup.
2 Explore the Admin Panel
After creating your account, go to http://auth.localhost/if/admin/. You'll see:
- Applications β all 10 apps are pre-configured (done by blueprints)
- Providers β 7 OIDC + 1 SAML provider, all wired up
- Users β just your admin account so far
- Groups β ready for you to create your company's structure
3 Log Into an App
Go to any app URL, for example docs.localhost (Outline). You'll see a login page with an SSO button:
- Outline β "Continue with OIDC"
- Mattermost β "GitLab" (it's actually Authentik β Mattermost calls all OAuth2 "GitLab")
- Gitea β "Sign in with Authentik"
- Seafile β "Single Sign-On"
- Kimai β "Login with Authentik"
- Vikunja β "Authentik"
- Vaultwarden β "Enterprise Single Sign-On" (enter any email β SSO redirects to Authentik)
Click the SSO button β you're redirected to Authentik β log in β redirected back to the app, logged in. That's the flow for all apps.
4 Stalwart Mail
Stalwart is slightly different β mailboxes are created automatically on first SSO login (JIT provisioning). After logging in at mail.localhost, your mailbox is ready for IMAP/SMTP at:
- IMAP:
mail.localhost:143 - SMTP:
mail.localhost:587 - Username: your Authentik email
5 What's Next
Now that you can log in, the next step is setting up your company's group structure:
- Adding Users & Groups β create real users and assign them to groups
- RBAC & Group Mapping β understand how groups translate to per-app permissions
RBAC & Group Mapping
The core feature: define groups once in Authentik, have permissions propagate to every app.
How It Works
Klyvra uses a hub-and-spoke model for access control:
- You create a group in Authentik (e.g., "engineering")
- Authentik fires a webhook to all 4 adapters
- Each adapter creates the matching resource in its app:
- Gitea β creates an organization
- Mattermost β creates a channel
- Outline β creates a group (for collection permissions)
- Stalwart β creates a mailing list
- When you add a user to the group, adapters sync membership
- The user's role in each app is determined by the role mapping config
Role Mapping
Each adapter reads a ROLE_MAPPING environment variable (JSON) that maps group names to app-specific roles:
# Gitea adapter example
ROLE_MAPPING='{"engineering": "owner", "management": "read", "hr": null}'
| Value | Effect |
|---|---|
"owner", "admin", etc. | App-specific role name β see table below |
null | Exclude β this group is NOT synced to this app at all |
| (group not listed) | Default role for the app (usually member) |
Available Roles Per App
| App | Roles | Default |
|---|---|---|
| Gitea | owner Β· admin Β· write Β· read | owner |
| Mattermost | admin Β· member | member |
| Outline | admin Β· member Β· viewer | member |
| Kimai | super_admin Β· teamlead Β· user | Mapped via SAML groups |
| Stalwart | member | member (mailing list) |
The null Role (Exclusion)
Setting a role to null means the group is completely invisible to that app. The adapter won't create anything β no org, no channel, no group. This is how you control information boundaries:
# HR should NOT see code repositories
ROLE_MAPPING='{"hr": null}' # on the Gitea adapter
# Engineering should NOT see HR timesheets
ROLE_MAPPING='{"engineering": null}' # on the Kimai adapter (if applicable)
Helm Chart: Declarative RBAC
When using the Helm chart, role mappings are defined declaratively in values.yaml:
rbac:
groups:
engineering:
displayName: "Engineering"
roles:
gitea: owner # Full repo access
outline: member # Wiki read/write
mattermost: member # Channel member
kimai: user # Track own time
stalwart: member # Mailing list
management:
displayName: "Management"
roles:
gitea: read # View-only repos
outline: admin # Wiki admin
mattermost: admin # Channel admin
kimai: teamlead # Approve timesheets
hr:
displayName: "Human Resources"
roles:
gitea: null # No code access
outline: admin # Wiki admin
kimai: teamlead # Approve timesheets
stalwart: member # Mailing list
The chart automatically converts this into per-adapter ROLE_MAPPING ConfigMaps.
Pending Membership Queue
If you add a user to a group before they've ever logged into an app, the adapter queues the membership. The next time that user logs in (Authentik fires a user_login webhook), all pending memberships are applied automatically.
Architecture
How all the pieces fit together.
Hub-and-Spoke SSO
All traffic enters through Caddy (reverse proxy) on a single port. Caddy routes by subdomain to the correct backend. Authentication flows through Authentik as the central identity hub.
Browser β http://{app}.{DOMAIN}:{PORT}
β
βββ Caddy (reverse proxy, static IP 172.28.0.250)
βββ auth.* β Authentik (OIDC/SAML provider)
βββ docs.* β Outline (OIDC)
βββ chat.* β Mattermost (OAuth2)
βββ git.* β Gitea (OIDC)
βββ files.* β Seafile (OAuth2)
βββ time.* β Kimai (SAML)
βββ tasks.* β Vikunja (OIDC)
βββ vault.* β Vaultwarden (OIDC)
βββ mail.* β Stalwart (OIDC)
βββ office.* β OnlyOffice (JWT)
βββ minio.* β MinIO (internal)
Authentication Protocols
| App | Protocol | Notes |
|---|---|---|
| Outline, Gitea, Vikunja, Vaultwarden, Stalwart | OIDC | Standard OpenID Connect |
| Mattermost | OAuth2 | Uses "GitLab" provider type (same protocol) |
| Seafile | OAuth2 | Custom OAuth via patched settings |
| Kimai | SAML 2.0 | Only external auth method Kimai supports |
| OnlyOffice | JWT | No user accounts β shared secret with Outline |
Blueprints (Zero-Touch Config)
Authentik blueprints are YAML files that declaratively create all SSO providers, webhook transports, and scope mappings. They're mounted into the Authentik worker and auto-applied on startup β no UI clicks needed.
There are 15 blueprints in apps/authentik/blueprints/: one per OIDC/SAML provider, one per webhook transport, plus shared scope and comms launcher configs.
Adapters
Adapters are lightweight FastAPI microservices that receive Authentik webhooks and translate them into app-specific API calls. There are 4 adapters:
| Adapter | Receives | Creates |
|---|---|---|
| outline-adapter | Group CRUD, user login | Outline groups + membership |
| mattermost-adapter | Group CRUD, user login | Channels + membership + slash commands |
| gitea-adapter | Group CRUD, user login | Organizations + team membership |
| stalwart-adapter | Group CRUD, user login | Mailing lists + JIT mailbox provisioning |
Init Containers
Init containers run once at first boot to configure apps that can't be fully configured via environment variables alone. All are idempotent β safe to re-run.
| Container | What it does |
|---|---|
| outline-init | Seeds API key, promotes first user to admin |
| mattermost-init | Creates team + bot token + slash commands |
| gitea-init | Registers OIDC auth source + service account |
| kimai-init | Fetches SAML signing cert, generates config |
| stalwart-init | Creates mail domain via REST API |
| minio-init | Creates S3 bucket for Outline |
The Split URL Pattern
A critical implementation detail: browser-facing and server-to-server OIDC URLs must differ inside Docker.
- Browser:
http://auth.localhost(external, via Caddy) - Server:
http://authentik-server:9000(Docker internal hostname)
Apps that support separate browser/server URLs use this split directly. Apps with a single authority URL (Gitea, Vikunja, Vaultwarden) use extra_hosts to point the auth domain to Caddy's static IP.
Adapters & Webhooks
How Authentik events propagate to every app in real time.
What Are Adapters?
Adapters are lightweight Python (FastAPI) microservices that act as translators between Authentik and each app. When something happens in Authentik (a group is created, a user logs in), Authentik fires an HTTP webhook to the relevant adapters, which then call the target app's API.
Authentik event
β
βββwebhookβββ outline-adapter ββAPIβββ Outline
βββwebhookβββ mattermost-adapter ββAPIβββ Mattermost
βββwebhookβββ gitea-adapter ββAPIβββ Gitea
βββwebhookβββ stalwart-adapter ββAPIβββ Stalwart
Webhook Events
| Event | Trigger | Adapter Action |
|---|---|---|
model_created (group) | New group created in Authentik | Create org/channel/group/mailing list |
model_updated (group) | Group membership changed | Sync membership to app |
model_deleted (group) | Group deleted | Delete corresponding resource |
user_login | User logs into any app | Apply pending memberships, JIT mailbox |
Pending Queue
Each adapter maintains a SQLite database for pending memberships. If a user is added to a group but hasn't logged into the target app (no local account exists), the membership is queued. On next login, all pending memberships are applied.
Adapter Endpoints
POST /webhook β Receives Authentik events
GET /health β Liveness probe (returns 200)
GET /pending β Lists queued pending operations
Shared Base
All adapters share apps/_shared/adapter_base.py which provides:
- SQLite database initialization and pending queue management
- Webhook payload parsing
load_role_mapping()β parsesROLE_MAPPINGenv varget_group_role()β resolves a group name to its app-specific role (or None for exclusion)
Adding Users & Groups
Step-by-step: create your company's user structure in Authentik.
1 Plan Your Groups
Think about your company's job functions. Each group maps to a set of permissions across all apps. Common examples:
| Group | Who's in it | What they get |
|---|---|---|
engineering | Developers, DevOps | Full code access, wiki read/write, task boards |
management | Team leads, executives | Wiki admin, timesheet approval, view-only code |
hr | HR team | Wiki admin, timesheet approval, NO code access |
design | Designers | File access, wiki read/write, task boards |
2 Create Groups in Authentik
- Open Authentik admin: auth.localhost/if/admin β Directory β Groups
- Click Create
- Enter the group name (e.g.,
engineering) - Click Create
Within seconds, all 4 adapters receive a webhook and create the corresponding resources:
- Gitea: organization
engineering - Mattermost: channel
engineering - Outline: group
engineering - Stalwart: mailing list
engineering@yourdomain.com
3 Create Users
- Go to Directory β Users in Authentik admin
- Click Create
- Fill in: username, display name, email
- Set a temporary password (user can change on first login)
- Click Create
4 Add Users to Groups
- Open the user you just created
- Go to the Groups tab
- Click Add to existing group
- Select the group(s) this user belongs to
5 Tell Users to Log In
Share the app URLs with your users. On first login to any app via SSO:
- A local account is auto-created in the app
- All pending group memberships are applied
- If Stalwart is enabled, a mailbox is auto-provisioned
- If Mattermost onboarding is enabled, they get a welcome DM with communication links
Custom Domain & TLS
Move from localhost to a real domain with automatic HTTPS.
Setting a Custom Domain
make destroy # Clean slate
make bootstrap DOMAIN=company.com # Generate .env with your domain
make up # Start everything
All services become auth.company.com, docs.company.com, chat.company.com, etc.
DNS Setup
Point a wildcard DNS record to your server:
*.company.com A β your-server-ip
Or create individual A records for each subdomain: auth, docs, chat, git, files, time, tasks, vault, mail, office, minio.
Enabling TLS (HTTPS)
Caddy supports automatic Let's Encrypt certificates. To enable:
1 Edit the Caddyfile
Open apps/caddy/Caddyfile and remove the auto_https off line. Change the protocol from http:// to bare domain matching:
# Before:
http://auth.{$DOMAIN}:{$HTTP_PORT} { ... }
# After (Caddy auto-enables HTTPS):
auth.{$DOMAIN} { ... }
2 Update ports
In compose.yaml, expose ports 80 and 443 on Caddy for HTTPβHTTPS redirect and TLS respectively.
3 Restart
make down && make up
Caddy automatically obtains and renews Let's Encrypt certificates for all subdomains.
Deploying with Helm
Deploy Klyvra on Kubernetes with the Helm chart.
Prerequisites
- Kubernetes cluster (1.25+)
- Helm 3
- An ingress controller (nginx-ingress, Traefik, etc.)
- cert-manager (for automatic TLS) β optional but recommended
Build & Push Images
Klyvra uses 10 custom images (adapters, init jobs, custom Stalwart/Seafile). Build and push them to your registry:
# Build locally
make images
# Push to your registry
REGISTRY=ghcr.io/your-org TAG=v0.1.0 make push-images
Install
helm install klyvra ./chart/klyvra \
--set global.domain=company.com \
--set global.imageRegistry=ghcr.io/your-org \
--set global.imageTag=v0.1.0
Configure RBAC in values.yaml
The chart's values.yaml has a dedicated RBAC section. Define your groups and their per-app roles:
rbac:
groups:
engineering:
displayName: "Engineering"
roles:
gitea: owner
outline: member
mattermost: member
kimai: user
stalwart: member
management:
displayName: "Management"
roles:
gitea: read
outline: admin
mattermost: admin
kimai: teamlead
hr:
displayName: "Human Resources"
roles:
gitea: null # Excluded from code repos
outline: admin
kimai: teamlead
The chart converts this into per-adapter ConfigMaps automatically β no manual JSON wrangling.
Enable/Disable Apps
# values.yaml
outline:
enabled: true
kimai:
enabled: false # Don't need time tracking? Disable it.
Ingress
The chart creates Ingress resources for all 11 subdomains. Configure your ingress class:
global:
ingress:
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
helm install, the same first-login process applies: visit the Authentik URL to create the admin account, then start creating users and groups.
App Reference
All 10 apps at a glance β what they do, how they authenticate, and what they replace.
| App | Subdomain | Auth | Replaces | Group Sync |
|---|---|---|---|---|
| Authentik | auth.* | Hub | Okta, Azure AD | β |
| Outline | docs.* | OIDC | Notion, Confluence | β Adapter |
| Mattermost | chat.* | OAuth2 | Slack, Teams | β Adapter |
| Gitea | git.* | OIDC | GitHub, GitLab | β Adapter |
| Seafile | files.* | OAuth2 | Google Drive, OneDrive | SSO only |
| Kimai | time.* | SAML | Toggl, Harvest | SAML groups |
| Vikunja | tasks.* | OIDC | Asana, Trello | SSO only |
| Vaultwarden | vault.* | OIDC | 1Password, LastPass | SSO only |
| OnlyOffice | office.* | JWT | Google Docs, MS Office | No accounts |
| Stalwart | mail.* | OIDC | Gmail, Exchange | β Adapter |
| MinIO | minio.* | Internal | AWS S3 | No accounts |
Notes
- OnlyOffice has no user accounts β it's embedded in Outline for document editing (Word/Excel/PowerPoint). Authenticated via shared JWT secret.
- MinIO is the S3 backend for Outline's file storage. Not a user-facing app. Uses static admin credentials.
- Vaultwarden uses the OIDCWarden fork β the official image doesn't support SSO.
- Mattermost uses Enterprise Edition (free tier, no license needed) because Team Edition removed OAuth in v9.x.
- Kimai maps SAML group claims to roles:
kimai-adminβ SUPER_ADMIN,kimai-teamleadβ TEAMLEAD.
Environment Variables
All config lives in .env, generated by make bootstrap.
User-Configurable
| Variable | Default | Description |
|---|---|---|
DOMAIN | localhost | Base domain β all subdomains derived from this |
HTTP_PORT | 80 | Port Caddy listens on |
MATTERMOST_TEAM_NAME | main | Default Mattermost team name |
ENABLE_COMMS_COMMANDS | true | Create /comms, /mail, /calendar slash commands |
ENABLE_COMMS_ONBOARDING | true | Send welcome DM with comms links on first login |
MAIL_URL | http://mail.${DOMAIN} | Target URL for mail redirect |
CALENDAR_URL | http://calendar.${DOMAIN} | Target URL for calendar redirect |
CONTACTS_URL | http://contacts.${DOMAIN} | Target URL for contacts redirect |
Auto-Generated (don't touch)
make bootstrap generates 30+ secrets using openssl rand. These include:
- Database passwords for all 7 databases
- OIDC client secrets for all 7 OIDC providers
- Authentik secret key, bootstrap token
- MinIO root password
- OnlyOffice JWT secret
- Stalwart admin password
make bootstrap, it regenerates all passwords. Existing database volumes still have the old passwords. Run make destroy first to wipe volumes.
Troubleshooting
Common issues and how to fix them.
Port 80 in use
make bootstrap HTTP_PORT=8080
make up
Stale volumes after re-bootstrapping
If you re-run make bootstrap (which regenerates passwords), existing database volumes still have the old passwords:
make destroy # removes containers AND volumes
make bootstrap # fresh .env
make up # clean start
*.localhost doesn't resolve
Most modern systems resolve *.localhost to 127.0.0.1 (RFC 6761). If your browser can't reach the URLs, add entries to /etc/hosts:
# /etc/hosts
127.0.0.1 auth.localhost docs.localhost chat.localhost git.localhost
127.0.0.1 files.localhost time.localhost tasks.localhost vault.localhost
127.0.0.1 mail.localhost office.localhost minio.localhost
Service won't start
docker compose logs <service-name> # Check logs
docker compose ps -a # Check exit codes
make logs SVC=<service-name> # Follow logs
SSO button missing in Seafile
Seafile's OAuth config is patched at runtime by an embedded boot script. If the SSO button is missing:
docker logs klyvra-seafile-1 2>&1 | grep oauth-boot
Vaultwarden SSO not working
Klyvra uses OIDCWarden (timshel/oidcwarden:latest), not the official Vaultwarden image. The official image silently ignores SSO_* env vars. OIDCWarden still requires a master password after SSO login (by design β it encrypts your vault).
Mattermost shows "GitLab" as SSO provider
This is expected. Mattermost's free tier only supports "GitLab" as an OAuth2 provider type. Authentik is configured to match GitLab's userinfo format, so it works transparently. The button says "GitLab" but actually redirects to Authentik.
Init container keeps restarting
Init containers use restart: on-failure and retry until their target app is ready. Check if the target app is healthy:
docker compose ps # Look for unhealthy services
make status # Full status dashboard
Checking adapter health
# View adapter logs
make logs SVC=outline-adapter
make logs SVC=gitea-adapter
# Check pending queue
docker compose exec outline-adapter curl -s localhost:8000/pending
Makefile Commands
Every command available for managing your Klyvra deployment.
| Command | Description |
|---|---|
make bootstrap | Generate .env with random secrets |
make bootstrap DOMAIN=x HTTP_PORT=y | Custom domain and port |
make up | Build images, start all services, wait for health |
make down | Stop all services (data preserved in volumes) |
make destroy | Stop and delete all data (volumes removed) |
make start / make stop | Start/stop without rebuilding |
make healthcheck | Quick HTTP check on all routes |
make test | Run 95 adapter unit tests |
make verify | Run full verification suite (74 checks) |
make logs [SVC=x] | Follow container logs (all or specific) |
make restart [SVC=x] | Restart services (all or specific) |
make status | Container status dashboard |
make images | Build all 10 custom Docker images |
make push-images | Build and push images to registry |