Conventions#
What Is a Convention?#
A convention is a standardized way for apps to plug into shared infrastructure automatically. Instead of manually configuring Traefik routes, SSO clients, or monitoring targets for every app you install, PSW uses conventions to generate all that wiring for you.
When you add Jellyfin , PSW doesn’t just deploy it — it also:
- Creates a Traefik
route so
https://jellyfin.yourdomain.caworks - Registers an SSO client with Authelia (if the app supports it)
- Adds a Prometheus scrape target (if the app exposes metrics)
- Creates a backup plan for Backrest (if the app has data worth backing up)
- Adds a dashboard entry on Homepage
All of this happens because Jellyfin’s metadata declares which conventions it participates in. You don’t configure any of it — PSW handles it.
The Five Convention Types#
| Convention | What It Generates | Aggregator |
|---|---|---|
| Routing | Traefik HTTP/HTTPS routes with TLS certificates (encryption for secure web traffic) | Traefik |
| SSO | Authelia OIDC client registrations for Single Sign-On | Authelia |
| Monitoring | Prometheus scrape configurations for metrics collection | Prometheus |
| Backup | Backrest backup plans for app data protection | Backrest |
| Homepage | Dashboard service entries for the Homepage UI | Homepage |
Routing#
Every app
with a subdomain gets a Traefik route. If your domain is casaeureka.ca and you add Sonarr
, PSW generates a route file:
# services/sonarr/routing/sonarr-routes.yml (auto-generated)
http:
routers:
sonarr:
rule: "Host(`sonarr.casaeureka.ca`)"
entryPoints: [websecure]
service: sonarr
middlewares: [authelia-forwardauth@file] # middleware name matches the forward-auth provider app
tls:
certResolver: letsencrypt
services:
sonarr:
loadBalancer:
servers:
- url: "http://10.10.0.101:8989"This makes https://sonarr.casaeureka.ca work with automatic HTTPS and SSO protection — all without you touching a single Traefik config file. The forward-auth middleware name (here authelia-forwardauth) is derived automatically from whichever app declares routing_mode: forward_auth_provider
in its metadata.
SSO#
Apps that support OAuth2/OIDC (industry-standard authentication protocols) can integrate with Authelia for Single Sign-On (SSO). The app declares this in its metadata :
# meta.yml
sso:
redirect_path: /identity/connect/oidc-signinPSW generates an OIDC (OpenID Connect — a protocol for verifying user identity) client fragment for Authelia , complete with client ID, client secret (from secrets ), and redirect URI. After convergence , logging into one app logs you into all of them.
Monitoring#
Apps that expose Prometheus metrics get a scrape configuration:
# meta.yml
monitoring:
metrics_path: /api/prometheus
auth_type: bearer
auth_secret: app_prometheus_tokenPSW generates a Prometheus scrape config so your Grafana dashboards automatically pick up the new app’s metrics.
Backup#
Apps with important data declare what to back up in their meta.yml. Each declaration registers one or more bundles in PSW’s bundles framework
, and the backrest.plans reconciler turns those bundles into Backrest
plans:
# meta.yml
backup:
database: true
volumes:
- path: /config
priority: criticalPSW renders one YAML per registered bundle under services/<app>/bundles/; Backrest’s aggregator collects them and the convergence engine pushes the resulting plan list into Backrest’s /config/config.json. See backups.md
for the full bundles + transports story.
Homepage#
Every deployed app with a subdomain automatically gets an entry on the Homepage dashboard, organized by category. No configuration needed.
How Conventions Flow#
Convention files flow through a two-phase pipeline: collect then sync.
Phase 1: Collect (during psw project render)
──────────────────────────────────────
Each app's convention files are generated, then collected
into the aggregator's directory:
services/sonarr/routing/sonarr-routes.yml ───┐
services/radarr/routing/radarr-routes.yml ───┤
services/jellyfin/routing/jellyfin-routes.yml┼──► services/traefik/dynamic/
services/grafana/routing/grafana-routes.yml ─┘
Phase 2: Sync (during convergence integration pass)
──────────────────────────────────────────────────────
Aggregated files are pushed to the target and the
aggregator service is restarted:
services/traefik/dynamic/ ──► rsync to target ──► restart TraefikPhase 1: Collect#
When you run psw project render (or when convergence
renders before deploying), the convention engine:
- Generates convention files for each deployed app based on its metadata
- Places them in the app’s convention subdirectory (e.g.,
services/sonarr/routing/) — these are committed to git as the single source of truth - Collects all convention files into the aggregator’s directory (e.g.,
services/traefik/dynamic/) — this destination is.gitignored because it’s derived from step 2. Every render on the workstation and every convergence run on the bootstrap target rebuilds it from the per-app fragments
Each aggregator declares where to collect from and where to put the results in its metadata :
# traefik's meta.yml
aggregator:
convention: routing
collect:
source_subdir: routing # Look in each app's routing/ folder
file_glob: "*.yml" # Grab all .yml files
dest_subdir: dynamic # Put them in traefik's dynamic/ folderPhase 2: Sync#
During convergence , the integration pass pushes the aggregated files to the target and restarts the service. Three sync strategies are available:
| Strategy | How It Works | Used By |
|---|---|---|
| dir | Rsync entire directories to the target, restart service | Traefik, Prometheus, Backrest |
| file | Copy individual files to the target, restart service | Homepage |
| redeploy | Full re-deploy of the app (for apps that need their config re-rendered from aggregated data) | Authelia |
Aggregators#
An aggregator is an infrastructure app that collects conventions from other apps. There’s one aggregator per convention type:
| Aggregator | Convention | What It Collects |
|---|---|---|
| Traefik | Routing | HTTP/HTTPS route files from all apps |
| Authelia | SSO | OIDC client fragments from SSO-enabled apps |
| Prometheus | Monitoring | Scrape config files from monitored apps |
| Backrest | Backup | Backup plan files from apps with data |
| Homepage | Homepage | Service entries for the dashboard |
Aggregators are just regular apps
that happen to declare an aggregator: capability in their metadata
. They’re deployed as core apps
(Traefik, Authelia) or as user-added apps (Prometheus, Backrest, Homepage).
Wiring #
Some app integrations go beyond convention files — they require API calls or config file edits. PSW handles this automatically through wiring , which splits into two phases: setup reconcilers run inline during deploy to configure an app’s own internals (admin accounts, API keys), and integration reconcilers run in the integration pass to connect one app to another.
Each app declares which other apps it integrates with in its meta.yml:
# backrest's meta.yml
integrations:
- ntfyPSW reads these declarations and performs the actual wiring automatically. For example, when both Backrest and ntfy are deployed, PSW injects the ntfy notification hook into Backrest’s config. This runs automatically during convergence — you don’t need to configure anything manually.
Broadcast Apps#
A broadcast app is a special type of app that automatically deploys to every managed target instead of a single one. You don’t assign it to a specific target — PSW injects it everywhere.
Examples:
- Alloy — log collector, shipped to every target so logs flow to Loki
- Node Exporter — metrics exporter, shipped to every target so Prometheus can scrape system metrics
Declared in the app’s metadata :
# alloy's meta.yml
broadcast: trueThe Integration Pass#
During convergence , after all apps are deployed (Phase 5), the integration pass wires everything together in three steps:
- Aggregator sync — Push collected convention files to targets and restart aggregator services
- DNS reconciliation — Update provider DNS records for all deployed apps
- Integration reconcilers — Connect apps to each other (SSO login, download clients, notifications, monitoring). Setup reconcilers already ran during the deploy phase, so by the time the integration pass kicks in each app already has its admin account and API key in place.
This is why adding an app is a two-step experience: first the app itself is deployed, then the integration pass makes it discoverable (routable, monitored, backed up, on the dashboard).
The Big Picture#
Here’s the full flow when you add an app:
psw app add sonarr --target media
│
▼
psw project render
├── Generates services/sonarr/routing/sonarr-routes.yml
├── Generates services/sonarr/sso/sonarr-sso.yml
├── Generates services/sonarr/monitoring/sonarr-scrape.yml
├── Collects routing file into services/traefik/dynamic/
├── Collects SSO file into services/authelia/oidc-clients/
└── Collects monitoring file into services/prometheus/scrape-configs/
│
▼
git commit && git push
│
▼
Convergence deploys Sonarr to the media target
│
▼
Integration pass:
├── Traefik gets the new route → sonarr.yourdomain.ca works
├── Authelia gets the SSO client → SSO login works
├── Prometheus gets the scrape config → metrics flow to Grafana
└── DNS provider gets the new record → domain resolvesAll of this from a single psw app add command. Conventions make it possible.