AGENTS.md — apps/status-page #
Public, unauthenticated, multi-tenant: the page is resolved from the request
host or the [domain] segment. Assume every response you touch is served to
anonymous visitors and cached.
Gated content #
Pages can be password-protected. Any new surface that renders page content —
route, API handler, feed, embed — must apply the same gate the HTML route
applies. The existing public representations to mirror are
apps/status-page/src/app/api/markdown/[[...path]] and
apps/status-page/src/app/api/status/[[...path]].
The gate decision lives in packages/services/src/page-access, so every
transport shares it. The statusPage tRPC procedures enforce it themselves:
get/getLight return chrome only to a denied caller — login, layout and OG
still render from them — and the detail procedures throw. A new procedure that
returns page content must call assertPageAccess from
packages/api/src/lib/page-access.ts. guardTRPCSource filters on a spoofable
header and is not a security boundary.
Never log or report tRPC input — it carries page passwords and subscriber
tokens. sentryLoggerLink attaches the operation path only.
Caching #
Markdown and JSON representations negotiate on Accept at the same URL, so
every response sets Vary: Accept plus a strong ETag. Cache-control depends on
the page's access type — a gated page must not inherit a public TTL.
Theming #
Themes come from @openstatus/theme-store as OKLCH CSS variables. Add or edit
a theme in that package; do not hard-code colours in a component.
--radius is themed too, and this app overrides it to 0rem. The scale in
packages/ui/src/globals.css is proportional to it (xs 0.2, rounded 0.4,
sm 0.6, md 0.8, lg 1.0, xl 1.4), so every step collapses with the theme
— use the tokens, never a hard-coded rounded-[4px], and prefer rounded-lg
over the equivalent rounded-(--radius). A pill that stays a pill under a
square theme is the bug. rounded-full is only for a chip that wraps a glyph
(StatusIcon); a coloured status marker takes rounded-lg, matching the bar it
describes. The .rounded-full override in globals.css exists only so
Tailwind's 9999px doesn't defeat --radius: 0 — never add more of those, fix
the component (see packages/ui/AGENTS.md).
Impact labels #
Status-page impact labels are coloured text only — no dots, no chevrons. The hover affordance is a dashed muted underline, never one tinted with the impact colour.
Theme explorer #
/ renders the theme explorer, and it is also where any request resolving to
no page row ends up. The proxy rewrites those to a 404
(resolveUnresolvedHostAction) so an unknown slug or a custom domain missing
from the DB never answers with the explorer or its OG image. Only
themes.openstatus.dev is indexable — see lib/theme-explorer-host.ts.
This is NOT the Next.js you know #
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.