Skip to content

ADR-002: Normalize proxy HTTP routes under /api/v1

FieldValue
IDADR-002
StatusACCEPTED
DecidersMaintainers
Date2026-04
Related Issueforgejo-proxy-docs-site#5 — docs: publish ADR and guides for /api/v1 normalization

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:

  1. Client confusion — callers had to remember which features lived under /api/v1, which lived under /api/v1/proxy, and which stayed at the root.
  2. CLI ambiguitythfg api could only be documented cleanly if there was one predictable API base path.
  3. 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.


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 api as 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 as thfg without 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:

  • /health is 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

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.

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.

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.


  • 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 hatchthfg api and thfg activitypub share one client-facing /api/v1 model 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.
  • 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.
  • Direct Forgejo requests still exist as a debugging comparison tool.
  • Root-path handling still exists, but only for health and ActivityPub resources.

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.


Callers should:

  1. replace /api/v1/proxy/... with normalized /api/v1/... routes
  2. treat thfg api paths as relative to /api/v1
  3. move ActivityPub API access to thfg activitypub or canonical /api/v1/activitypub/... aliases, and use raw rooted curl only when the published federation URL itself matters
  4. validate updated scripts against the published proxy OpenAPI spec

See Migrate to the normalized /api/v1 API surface for the operator-facing migration guide.

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:

  1. a concrete migration plan for published actor and collection URLs
  2. compatibility testing against external federation consumers
  3. an explicit decision on redirects, aliases, and how long they would remain in place
  4. updated CLI and operator guidance for the new federation URL model