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:
- Given
mastodon.enable = true, When the component evaluates, Then it requestsmastodon-oidc,mastodon-smtp,mastodon-cache,mastodon-db,spiffe,tls,web,mastodon-bucket,vars-backendandcodez. - Given
mastodon.enable = false, When the component evaluates, Then it requests no resources. - Given no
subdomainvalue, When the domain is derived, Then the application serves atmastodon.<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:
- Given an authelia provider in the deployment, When the
mastodon-oidcrequest resolves, Then the clientmastodonhas the redirect URIhttps://<domain>/auth/auth/openid_connect/callbackand the scopesopenid,profileandemail. - Given the client secret credential, When the login units start, Then they read
OIDC_CLIENT_SECRETfrom a root-owned env file under/run, not fromextraConfig. - Given no authelia provider, When the component evaluates, Then the
mastodon-oidcrequest 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:
- Given a selected key such as
SINGLE_USER_MODE, When the operator sets it, Then the value reaches the Mastodon environment as a string. - Given a key that the selection leaves out, such as
LOCAL_DOMAIN, When the operator sets it, Then the schema refuses the value. - Given an operator value for a
services.mastodonoption thatcodezalso 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:
- Given a routed
postgresqlprovider, When the backup runs, Thenpg_dumpconnects to the remote database with the rendered password. - 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-dirsfails on a credential that the agent has not rendered yet. The services haveWants=andAfter=on it, notRequires=, so systemd keeps their start jobs, and upstreamRestart = "always"retries them.mastodon-init-dirsretries 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
ExecStartPregate waits until the file carries a password. - The default
hiredisdriver reads no CA from the environment. The component setsREDIS_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-keysfiles live in OpenBao KV.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
name = "Mastodon",package = pkgs.mastodon, the iconlogo-symbol-icon.svgfrompkgs.mastodon.src, andoptional = true. - FR-002: The component MUST offer
enable,subdomain(defaultmastodon) and the hiddendomainfromsharedOptions.nix, plusmediaAutoRemove.enableandmediaAutoRemove.olderThanDaysofservices.mastodon. - FR-003: The component MUST type
settingswith the pinnedmastodon-config-schema, narrowed to the selection inoperatorSettings, and MUST write each value to the environment as a string. - FR-004: The component MUST request an
oidcclientmastodonwith the redirect URIhttps://<domain>/auth/auth/openid_connect/callbackand the scopesopenid,profile,email. - FR-005: The component MUST keep
OIDC_CLIENT_SECRETout 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
smtpwith the sendernoreply@<domain>and setSMTP_LOGINto the account user name. - FR-007: The component MUST request
rediswithnamespace = "mastodon"and renderREDIS_URLat run time, with therediss://scheme when thetlsfield of the contract result is true andredis://otherwise, to/run/mastodon-redis/url.env. - FR-008: The component MUST request
postgresqlwith database, consumer and usermastodon, and the password file ownermastodon. - FR-009: The component MUST request
garagethroughmkGarageRequestwith the website bucketmastodon, and setS3_BUCKETto the physical bucket name. - FR-010: The component MUST request
generateFilesinstancemastodon-keyswithsecretKeyBase, threeactiveRecordEncryptionkeys,vapidPrivateKeyandvapidPublicKey, and check the VAPID key lengths. - FR-011: The component MUST request
spiffe,sslfordomain, andfirewallpublic TCP ports80and443. - FR-012: The
codeznixos-modulerequest MUST enableservices.mastodonand merge the operator'sservices.mastodonvalues at normal priority. - FR-013: The component MUST request a
streamingBackupof thepostgresdatabase to themastodon-backupsbucket with the restic provider. - FR-014: The
redisandpostgresqlrequests MUST contain only data. Thecodezmodule MUST render their clients (cache.nix,database.nix) fromcontracts.redis.results.mastodon.defaultandcontracts.postgresql.results.mastodon.default, and MUST render nothing when the result is absent.
Key Entities
mastodon-keys: thegenerateFilesinstance with the Rails and VAPID secrets. The owner ismastodon./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 exampleAUTHORIZED_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-servicepasses. 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-gateandnix-unit-setups-valkey-gatepass. - SC-003: A recreated container gets the same
mastodon-keysvalues, because they come from OpenBao KV. No check covers this.
Assumptions
- The
operatorgroup has the sharedvalkey,smtp,lldapandautheliacomponents. 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
servernode with a dev-mode OpenBao (core/components/operator/testing/lib.nix).
Known Deviations
- An operator secret in
settings, such asDEEPL_API_KEY, lands in the Nix store and in the stored deployment settings (#2133). mkGarageRequestgives each website bucket a CORS rule forGETfrom*. A TODO says this is too broad.