ADR-002: Normalize proxy HTTP routes under /api/v1
| Field | Value |
|---|---|
| ID | ADR-002 |
| Status | ACCEPTED |
| Deciders | Maintainers |
| Date | 2026-04 |
| Related Issue | forgejo-proxy-docs-site#5 — docs: publish ADR and guides for /api/v1 normalization |
Context
Section titled “Context”The proxy had accumulated multiple URL shapes over time:
- canonical-looking API routes under
/api/v1/... - legacy proxy-prefixed routes such as
/api/v1/proxy/... - root-path custom routes that behaved like normal API endpoints
That mixed contract caused three recurring problems:
- Client confusion — callers had to remember which features lived under
/api/v1, which lived under/api/v1/proxy, and which stayed at the root. - CLI ambiguity —
thfg apicould only be documented cleanly if there was one predictable API base path. - Spec drift — OpenAPI, runtime behaviour, examples, and automation scripts were harder to keep aligned when similar resources used different path families.
The project also wanted to describe Forgejo Proxy as the canonical HTTP surface for this ecosystem, instead of documenting a permanent split between direct Forgejo calls and proxy-specific route islands.
Decision
Section titled “Decision”Normalize proxy HTTP routes so that API families live under /api/v1/....
Keep root-path exceptions intentionally narrow:
GET /health/activitypub/**
The practical rules are:
- document proxy API resources under
/api/v1/... - remove
/api/v1/proxy/...from the public contract - treat
thfg apias a client for paths relative to/api/v1 - keep rooted
/activitypub/**as the published federation surface - allow hidden
/api/v1/activitypub/...aliases for API clients such asthfgwithout publishing them as duplicate OpenAPI resources
Root exceptions are deliberate, not leftovers
Section titled “Root exceptions are deliberate, not leftovers”The rooted paths are not historical accidents that we kept for convenience. They represent two different non-REST concerns:
/healthis an operational probe for load balancers, uptime checks, and deployment diagnostics/activitypub/**is a federation-facing document space whose URLs are part of the public identity of actors, inboxes, outboxes, followers, and related resources
Why this option
Section titled “Why this option”1. One mental model for operators and automation
Section titled “1. One mental model for operators and automation”Callers can assume that normal API resources belong under /api/v1. That makes examples, shell scripts, test fixtures, and runbooks easier to write and review.
2. Cleaner CLI semantics
Section titled “2. Cleaner CLI semantics”thfg api now has a simple contract: accept a path starting with /, join it onto the active /api/v1 base URL, and reserve root-path resources for dedicated tooling.
3. Better documentation and OpenAPI alignment
Section titled “3. Better documentation and OpenAPI alignment”A normalized route tree is easier to represent in the merged spec, easier to validate, and easier to explain without long exception tables.
4. Narrower surface for special cases
Section titled “4. Narrower surface for special cases”Keeping only /health and /activitypub/** at the root preserves the few cases that genuinely benefit from that shape without letting the root namespace grow into a second API tree.
5. ActivityPub behaves like public web identity, not versioned REST
Section titled “5. ActivityPub behaves like public web identity, not versioned REST”ActivityPub resources are not consumed like a token-authenticated CRUD API where clients first learn a base path and then append versioned resource names. Federation software dereferences URLs that appear inside ActivityPub documents and expects those URLs to remain stable over time.
That matters for the proxy because:
- actor IDs, inbox URLs, outbox URLs, followers URLs, and following URLs become part of the federated contract once published
/api/v1/...is an API versioning namespace, which is a poor fit for long-lived ActivityPub identifiers- ActivityPub is frequently public or signature-based rather than bearer-token API traffic, so it does not share the same operational expectations as the Forgejo REST API
- interoperating implementations expect dereferenceable web resources, not a second versioned API tree dedicated to federation
Keeping ActivityPub rooted avoids baking API-versioning concerns into federated object identifiers.
6. /health stays rooted for operations, not for client convenience
Section titled “6. /health stays rooted for operations, not for client convenience”GET /health is intentionally different from the rest of the proxy because it is used by infrastructure components and operators who need one stable, unversioned readiness/liveness probe.
Putting health under /api/v1 would add API semantics to an operational endpoint without improving client ergonomics. Rooting it keeps deployment checks, Traefik wiring, uptime probes, and human debugging simple.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Predictable API layout — most HTTP examples now share one base shape.
- Lower documentation overhead — guides and reference pages can describe one canonical contract.
- Clearer CLI escape hatch —
thfg apiandthfg activitypubshare one client-facing/api/v1model without forcing the public federation URLs to change. - Easier validation — route policies in runtime and spec tooling can enforce the same rule.
- Stable federation URLs — ActivityPub identifiers do not inherit REST API versioning concerns.
Negative / trade-offs
Section titled “Negative / trade-offs”- Breaking change for older callers — scripts that used
/api/v1/proxy/...or other legacy root paths must be updated. - Project card writes are more explicit — normalized project APIs are column-scoped, so some raw callers must resolve a column ID where older helper routes implied one.
- Short-term migration cost — examples, tests, and operator runbooks need coordinated updates.
- Two rooted exceptions remain — the contract is cleaner, but no longer perfectly uniform because operational health and federation concerns are intentionally treated differently from normal REST resources.
- ActivityPub tooling needs dedicated guidance — the rooted public URLs still matter for federation, while API clients may use canonical aliases, so the docs must explain both views cleanly.
Neutral
Section titled “Neutral”- Direct Forgejo requests still exist as a debugging comparison tool.
- Root-path handling still exists, but only for health and ActivityPub resources.
Alternatives considered
Section titled “Alternatives considered”Option A: Keep the mixed route families
Section titled “Option A: Keep the mixed route families”Rejected. It preserves backward compatibility in the short term, but continues the ambiguity that made the contract hard to teach, hard to test, and hard to document.
Option B: Move everything under /api/v1, including health and ActivityPub
Section titled “Option B: Move everything under /api/v1, including health and ActivityPub”Rejected. GET /health is a conventional operational endpoint, and ActivityPub resources benefit from staying rooted in the federated URL space that consumers already expect. Moving ActivityPub under /api/v1 would mix versioned REST concerns into public federation identifiers and would require a careful migration of actor/object URLs, compatibility testing with federated consumers, and probably a long redirect/alias period.
Option C: Normalize API families under /api/v1 and keep only narrow root exceptions
Section titled “Option C: Normalize API families under /api/v1 and keep only narrow root exceptions”Accepted. This gives the project one canonical API tree without over-normalizing the few routes that have strong reasons to remain at the root.
Migration guidance
Section titled “Migration guidance”Callers should:
- replace
/api/v1/proxy/...with normalized/api/v1/...routes - treat
thfg apipaths as relative to/api/v1 - move ActivityPub API access to
thfg activitypubor canonical/api/v1/activitypub/...aliases, and use raw rootedcurlonly when the published federation URL itself matters - validate updated scripts against the published proxy OpenAPI spec
See Migrate to the normalized /api/v1 API surface for the operator-facing migration guide.
Revisit conditions
Section titled “Revisit conditions”Reopen this decision only if the project is willing to treat ActivityPub as a newly versioned public contract rather than a rooted federation surface. That would require:
- a concrete migration plan for published actor and collection URLs
- compatibility testing against external federation consumers
- an explicit decision on redirects, aliases, and how long they would remain in place
- updated CLI and operator guidance for the new federation URL model