Pin the schema of the default source

Freezing the default-source schema baseline (SELF_MODULE_SOURCE)

When a deployment is submitted with no module_source (the built-in default), the panel's schema-change view (#213) needs to remember which schema that deploy ran against. Without help it stores SQL NULL, which later resolves to whatever schema api/ has since been rebuilt with -- so the "old" side of the diff floats forward and the view goes blind to exactly the drift it should show.

To pin it, api/ records a fetchTree reference to its own revision at submit time, read from the SELF_MODULE_SOURCE environment variable (a JSON object). A revision is a property of the deploy, not of the source tree, so it is supplied at runtime rather than baked in. See Default-source baseline freeze for the full rationale.

With flakes (automatic)

Nothing to do. flake.nix derives the pin from the flake's locked self and threads it through to the service, so a clean checkout pins itself. A dirty working tree has no reproducible revision, so the baseline simply floats for that build -- which is fine, since an uncommitted tree is not reproducible anyway.

Without flakes (or any manual / non-Nix run)

Export SELF_MODULE_SOURCE before starting api/. Generate the pin for the revision you are running with a single command (it only needs the revision to be pushed -- fetchTree fetches it from the remote, not your working tree):

export SELF_MODULE_SOURCE="$(
  rev=$(git rev-parse HEAD)
  url=https://git.fediversity.eu/fediversity/fediversity
  nix-instantiate --eval --impure --strict --json --expr "
    let t = builtins.fetchTree { type = \"git\"; url = \"$url\"; rev = \"$rev\"; };
    in { type = \"git\"; url = \"$url\"; rev = \"$rev\"; narHash = t.narHash; }
  "
)"

# then, in the same shell (`devShells.<system>.api-http-stack` puts the
# `api-http-stack` binary on PATH; see #1094 for the non-flake shell parity):
nix-shell -A devShells.x86_64-linux.api-http-stack --run api-http-stack default.nix

builtins.fetchTree fetches the revision from the remote and exposes its narHash; the surrounding expression projects it to the {type, url, rev, narHash} shape api/ expects (--strict forces the narHash thunk so it serializes). Point url at the remote that actually hosts the revision (use your fork's clone URL if you pushed there). The import ./default.nix { selfModuleSource = { ... }; } argument accepts the same object if you would rather pass it in Nix than via the environment.

If the variable is unset, malformed, or names a revision whose schema assets cannot be fetched, the baseline falls back to floating and a default form load still works -- the pin is an enhancement, never a hard dependency.

Pinning the schema to one deployment method / effect (PIN_EFFECT)

The demo api serves an effect-agnostic form: its deployment-method schema is a discriminated union of every member -- the deployment methods (ssh-hosts, tf-hosts, tf-incus-hosts, tf-incus-operator-hosts, k8s-hosts, k8s-operator-hosts) and the non-deploy effects (tf-incus-image, octodns-zone) -- so the form shows a picker and the submission carries a tag key naming the chosen member.

A production api usually deploys via exactly one of these, so PIN_EFFECT collapses the union to that single member (issue #723). The tag names any union member -- a deployment method or a non-deploy effect -- hence "effect", not "method". With it set the form has no picker: the served schema drops deployment-method entirely and carries that member's settings at its root, so the form renders them as ordinary fields and api/ validates submissions in the same shape. Which effect the build serves is not on the wire at all; it is recorded in pinned-effect.json beside the schema, and the submit path recovers it from there.

A value stored against an earlier, unpinned build is keyed under deployment-method.<tag>, so the pin also emits a rename onto the root (deployment-method.<tag>.<name> -> <name>) into the same migrations table that carries declared option renames. Both lanes apply it: the panel's island migrates on read, and api/ migrates a submission before validating it, so a pre-pin configuration needs no hand editing.

# api/ devShell (or `./api#api-http-stack` from the repo root)
PIN_EFFECT=tf-incus-operator-hosts nix run --impure .#api-http-stack

A NixOS deployment sets the same pin declaratively, as services.fediversity-api.pinEffect, which reaches the same pinEffect build param: the environment variable is the dev-shell / direct-entrypoint lane, the module option the deployed one, so a node never needs an impure ambient build. api/nix/component/api.nix pins the hosting-provider group's api to tf-incus-operator-hosts, the one method whose coordinates that provider curates.

The pin drives the generated schema.json, which is the same file api/ validates against -- so the pin is the contract (core/lib/schema-for-source.nix). PIN_EFFECT is read once on the src side by the top-level entrypoint (default.nix), which bakes it into the build (pinEffect build param) so the running api recovers its one effect from its OWN artifact (pinned-effect.json, beside the schema in one store path) rather than the ambient environment. The api never re-reads PIN_EFFECT at run time, so the served schema and the pin it interprets submissions against cannot desync -- there is a single source of truth (issue #723 follow-up). --impure is required because the pin collapses the schema at BUILD time and Nix only reads the ambient PIN_EFFECT under --impure; the one-step nix run --impure reaches the build, so the sidecar carries the pin. A PIN_EFFECT naming no declared effect (a typo such as tf=incus-hosts for tf-incus-hosts) is rejected loudly -- at build by the schema collapse, and at submit by the resolver reading the baked value -- never silently mis-resolved. So is a pinned setting whose name a top-level option already takes, since lifting it to the root would shadow one of the two.

Unset (the default) bakes the whole union: the wire's tag key names the method.