Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Birdd reference stack

The Birdd reference stack is the deployment used by the project nodes and the recommended starting point for a new operator. It is an opinionated serving suggestion, not a required architecture: the core birdd service does not depend on Grist, Signal, Garage, Authelia, Caddy, or the included observability tools.

The NixOS full-node role enables the complete stack, but it deliberately does not create third-party accounts, DNS records, or most secrets. It does initialize the local Garage service and its Birdd application credentials. This page describes the remaining operator setup. Third-party account procedures can change independently of Birdd; follow the linked upstream documentation when it differs from this overview.

NixOS configuration

A full node needs four public hostnames pointing to the VPS. The examples on this page use:

PurposeHostname
Grist officeoffice.example.org
Authelia loginauth.example.org
Birdd administrationadmin.example.org
Grafanametrics.example.org

All four names must fit within the configured cookie domain. Allow inbound TCP ports 80 and 443 so Caddy can obtain certificates and serve HTTPS.

The following is a complete configuration skeleton. Replace every example domain, identity, path, and relay with deployment-specific values:

{inputs, pkgs, ...}: {
  imports = [inputs.birdd.nixosModules.full-node];

  time.timeZone = "America/Toronto";
  networking.firewall.allowedTCPPorts = [80 443];

  services.birdd = {
    package = inputs.birdd.packages.${pkgs.system}.birdd-with-docs;
    nodeName = "pallet";
    flockDisplayName = "Pallet Town Birders";
    flockKeyFile = "/var/lib/private/birdd/flock.key";
    relays = ["wss://relay.example.com"];

    signalAccountFile = "/var/lib/private/signal-cli/account";
    signalGroupIdFile = "/var/lib/private/birdd/signal-group-id";
    gristApiKeyFile = "/var/lib/private/birdd/grist-api-key";
    gristDocumentIdFile = "/var/lib/private/birdd/grist-document-id";
    gristWebhookSecretFile = "/var/lib/private/birdd/grist-webhook-secret";

    settings = {
      signal.base_url = "http://127.0.0.1:7583";
      grist.base_url = "http://127.0.0.1:8484";
    };
  };

  services.birddSignalCli.accountFile =
    "/var/lib/private/signal-cli/account";

  # Garage and Birdd media are enabled by full-node. Size this for the VPS.
  services.birddGarage.capacity = "50G";

  services.gristContainer = {
    domain = "office.example.org";
    authDomain = "auth.example.org";
    environmentFile = "/var/lib/private/grist/environment";
  };

  services.birddOperatorPortal = {
    siteName = "Pallet Town Birders";
    officeDomain = "office.example.org";
    authDomain = "auth.example.org";
    adminDomain = "admin.example.org";
    metricsDomain = "metrics.example.org";
    cookieDomain = "example.org";
    grafanaSecretFile = "/var/lib/private/grafana/secret-key";
  };
}

Keep credential files out of the Nix store. Garage creates its RPC and Birdd application credentials under /var/lib/private on first start. The other services expect root-managed files there and do not start when required files are absent. It is safe to apply the NixOS configuration before provisioning every integration; the affected units remain inactive until their files exist.

signal-cli

signal-cli owns the Signal account and protocol state. The birdd-signal adapter consumes its local event stream, accepts commands from one configured group, and sends responses through the same account. The NixOS module keeps the unauthenticated signal-cli HTTP API on loopback and installs signal-cli-admin for account administration under the correct service user.

The paved-road setup uses a dedicated phone number registered directly with signal-cli:

  1. Apply the NixOS configuration while the account and group ID files are still absent. The Signal units will remain inactive.

  2. Create services.birddSignalCli.accountFile containing the account number in E.164 format, such as +15551234567, with root ownership and mode 0600.

  3. Register the number and complete its SMS or voice verification:

    signal-cli-admin register
    signal-cli-admin verify 123-456
    
  4. Add that account to the flock’s Signal group. Receive once to update its local group state, then list the known groups as JSON:

    signal-cli-admin receive --timeout 10
    signal-cli-admin --output json listGroups
    
  5. Find the intended group by name and copy its exact id value into services.birdd.signalGroupIdFile. The opaque base64 group ID, not the group name, is Birdd’s authorization boundary. Protect the file with root ownership and mode 0600.

  6. Start the daemon and adapter, then check the adapter health endpoint:

    systemctl start signal-cli birdd-signal
    curl http://127.0.0.1:8789/health
    

Registration commonly requires a CAPTCHA token, and linking signal-cli as a secondary device uses a different procedure. See the upstream signal-cli quick start and device-linking guide. Do not run a separate root-owned signal-cli instance against the service data directory.

Once the health check succeeds, send !help in the authorized group. See Using Birdd with Signal for the member commands and Configuration for adapter settings.

Garage

Garage stores sanitized image and audio attachments behind an S3-compatible API. It is not exposed publicly: Birdd reads and writes objects through 127.0.0.1:3900, while authorized remote flocks fetch media through Birdd’s HTTPS /_birdd/media route.

The full-node role enables a single-node Garage deployment, generates its RPC secret and Birdd access key, assigns the configured storage capacity, creates the birdd-media bucket, and grants the application key read and write access. The generated credentials are connected to the Signal and Grist adapters through systemd credentials. Check provisioning with:

systemctl status garage birdd-garage-provision
birdd-garage-admin status
birdd-garage-admin bucket info birdd-media

Set services.birddGarage.capacity to the disk space Garage may use. The defaults are otherwise aligned with the Birdd media configuration: region garage, bucket birdd-media, and loopback endpoint http://127.0.0.1:3900. See Media storage for attachment limits and federation behavior.

The reference setup has replication factor one because it runs on one VPS. Garage is object storage, not a backup: losing that VPS loses the only copy of its media unless /var/lib/garage is backed up elsewhere. Consult Garage’s operations documentation before changing topology, replication, or storage layout.

Authelia

Authelia is the authentication boundary for Grist, birdd-admin-web, and Grafana. The reference configuration requires two-factor authentication and uses a local YAML user database.

Create these files under services.birddOfficeAuth.secretsDir, which defaults to /var/lib/private/authelia-birdd:

FileContents
jwt-secretA strong random secret
storage-encryption-keyA separate strong random secret
users-database.ymlOperator identities and password hashes

Generate password hashes with authelia crypto hash generate argon2 and follow Authelia’s user database format. The email address assigned to an operator must match the identity used when provisioning Grist. Keep all three files root-owned and private.

After deployment, sign in at the authentication hostname and request WebAuthn enrollment. The reference stack uses Authelia’s filesystem notifier, so retrieve the registration link from /var/lib/authelia-birdd/notifications.txt on the node instead of waiting for email. Authelia maintains its own persistent database, so include it in the node backup plan.

Grist

Grist provides the spreadsheet interface for reviewing and editing local observations. It runs from a digest-pinned upstream image, stores its data under /var/lib/grist, and trusts only identities supplied by the Authelia and Caddy boundary.

Create services.gristContainer.environmentFile with a random session secret and the email address of the initial operator:

GRIST_SESSION_SECRET=replace-with-a-long-random-secret
GRIST_DEFAULT_EMAIL=operator@example.org

The module supplies the remaining reference-stack environment, including the public URL, forced login, forwarded authentication, service accounts, formula sandboxing, and webhook restrictions. See Grist’s self-hosting documentation before changing those defaults.

Create a separate random bearer secret at services.birdd.gristWebhookSecretFile. Once Grist and Authelia are reachable, run birdd grist-provision from the same reviewed Birdd package revision as the deployed node:

birdd grist-provision \
  --base-url http://127.0.0.1:8484 \
  --admin-email operator@example.org \
  --public-webhook-url https://office.example.org/_birdd/grist-webhook \
  --webhook-secret-file /var/lib/private/birdd/grist-webhook-secret \
  --api-key-output-file /var/lib/private/birdd/grist-api-key \
  --document-id-output-file /var/lib/private/birdd/grist-document-id

Run this command as root because it writes root-managed credential files. It creates or updates the Birdd workspace, document schema, service account, and webhook. Start reconciliation after provisioning and verify both integration units:

systemctl start birdd-grist-reconcile.service birdd-grist-webhook
systemctl status birdd-grist-reconcile.timer birdd-grist-webhook

Re-run provisioning after an upgrade that changes the generated Grist schema or after changing loaded extension projections. See Using Grist for editor behavior.

birdd-admin-web

birdd-admin-web is the operator portal. It reads Birdd’s loopback-only admin API, presents node and federation status, and embeds the Grist and Grafana interfaces. It receives neither the flock key nor adapter credentials.

The services.birddOperatorPortal configuration starts it automatically. There is no separate account setup: Caddy passes the authenticated Authelia identity to the portal. Open the configured administration hostname after Authelia is working.

Caddy

Caddy is the only public HTTP entry point in the reference stack. It obtains TLS certificates and routes the office, authentication, administration, and metrics hostnames to loopback services. The Grist webhook and media paths are routed separately from browser authentication and enforce their own request authorization.

Create DNS records for all four hostnames before starting Caddy, permit inbound ports 80 and 443, and make sure no other service occupies those ports. Do not expose the loopback application ports directly.

Prometheus and Grafana

Prometheus collects Birdd, adapter, host, and service metrics. Grafana serves a pre-provisioned node dashboard through the authenticated metrics hostname. Alloy sends metrics to Prometheus, traces to Tempo, and systemd journal logs to Loki; the operator portal uses the resulting data without a separate setup wizard.

Create services.birddOperatorPortal.grafanaSecretFile containing a strong random value with root ownership and mode 0600. Grafana’s own login form is disabled because the reference stack uses the Authelia identity supplied by Caddy.

Use the checks in Monitoring to verify each local target. The Garage, Prometheus, Grafana, Loki, Tempo, and Authelia data directories are persistent operational state; decide how much history to preserve when following Backups and recovery.