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:
| Purpose | Hostname |
|---|---|
| Grist office | office.example.org |
| Authelia login | auth.example.org |
| Birdd administration | admin.example.org |
| Grafana | metrics.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:
-
Apply the NixOS configuration while the account and group ID files are still absent. The Signal units will remain inactive.
-
Create
services.birddSignalCli.accountFilecontaining the account number in E.164 format, such as+15551234567, with root ownership and mode0600. -
Register the number and complete its SMS or voice verification:
signal-cli-admin register signal-cli-admin verify 123-456 -
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 -
Find the intended group by name and copy its exact
idvalue intoservices.birdd.signalGroupIdFile. The opaque base64 group ID, not the group name, is Birdd’s authorization boundary. Protect the file with root ownership and mode0600. -
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:
| File | Contents |
|---|---|
jwt-secret | A strong random secret |
storage-encryption-key | A separate strong random secret |
users-database.yml | Operator 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.