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:

  1. Given pixelfed.enable = true, When the component evaluates, Then it requests pixelfed-oidc, pixelfed-smtp, pixelfed-cache, pixelfed-db, spiffe, tls, web, vars-backend, pixelfed-bucket and codez.
  2. Given pixelfed.enable = false, When the component evaluates, Then it requests no resources.
  3. 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:

  1. Given an authelia provider, When the pixelfed-oidc request resolves, Then the client pixelfed has the redirect URI https://<domain>/auth/oidc/callback and the scopes openid, profile, email.
  2. Given the OIDC request, When the secrets file renders, Then it carries PF_OIDC_CLIENT_SECRET from OpenBao KV, and the Nix store does not.
  3. Given the group-named directory administrator, When it logs in, Then Pixelfed accepts the user name, because the setups do not use admin or administrator.

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:

  1. Given an operator maxUploadSize, When the node evaluates, Then the value reaches services.pixelfed.maxUploadSize.
  2. Given a name that matches a prefix entry such as PF_BOUNCER_*, When the operator sets it, Then the schema accepts it.
  3. Given a string with a space, # or $, When the component writes .env, Then the value is in double quotes with \, ", $ and newline escaped.
  4. Given OPEN_REGISTRATION = false from 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:

  1. Given the garage request, When Pixelfed stores an image, Then FILESYSTEM_CLOUD = "s3" and AWS_BUCKET is the physical name of pixelfed.
  2. Given the backup runs, When restic-backups-pixelfed_postgres starts, Then it dumps pixelfed with pg_dump.

Edge Cases

  • No redis provider resolves. services.pixelfed.redis.createLocally stays at its mkDefault true and a local redis runs.
  • The cache does not answer. pixelfed-data-setup waits 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 postgresql provider resolves. services.pixelfed.database.createLocally stays at its mkDefault true, and a local PostgreSQL runs. DB_HOST is the socket directory /run/postgresql, and the pixelfed role owns its database.
  • The postgresql result asks for a verify-* sslMode. The evaluation fails, because upstream Pixelfed sets sslmode to prefer and reads no CA path.
  • php-fpm clear_env drops SSL_CERT_FILE. The component sets env[SSL_CERT_FILE] on the pixelfed pool when the node defines it.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare name = "Pixelfed", package = pkgs.pixelfed, the icon public/img/pixelfed-icon-color.svg from pkgs.pixelfed.src, and optional = true.
  • FR-002: The component MUST offer enable, subdomain (default pixelfed) and the hidden domain from sharedOptions.nix, and expose only maxUploadSize of services.pixelfed.
  • FR-003: The component MUST type settings with the pinned pixelfed-config-schema restated by environment variable, narrowed to operatorSettings, where a name that ends in * selects by prefix.
  • FR-004: The component MUST write each string value of settings to .env in double quotes with the special characters escaped.
  • FR-005: The component MUST set OPEN_REGISTRATION = true at priority 900, above the module default and below the operator value.
  • FR-006: The component MUST request an oidc client pixelfed with the redirect URI https://<domain>/auth/oidc/callback and set PF_OIDC_AUTHORIZE_URL, PF_OIDC_TOKEN_URL and PF_OIDC_PROFILE_URL from the contract result.
  • FR-007: The component MUST request smtp with the sender noreply@<domain>, MAIL_ENCRYPTION = "tls" and MAIL_FROM_NAME = "Pixelfed".
  • FR-008: The component MUST request redis with namespace = "pixelfed", REDIS_SCHEME = "tls" when the tls field of the contract result is true and "tcp" otherwise, the ACL user in REDIS_USERNAME, and gate pixelfed-data-setup on the cache.
  • FR-009: The component MUST request generateFiles instance pixelfed-app with appKey, 16 random bytes in hex, owned by pixelfed.
  • 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_SECRET and MAIL_PASSWORD into the pixelfed.secretsFile with the openbao-agent provider for the unit pixelfed-data-setup.
  • FR-011: The component MUST request garage through mkGarageRequest with the website bucket pixelfed, and proxy /public/ to the bucket web URL.
  • FR-012: The component MUST request spiffe, ssl for domain, and firewall public TCP ports 80 and 443.
  • FR-013: The codez nixos-module request MUST enable services.pixelfed, add nginx to the pixelfed group, and take the vhost certificate and key from the ssl contract result.
  • FR-014: The component MUST request a streamingBackup of the postgres database to the pixelfed-backups bucket with the restic provider.
  • FR-015: The redis request MUST contain only data. The codez module MUST render its client (cache.nix) from contracts.redis.results.pixelfed.default, and MUST render nothing when the result is absent.
  • FR-016: The component MUST request postgresql instance pixelfed-db with the database pixelfed, the consumer pixelfed and the password file owned by pixelfed with mode 0400. The codez module (database.nix) MUST render the client from contracts.postgresql.results.pixelfed.default: DB_CONNECTION = "pgsql", DB_HOST, DB_PORT, DB_DATABASE and DB_USERNAME from the result, database.createLocally = false, DB_PASSWORD from 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: the generateFiles instance with the Laravel APP_KEY.
  • pixelfed.secretsFile: the fileSecrets instance that services.pixelfed.secretFile reads, owned by pixelfed.
  • 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 example APP_NAME, OPEN_REGISTRATION, STRIPE_*.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: test-pixelfed-service passes. 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-gate and nix-unit-setups-operator-config pass.
  • SC-003: A user logs in through authelia with the group-named directory administrator.

Assumptions

  • The operator group has the shared valkey, smtp, lldap and authelia components. 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 as STRIPE_SECRET, 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.
  • garage.nix has a TODO to upstream a services.pixelfed.nginx.enable option, and node.nix has a TODO to make OPEN_REGISTRATION a general option.