Feature Specification: Component mastodon

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/components/operator/mastodon/default.nix, core/components/operator/mastodon/oidc.nix, core/components/operator/mastodon/test.nix, core/components/operator/mastodon/test-script.py, core/components/operator/default.nix, core/components/operator/app-resources.nix, core/components/operator/operator-facing-options.csv, core/components/operator/testing/lib.nix, core/components/sharedOptions.nix, core/checks/nix-unit-tests.nix), pull requests #1384, #1597, #1651, #1724, #1742, #1750, #1752, #1762, #1768, #1834, #1835, #1846, #1853, #1867, #1911.

User Scenarios & Testing (mandatory)

User Story 1 - Run a Mastodon instance for a group (Priority: P1)

An operator enables mastodon in the operator group. The component places resource requests for single sign-on, mail, cache, database, object storage, workload identity, TLS and public ports. The setups route each request to a provider in the deployment. The node then serves Mastodon at https://<subdomain>.<domain>. The family spec ../spec.md gives what all operator applications share.

Why this priority: the component exists to deploy this application.

Independent Test: test-mastodon-service (core/components/operator/mastodon/test.nix). It creates an account, logs in with toot, posts text and an image, reads the image from garage, and backs up the database.

Acceptance Scenarios:

  1. Given mastodon.enable = true, When the component evaluates, Then it requests mastodon-oidc, mastodon-smtp, mastodon-cache, mastodon-db, spiffe, tls, web, mastodon-bucket, vars-backend and codez.
  2. Given mastodon.enable = false, When the component evaluates, Then it requests no resources.
  3. Given no subdomain value, When the domain is derived, Then the application serves at mastodon.<deployment domain>.

User Story 2 - Log in with the group's single sign-on (Priority: P1)

A user of the group logs in to Mastodon with the account from the group directory. Authelia is the OIDC provider. Mastodon reads its OIDC settings from OIDC_* environment variables.

Why this priority: SSO is the only path from the group directory to the application.

Independent Test: nix-unit-components-mastodon-oidc and the "OIDC issuer + client secret" subtest of test-mastodon-service.

Acceptance Scenarios:

  1. Given an authelia provider in the deployment, When the mastodon-oidc request resolves, Then the client mastodon has the redirect URI https://<domain>/auth/auth/openid_connect/callback and the scopes openid, profile and email.
  2. Given the client secret credential, When the login units start, Then they read OIDC_CLIENT_SECRET from a root-owned env file under /run, not from extraConfig.
  3. Given no authelia provider, When the component evaluates, Then the mastodon-oidc request stays inert.

User Story 3 - Configure the instance policy (Priority: P2)

An operator sets instance policy through mastodon.settings and mastodon.mediaAutoRemove. The form shows only the keys that the component selects from the upstream configuration schema.

Why this priority: operators run their instance by their own policy, but the instance works with the defaults.

Independent Test: nix-unit-lib-option-selection and nix-unit-lib-json-schema cover the narrowing helpers. No check evaluates the mastodon selection itself.

Acceptance Scenarios:

  1. Given a selected key such as SINGLE_USER_MODE, When the operator sets it, Then the value reaches the Mastodon environment as a string.
  2. Given a key that the selection leaves out, such as LOCAL_DOMAIN, When the operator sets it, Then the schema refuses the value.
  3. Given an operator value for a services.mastodon option that codez also sets, When the node evaluates, Then the evaluation fails and names the option.

User Story 4 - Back up the database off the node (Priority: P3)

The component backs up the Mastodon database to the mastodon-backups bucket with restic. The cache is not backed up here, because the valkey node owns its own backup.

Why this priority: recovery needs the database, but the instance runs without a backup.

Independent Test: the "Backing up {db} works" subtest of test-mastodon-service.

Acceptance Scenarios:

  1. Given a routed postgresql provider, When the backup runs, Then pg_dump connects to the remote database with the rendered password.
  2. Given the garage request, When the backup is set up, Then the restic repository is s3:<garage api url>/<physical name of mastodon-backups>.

Edge Cases

  • mastodon-init-dirs fails on a credential that the agent has not rendered yet. The services have Wants= and After= on it, not Requires=, so systemd keeps their start jobs, and upstream Restart = "always" retries them.
  • mastodon-init-dirs retries itself (Restart = "on-failure", RestartSec = "30s", no start limit), because a failed upstream oneshot otherwise stays failed.
  • The database password file is not rendered yet. An ExecStartPre gate waits until the file carries a password.
  • The default hiredis driver reads no CA from the environment. The component sets REDIS_DRIVER = "ruby", so the TLS-only valkey connection verifies against the private CA.
  • A container recreate keeps the same Rails and VAPID keys, because the mastodon-keys files live in OpenBao KV.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare name = "Mastodon", package = pkgs.mastodon, the icon logo-symbol-icon.svg from pkgs.mastodon.src, and optional = true.
  • FR-002: The component MUST offer enable, subdomain (default mastodon) and the hidden domain from sharedOptions.nix, plus mediaAutoRemove.enable and mediaAutoRemove.olderThanDays of services.mastodon.
  • FR-003: The component MUST type settings with the pinned mastodon-config-schema, narrowed to the selection in operatorSettings, and MUST write each value to the environment as a string.
  • FR-004: The component MUST request an oidc client mastodon with the redirect URI https://<domain>/auth/auth/openid_connect/callback and the scopes openid, profile, email.
  • FR-005: The component MUST keep OIDC_CLIENT_SECRET out of the Nix store and out of the unit environment, and give it only to the units that serve logins.
  • FR-006: The component MUST request smtp with the sender noreply@<domain> and set SMTP_LOGIN to the account user name.
  • FR-007: The component MUST request redis with namespace = "mastodon" and render REDIS_URL at run time, with the rediss:// scheme when the tls field of the contract result is true and redis:// otherwise, to /run/mastodon-redis/url.env.
  • FR-008: The component MUST request postgresql with database, consumer and user mastodon, and the password file owner mastodon.
  • FR-009: The component MUST request garage through mkGarageRequest with the website bucket mastodon, and set S3_BUCKET to the physical bucket name.
  • FR-010: The component MUST request generateFiles instance mastodon-keys with secretKeyBase, three activeRecordEncryption keys, vapidPrivateKey and vapidPublicKey, and check the VAPID key lengths.
  • FR-011: The component MUST request spiffe, ssl for domain, and firewall public TCP ports 80 and 443.
  • FR-012: The codez nixos-module request MUST enable services.mastodon and merge the operator's services.mastodon values at normal priority.
  • FR-013: The component MUST request a streamingBackup of the postgres database to the mastodon-backups bucket with the restic provider.
  • FR-014: The redis and postgresql requests MUST contain only data. The codez module MUST render their clients (cache.nix, database.nix) from contracts.redis.results.mastodon.default and contracts.postgresql.results.mastodon.default, and MUST render nothing when the result is absent.

Key Entities

  • mastodon-keys: the generateFiles instance with the Rails and VAPID secrets. The owner is mastodon.
  • /run/openbao-secrets/mastodon-web.env: the S3 key env file. mastodon-web, sidekiq and streaming read it.
  • operatorSettings: the list of environment variables that the operator can set, for example AUTHORIZED_FETCH, DEEPL_*, SINGLE_USER_MODE.
  • operator-facing-options.csv: the audit table that gives the audience of each option, and marks sensitive ones.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: test-mastodon-service passes. A user posts an image, the image comes from garage, and the CSP allows it.
  • SC-002: nix-unit-components-mastodon-oidc, nix-unit-setups-mastodon-trust, nix-unit-setups-operator-config, nix-unit-setups-postgresql-render-gate and nix-unit-setups-valkey-gate pass.
  • SC-003: A recreated container gets the same mastodon-keys values, because they come from OpenBao KV. No check covers this.

Assumptions

  • The operator group has the shared valkey, smtp, lldap and authelia components. See ../spec.md.
  • The upstream schema comes from a branch pin of upstream pull request mastodon#38474. A rename of a selected key fails the evaluation.
  • The test runs all services on one server node with a dev-mode OpenBao (core/components/operator/testing/lib.nix).

Known Deviations

  • An operator secret in settings, such as DEEPL_API_KEY, lands in the Nix store and in the stored deployment settings (#2133).
  • mkGarageRequest gives each website bucket a CORS rule for GET from *. A TODO says this is too broad.