@@ 294,6 294,87 @@ consume this: `spec_search`, `spec_read`, `spec_propose`, `spec_comment`,
Genuinely net-new: proposals, prose diff, review UI, agent tokens, frontmatter
lifecycle, projects/aggregation.
+## SourceHut integration
+
+**Recipe B** from the `sourcehut-custom-service` model: pure Go, API side on
+`core-go`, web chrome reimplemented (`core-go` has no HTML templating — the
+Jinja chrome lives only in Python `core.sr.ht`). compare.sr.ht already did
+exactly this, so its nav template, cookie→identity handler, and SCSS entry are
+the thing to copy rather than rederive.
+
+Integration is entirely config + nginx + DNS. **No upstream source is modified.**
+
+### Our section
+
+The section name **must** be the literal `spec.sr.ht` — the `.sr.ht` suffix is
+what puts us in the nav `network` list (`core.sr.ht/srht/app/flask.py::_network`)
+and what other services look us up by.
+
+```ini
+[spec.sr.ht]
+origin=https://spec.srht.bigb.es
+connection-string=postgresql://specsrht@localhost/spec.sr.ht?sslmode=disable
+; Bare repos, service-owned: <repos>/~<user>/<space>
+repos=/var/lib/spec
+; Materialized checkouts + bleve project indexes. Pure cache; safe to delete.
+cache=/var/cache/spec
+static-dir=/usr/share/sourcehut/spec.sr.ht/static
+;bind-address=127.0.0.1:5091
+migrate-on-upgrade=yes
+```
+
+Canonical key names only — `origin`, `connection-string`, `migrate-on-upgrade`
+are read by the shared accessors (`config.GetOrigin`, `server.WithDefaultMiddleware`).
+`repos` mirrors dolt.sr.ht/git.sr.ht; `static-dir` and `bind-address` are the
+local house convention already used by both siblings. `cache` is ours.
+
+### Shared keys we read in place (never duplicate)
+
+| Key | Used for |
+|---|---|
+| `[sr.ht] network-key` | Fernet-decrypt `sr.ht.unified-login.v1`; mint Internal auth tokens |
+| `[sr.ht] owner-name` / `owner-email` | **`config.GetOwner` panics if missing**; also the committer identity on merges |
+| `[sr.ht] site-name` / `environment` | nav brand; non-`production` shows the dev banner |
+| `[sr.ht] internal-ipnet` | this host must fall inside it or internal GraphQL calls are rejected |
+| `[webhooks] private-key` | **`crypto.InitCrypto` fatally requires it even though v1 emits no webhooks** |
+| `[meta.sr.ht] origin` | login/logout redirects, profile fetch, PAT validation |
+| `[git.sr.ht] repos` / `api-origin` | only for read-only mounts of `docs/` dirs in existing git.sr.ht repos |
+
+### Wiring checklist
+
+1. **Config** — the `[spec.sr.ht] origin=` line must be visible to **every other
+ service's** config, not just ours; each service builds its own nav
+ independently. One shared `/etc/sr.ht/config.ini` makes this automatic —
+ then **restart the other services** so they pick up the switcher entry.
+2. **DNS** — `spec.srht.bigb.es` must be under the shared cookie domain
+ (`*.srht.bigb.es`), or the unified-login cookie is never sent to us and every
+ viewer looks anonymous.
+3. **nginx** — plain `proxy_pass http://127.0.0.1:5091`, modeled on
+ `contrib/compare.sr.ht.conf`. Note `client_max_body_size` needs raising if
+ attachments are allowed.
+4. **internal-ipnet** — same prerequisite as both siblings.
+5. **Migrations** — `specsrht-migrate`, a brant wrapper, copied from
+ `doltsrht-migrate`.
+
+### GraphQL federation: deliberately not in v1
+
+Bots use REST + MCP; a gqlgen schema buys nothing for the loop this service
+exists to serve. dolt.sr.ht made the same call.
+
+**Verified that opting out is safe.** `api.sr.ht` federates *every* config
+section ending in `.sr.ht` — it does not consult an allow-list, so our section
+does get added as a federated service pointed at `origin + "/query"`, which we
+will not serve. `updateSchema` (`api.sr.ht/main.go`) handles this explicitly:
+services that are offline or have an invalid schema are logged and **skipped**,
+and `BuildSchema` runs over only the healthy ones. So the effect is one
+`Unable to update service "spec.sr.ht"` log line, not a broken gateway.
+
+It is also not a hot loop: schema refresh is **SIGHUP-driven, not on a ticker**
+(the goroutine selects on `signalChan`), so the failing fetch happens at startup
+and on explicit reload only. dolt.sr.ht and compare.sr.ht already run with this
+exact property today. Should we ever want federation, adding `api-origin=` and
+serving `/query` is the whole change.
+
## Phases
**Phase 0 — de-risk gate.** Two spikes, both must pass before anything else is
@@ 331,6 412,10 @@ notifications, vector search over the project index.
- **Project URL namespace.** `~user/+project` distinguishes projects from spaces
(`~user/space`) in one character; alternatives are `/projects/~user/name` or
reusing hub.sr.ht's own namespace.
-- **Port.** compare.sr.ht is on 5090, dolt.sr.ht on 5306–5308. 5091 is free.
+- **Port.** compare.sr.ht is on 5090, dolt.sr.ht on 5306–5308. 5091 is free and
+ is what the config block above assumes.
+- **MCP transport.** Streamable HTTP on the same chi router (`/mcp`) keeps it to
+ one listener and one nginx block; a second port is only needed if MCP ends up
+ wanting different timeouts than the web UI.
- **LICENSE.** Unchosen, same as compare.sr.ht. SourceHut's own services are
AGPL/GPL.