Feature Specification: Component pixelfed
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/components/operator/pixelfed/default.nix, core/components/operator/pixelfed/oidc.nix, core/components/operator/pixelfed/database.nix, core/components/operator/pixelfed/node.nix, core/components/operator/pixelfed/test.nix, core/components/operator/pixelfed/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/setups/hosts-common.nix, core/setups/pixelfed-database-test.nix, core/checks/nix-unit-tests.nix), pull requests #1384, #1519, #1651, #1724, #1743, #1744, #1747, #1751, #1763, #1794, #1798, #1835, #1911.
User Scenarios & Testing (mandatory)
User Story 1 - Run a Pixelfed instance for a group (Priority: P1)
An operator enables pixelfed in the operator group. The component places
resource requests for single sign-on, mail, cache, database, object storage,
generated secrets, workload identity, TLS and public ports. The setups route each
request to a provider in the deployment. The node then serves Pixelfed at
https://<subdomain>.<domain>. The database is a pixelfed database for each
deployment on the central PostgreSQL. The family spec
../spec.md gives what all operator applications share.
Why this priority: the component exists to deploy this application.
Independent Test: test-pixelfed-service
(core/components/operator/pixelfed/test.nix). It creates an account,
posts an image, checks that the image comes from garage, and backs up the
database.
Acceptance Scenarios:
- Given
pixelfed.enable = true, When the component evaluates, Then it requestspixelfed-oidc,pixelfed-smtp,pixelfed-cache,pixelfed-db,spiffe,tls,web,vars-backend,pixelfed-bucketandcodez. - Given
pixelfed.enable = false, When the component evaluates, Then it requests no resources. - Given no operator value for
OPEN_REGISTRATION, When the node evaluates, Then registration is open.
User Story 2 - Log in with the group's single sign-on (Priority: P1)
A user of the group logs in to Pixelfed with the account from the group
directory. Pixelfed has generic OIDC without discovery, so the component
gives it the authorize, token and userinfo endpoints from the contract
result as PF_OIDC_* variables.
Why this priority: SSO is the only path from the group directory to the application.
Independent Test: the subtests "Pixelfed carries the contract-derived
OIDC endpoints + client secret", "pixelfed accepts the seeded directory
admin as a username" and "The seeded directory admin authenticates through
authelia (SSO first factor)" of test-pixelfed-service.
Acceptance Scenarios:
- Given an authelia provider, When the
pixelfed-oidcrequest resolves, Then the clientpixelfedhas the redirect URIhttps://<domain>/auth/oidc/callbackand the scopesopenid,profile,email. - Given the OIDC request, When the secrets file renders, Then it carries
PF_OIDC_CLIENT_SECRETfrom OpenBao KV, and the Nix store does not. - Given the group-named directory administrator, When it logs in, Then Pixelfed accepts the user name, because the setups do not use
adminoradministrator.
User Story 3 - Configure the instance policy (Priority: P2)
An operator sets instance policy through pixelfed.settings and
pixelfed.maxUploadSize. The settings type comes from Pixelfed's
configuration schema, keyed by environment variable and narrowed to the
names in operatorSettings.
Why this priority: operators run their instance by their own policy, but the instance works with the defaults.
Independent Test: the subtest "An operator-set upstream option reaches
services.pixelfed" of test-pixelfed-service, and
nix-unit-lib-option-selection, nix-unit-lib-json-schema and
nix-unit-lib-nixos-service-options for the helpers.
Acceptance Scenarios:
- Given an operator
maxUploadSize, When the node evaluates, Then the value reachesservices.pixelfed.maxUploadSize. - Given a name that matches a prefix entry such as
PF_BOUNCER_*, When the operator sets it, Then the schema accepts it. - Given a string with a space,
#or$, When the component writes.env, Then the value is in double quotes with\,",$and newline escaped. - Given
OPEN_REGISTRATION = falsefrom the operator, When the node evaluates, Then the operator value wins over the component default.
User Story 4 - Store media in garage and back up the database (Priority: P3)
Pixelfed writes media to the garage bucket pixelfed. nginx proxies
/public/ to the bucket web endpoint. The component backs up the PostgreSQL
database to the pixelfed-backups bucket with restic.
Why this priority: media in object storage keeps the node small, and recovery needs the database.
Independent Test: the "access image in garage", "Check that image
comes from garage" and "Backing up {db} works" subtests of
test-pixelfed-service.
Acceptance Scenarios:
- Given the garage request, When Pixelfed stores an image, Then
FILESYSTEM_CLOUD = "s3"andAWS_BUCKETis the physical name ofpixelfed. - Given the backup runs, When
restic-backups-pixelfed_postgresstarts, Then it dumpspixelfedwithpg_dump.
Edge Cases
- No
redisprovider resolves.services.pixelfed.redis.createLocallystays at itsmkDefault trueand a local redis runs. - The cache does not answer.
pixelfed-data-setupwaits for a verified TLS connection, because a failed job dispatch fails the migration run. PostgreSQL rolls back the failed migration and a later run applies it again. - No
postgresqlprovider resolves.services.pixelfed.database.createLocallystays at itsmkDefault true, and a local PostgreSQL runs.DB_HOSTis the socket directory/run/postgresql, and thepixelfedrole owns its database. - The
postgresqlresult asks for averify-*sslMode. The evaluation fails, because upstream Pixelfed setssslmodetopreferand reads no CA path. - php-fpm
clear_envdropsSSL_CERT_FILE. The component setsenv[SSL_CERT_FILE]on thepixelfedpool when the node defines it.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
name = "Pixelfed",package = pkgs.pixelfed, the iconpublic/img/pixelfed-icon-color.svgfrompkgs.pixelfed.src, andoptional = true. - FR-002: The component MUST offer
enable,subdomain(defaultpixelfed) and the hiddendomainfromsharedOptions.nix, and expose onlymaxUploadSizeofservices.pixelfed. - FR-003: The component MUST type
settingswith the pinnedpixelfed-config-schemarestated by environment variable, narrowed tooperatorSettings, where a name that ends in*selects by prefix. - FR-004: The component MUST write each string value of
settingsto.envin double quotes with the special characters escaped. - FR-005: The component MUST set
OPEN_REGISTRATION = trueat priority900, above the module default and below the operator value. - FR-006: The component MUST request an
oidcclientpixelfedwith the redirect URIhttps://<domain>/auth/oidc/callbackand setPF_OIDC_AUTHORIZE_URL,PF_OIDC_TOKEN_URLandPF_OIDC_PROFILE_URLfrom the contract result. - FR-007: The component MUST request
smtpwith the sendernoreply@<domain>,MAIL_ENCRYPTION = "tls"andMAIL_FROM_NAME = "Pixelfed". - FR-008: The component MUST request
rediswithnamespace = "pixelfed",REDIS_SCHEME = "tls"when thetlsfield of the contract result is true and"tcp"otherwise, the ACL user inREDIS_USERNAME, and gatepixelfed-data-setupon the cache. - FR-009: The component MUST request
generateFilesinstancepixelfed-appwithappKey, 16 random bytes in hex, owned bypixelfed. - FR-010: The component MUST render
APP_KEY,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,REDIS_PASSWORD, and, when the matching want exists,DB_PASSWORD,PF_OIDC_CLIENT_SECRETandMAIL_PASSWORDinto thepixelfed.secretsFilewith theopenbao-agentprovider for the unitpixelfed-data-setup. - FR-011: The component MUST request
garagethroughmkGarageRequestwith the website bucketpixelfed, and proxy/public/to the bucket web URL. - FR-012: The component MUST request
spiffe,sslfordomain, andfirewallpublic TCP ports80and443. - FR-013: The
codeznixos-modulerequest MUST enableservices.pixelfed, addnginxto thepixelfedgroup, and take the vhost certificate and key from thesslcontract result. - FR-014: The component MUST request a
streamingBackupof thepostgresdatabase to thepixelfed-backupsbucket with the restic provider. - FR-015: The
redisrequest MUST contain only data. Thecodezmodule MUST render its client (cache.nix) fromcontracts.redis.results.pixelfed.default, and MUST render nothing when the result is absent. - FR-016: The component MUST request
postgresqlinstancepixelfed-dbwith the databasepixelfed, the consumerpixelfedand the password file owned bypixelfedwith mode0400. Thecodezmodule (database.nix) MUST render the client fromcontracts.postgresql.results.pixelfed.default:DB_CONNECTION = "pgsql",DB_HOST,DB_PORT,DB_DATABASEandDB_USERNAMEfrom the result,database.createLocally = false,DB_PASSWORDfrom the engine's static credentials in the secrets file, and a backup that dumps the remote database. When the result is absent, it MUST use a local PostgreSQL. MariaDB MUST NOT run on the node.
Key Entities
pixelfed-app: thegenerateFilesinstance with the LaravelAPP_KEY.pixelfed.secretsFile: thefileSecretsinstance thatservices.pixelfed.secretFilereads, owned bypixelfed.pixelfed-data-setup: the unit that runs migrations. It reads the secrets file and waits for the cache.operatorSettings: the list of environment variables that the operator can set, for exampleAPP_NAME,OPEN_REGISTRATION,STRIPE_*.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
test-pixelfed-servicepasses. A posted image comes from garage, and the backup works. - SC-002:
nix-unit-setups-pixelfed-database,nix-unit-setups-pixelfed-trust,nix-unit-setups-valkey-gateandnix-unit-setups-operator-configpass. - SC-003: A user logs in through authelia with the group-named directory administrator.
Assumptions
- The
operatorgroup has the sharedvalkey,smtp,lldapandautheliacomponents. See../spec.md. - The setups name the directory administrator after the group, which keeps it off Pixelfed's restricted names (
core/setups/hosts-common.nix). - The schema comes from a branch pin of upstream pull request pixelfed#6555. A rename of a selected variable fails the evaluation.
Known Deviations
- An operator secret in
settings, such asSTRIPE_SECRET, 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.garage.nixhas a TODO to upstream aservices.pixelfed.nginx.enableoption, andnode.nixhas a TODO to makeOPEN_REGISTRATIONa general option.