Gate app/api/public/* behind isCloud() — every anonymous route returns 404 when MOTIR_CLOUD is unset
When MOTIR_CLOUD is unset, every anonymous route under app/api/public/ returns 404 instead of serving. This is the CAPABILITY gate: a self-hosted build offers no public read surface at all — not a hidden page, an absent capability.
The unavailable answer is 404 (a JSON { error: 'Not found' }), decided and recorded here: a 404 leaks no existence (unlike a 403, which admits "this exists but you may not see it"), and it is the convention for a feature an instance does not ship. It is the same shape on every gated route, via one shared guard, so the answer is consistent by construction.
Approach: a shared route-layer guard (a small helper the routes import, or a shared wrapper) that each app/api/public/* route calls first — returning NextResponse.json({ error: 'Not found' }, { status: 404 }) when !isCloud(). This is a route-layer concern only (routes are HTTP-only per the 4-layer convention); no service/repository change. The guard reads the isCloud() predicate, not isCloudBilling() and not a raw process.env read. When MOTIR_CLOUD=true the routes behave exactly as they do today. Ships its own per-route test: MOTIR_CLOUD unset → 404 on each of the 11 routes; set → the existing public-API contract tests still pass.
Acceptance criteria
- With
MOTIR_CLOUDunset, every route underapp/api/public/returns 404 (the one shared JSON shape); none returns 200, none 500s, none throws a stack trace. - With
MOTIR_CLOUD=true, every route behaves byte-identically to today (the existing public-API contract tests still pass). - The guard reads
isCloud()only (single-reader rule) — asserted by the single-reader test from the predicate card. - The 404 shape is shared by ONE helper, so every gated route answers identically; the card records WHY 404 (leaks no existence), not 403/redirect.
- ≥90% coverage on the touched files.
Context refs
app/api/public/— the 11 anonymous routes:categories,explore,p/[identifier]/{route,changelog,follow,items,roadmap,subscribe,tree},projects/[projectId]/requests/{route,duplicates}lib/billing/availability.ts—isCloud()lib/ai/planningConfig.ts— the flag pattern this must not be confused with