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.ca works
  • 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#

ConventionWhat It GeneratesAggregator
RoutingTraefik HTTP/HTTPS routes with TLS certificates (encryption for secure web traffic)Traefik
SSOAuthelia OIDC client registrations for Single Sign-OnAuthelia
MonitoringPrometheus scrape configurations for metrics collectionPrometheus
BackupBackrest backup plans for app data protectionBackrest
HomepageDashboard service entries for the Homepage UIHomepage

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-signin

PSW 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_token

PSW 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: critical

PSW 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 Traefik

Phase 1: Collect#

When you run psw project render (or when convergence renders before deploying), the convention engine:

  1. Generates convention files for each deployed app based on its metadata
  2. Places them in the app’s convention subdirectory (e.g., services/sonarr/routing/) — these are committed to git as the single source of truth
  3. 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/ folder

Phase 2: Sync#

During convergence , the integration pass pushes the aggregated files to the target and restarts the service. Three sync strategies are available:

StrategyHow It WorksUsed By
dirRsync entire directories to the target, restart serviceTraefik, Prometheus, Backrest
fileCopy individual files to the target, restart serviceHomepage
redeployFull 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:

AggregatorConventionWhat It Collects
TraefikRoutingHTTP/HTTPS route files from all apps
AutheliaSSOOIDC client fragments from SSO-enabled apps
PrometheusMonitoringScrape config files from monitored apps
BackrestBackupBackup plan files from apps with data
HomepageHomepageService 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:
  - ntfy

PSW 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: true

The Integration Pass#

During convergence , after all apps are deployed (Phase 5), the integration pass wires everything together in three steps:

  1. Aggregator sync — Push collected convention files to targets and restart aggregator services
  2. DNS reconciliation — Update provider DNS records for all deployed apps
  3. 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 resolves

All of this from a single psw app add command. Conventions make it possible.