From 3d06b9af3d261277a67918fe4cf3a509298e75df Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 13 Oct 2026 08:39:09 +0000
Subject: [PATCH 1/5] feat: add standalone Cap status monitoring app
---
.github/workflows/status.yml | 31 +
apps/status/.env.example | 9 +
apps/status/.gitignore | 2 +
apps/status/README.md | 59 ++
apps/status/app/api/cron/check/route.ts | 79 ++
apps/status/app/api/status/route.ts | 21 +
apps/status/app/globals.css | 986 ++++++++++++++++++++++++
apps/status/app/layout.tsx | 41 +
apps/status/app/page.tsx | 5 +
apps/status/components/icons.tsx | 68 ++
apps/status/components/service-row.tsx | 172 +++++
apps/status/components/status-page.tsx | 388 ++++++++++
apps/status/lib/probe.ts | 79 ++
apps/status/lib/services.ts | 69 ++
apps/status/lib/state.ts | 189 +++++
apps/status/lib/store.ts | 37 +
apps/status/lib/types.ts | 66 ++
apps/status/next.config.ts | 19 +
apps/status/package.json | 24 +
apps/status/public/cap-icon.svg | 7 +
apps/status/public/cloud-left.webp | Bin 0 -> 36836 bytes
apps/status/public/cloud-right.webp | Bin 0 -> 25496 bytes
apps/status/screenshots/desktop.png | Bin 0 -> 493002 bytes
apps/status/screenshots/mobile.png | Bin 0 -> 204754 bytes
apps/status/tests/probe.test.ts | 86 +++
apps/status/tests/routes.test.ts | 174 +++++
apps/status/tests/state.test.ts | 134 ++++
apps/status/tsconfig.json | 30 +
apps/status/vercel.json | 6 +
bun.lock | 24 +
30 files changed, 2805 insertions(+)
create mode 100644 .github/workflows/status.yml
create mode 100644 apps/status/.env.example
create mode 100644 apps/status/.gitignore
create mode 100644 apps/status/README.md
create mode 100644 apps/status/app/api/cron/check/route.ts
create mode 100644 apps/status/app/api/status/route.ts
create mode 100644 apps/status/app/globals.css
create mode 100644 apps/status/app/layout.tsx
create mode 100644 apps/status/app/page.tsx
create mode 100644 apps/status/components/icons.tsx
create mode 100644 apps/status/components/service-row.tsx
create mode 100644 apps/status/components/status-page.tsx
create mode 100644 apps/status/lib/probe.ts
create mode 100644 apps/status/lib/services.ts
create mode 100644 apps/status/lib/state.ts
create mode 100644 apps/status/lib/store.ts
create mode 100644 apps/status/lib/types.ts
create mode 100644 apps/status/next.config.ts
create mode 100644 apps/status/package.json
create mode 100644 apps/status/public/cap-icon.svg
create mode 100644 apps/status/public/cloud-left.webp
create mode 100644 apps/status/public/cloud-right.webp
create mode 100644 apps/status/screenshots/desktop.png
create mode 100644 apps/status/screenshots/mobile.png
create mode 100644 apps/status/tests/probe.test.ts
create mode 100644 apps/status/tests/routes.test.ts
create mode 100644 apps/status/tests/state.test.ts
create mode 100644 apps/status/tsconfig.json
create mode 100644 apps/status/vercel.json
diff --git a/.github/workflows/status.yml b/.github/workflows/status.yml
new file mode 100644
index 00000000000..a22fc67fc82
--- /dev/null
+++ b/.github/workflows/status.yml
@@ -0,0 +1,31 @@
+name: Status app
+
+on:
+ pull_request:
+ paths:
+ - "apps/status/**"
+ - "bun.lock"
+ - ".github/workflows/status.yml"
+ push:
+ branches: [main]
+ paths:
+ - "apps/status/**"
+ - "bun.lock"
+ - ".github/workflows/status.yml"
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: "1.4.0"
+ - run: bun install --frozen-lockfile
+ - run: bun run --cwd apps/status test
+ - run: bun run --cwd apps/status build
+ env:
+ NEXT_TELEMETRY_DISABLED: "1"
diff --git a/apps/status/.env.example b/apps/status/.env.example
new file mode 100644
index 00000000000..dd6d639d522
--- /dev/null
+++ b/apps/status/.env.example
@@ -0,0 +1,9 @@
+UPSTASH_REDIS_REST_URL=
+UPSTASH_REDIS_REST_TOKEN=
+CRON_SECRET=
+STATUS_WEBSITE_URL=https://cap.so
+STATUS_API_URL=https://cap.so/api/auth/providers
+STATUS_SHARING_URL=
+STATUS_STORAGE_URL=
+STATUS_MEDIA_URL=
+STATUS_RENDER_URL=
diff --git a/apps/status/.gitignore b/apps/status/.gitignore
new file mode 100644
index 00000000000..81f74ff7895
--- /dev/null
+++ b/apps/status/.gitignore
@@ -0,0 +1,2 @@
+.next/
+next-env.d.ts
diff --git a/apps/status/README.md b/apps/status/README.md
new file mode 100644
index 00000000000..c504834a68a
--- /dev/null
+++ b/apps/status/README.md
@@ -0,0 +1,59 @@
+# Cap Status
+
+An independent Next.js app for `status.cap.so`. It reuses the current landing page’s Instrument Sans, Source Serif 4, DM Mono, cloud artwork, and colour palette. It has no dependency on the main web app, its database, or its deployment.
+
+## Deploy on Vercel
+
+1. Import this repository into a **separate Vercel project**, with root directory **`apps/status`**, framework **Next.js**, and build command **`bun run build`**. Keep access to the repository root enabled so Vercel uses the existing Bun workspace and lockfile. Use Bun 1.4.0. Do not attach the main site’s environment variables.
+2. Attach a dedicated Upstash Redis database through the Vercel Marketplace. Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` on the production environment. Keep preview data in a separate database, or leave preview storage unconfigured.
+3. Set a random `CRON_SECRET` of at least 32 bytes on production. Vercel sends it as the cron’s Bearer token. Do not prefix any of these variables with `NEXT_PUBLIC_`.
+4. Configure the production service URLs below. Deploy on a plan supporting **one-minute cron jobs** (Vercel Pro or higher), then add `status.cap.so` to this project and point its DNS at Vercel. The page starts with honest “no data” states until checks run.
+5. Verify the first `/api/cron/check` invocation succeeds in the Vercel logs and `/api/status` contains current observations for every configured service. Set an **external dead-man monitor** to alert when `/api/status` has an old `checkedAt` (over 180 seconds), returns non-200, or cannot be reached. This detects a failed scheduler, Redis, or the status deployment itself.
+
+No production deployment, DNS changes, external monitors, or storage resources are created by this PR.
+
+## Service coverage
+
+All endpoints must be direct HTTPS URLs returning 2xx; redirects are intentionally rejected so a sign-in redirect or protection page cannot count as a healthy service. Use dedicated, public, read-only canaries; never use a customer’s private recording or a signed URL that expires.
+
+| Environment variable | Probe | Coverage |
+| --- | --- | --- |
+| `STATUS_WEBSITE_URL` | HEAD; defaults to `https://cap.so` | Public website reachability |
+| `STATUS_API_URL` | GET; defaults to `https://cap.so/api/auth/providers` | Authentication API reachability and a real provider payload; does not simulate sign-in or prove database health |
+| `STATUS_SHARING_URL` | HEAD; required | A permanent public test recording’s share page returning 200 |
+| `STATUS_STORAGE_URL` | HEAD; required | A small, permanent canary object on the production storage/CDN path; does not test uploads |
+| `STATUS_MEDIA_URL` | GET; required | Media server `/health`, including `status === "ok"` rather than HTTP status alone |
+| `STATUS_RENDER_URL` | GET; required | Render coordinator `/health`, requiring `ok`, active workers, and render slots |
+
+Missing or invalid configuration is shown as unknown, never operational. Add other services to `lib/services.ts` and their environment variables to `.env.example`. Each service gets its own history and automatic incidents. The `/health` URLs are existing endpoints; this app adds no instrumentation, middleware, migrations, or code to Cap’s production services.
+
+These are availability checks, not end-to-end recording/upload/playback tests. A cache-served canary confirms the serving path, not every downstream dependency. Keep functional synthetic tests and provider alerting alongside this page.
+
+## Monitoring and persistence
+
+- Vercel cron runs once per minute **only on production**. `/api/cron/check` rejects unauthenticated calls. Preview deployments cannot run the collector.
+- Six lightweight probes run concurrently, each with an eight-second deadline. Healthy responses over three seconds are degraded. An unhealthy probe is retried once immediately. At most 12 requests per minute reach Cap, independent of page traffic. HEAD probes do not download recordings; GET probes use only small health JSON.
+- A 55-second Redis lease prevents overlapping collectors. The function deadline is 50 seconds. Saving observations and releasing the lease is one atomic Lua operation conditional on lease ownership. A crashed collector’s lease expires. Duplicate invocations in the same UTC minute do not double-count.
+- A first confirmed unhealthy sample shows degraded/unknown status. Two consecutive unhealthy samples open an incident; two consecutive successful samples close it. After a monitoring gap, consecutive counters reset. An unobserved gap never resolves an incident.
+- Redis stores 90 UTC calendar days of daily observed success/degradation/failure counts and all ongoing incidents and up to 500 recently resolved incidents. Empty days remain empty. Uptime is successful observed checks divided by all observed checks; it is not a promise about unobserved time. Hover, focus, or tap a day to see the sample count.
+- Visitors read only `/api/status`, cached at the CDN for 30 seconds. The visible page refreshes once a minute and stops polling in hidden tabs. It independently marks observations older than three minutes unknown. Storage failures show monitoring unavailable; they never fabricate healthy data. No service URLs, health payloads, or credentials appear in the public snapshot.
+
+The status page and its scheduler share Vercel. The independent dead-man monitor is needed to notify operators of a Vercel-wide outage. This version records automatic incidents; notifications and manually written incident updates are outside its scope.
+
+## Local checks
+
+From the repository root, install with `bun install --frozen-lockfile`. Then:
+
+```sh
+bun run --cwd apps/status test
+bun run --cwd apps/status typecheck
+bun run --cwd apps/status build
+```
+
+Copy `.env.example` to `.env.local` in this app for local configuration. Use a separate development Redis database. `bun run --cwd apps/status start` serves the production build on port 3002; `dev` uses the same port. Respect the repository’s existing-server rules when starting a local server.
+
+## Screenshots
+
+[Desktop preview](screenshots/desktop.png) · [Mobile preview](screenshots/mobile.png)
+
+These screenshots show the production build with sample monitoring responses, not live service history. The browser checks also cover missing storage, stale observations, an active outage, incident display, service expansion, and keyboard navigation through daily uptime. Automated WCAG 2.1 A/AA checks pass for the normal and outage views.
diff --git a/apps/status/app/api/cron/check/route.ts b/apps/status/app/api/cron/check/route.ts
new file mode 100644
index 00000000000..92e5550038a
--- /dev/null
+++ b/apps/status/app/api/cron/check/route.ts
@@ -0,0 +1,79 @@
+import { randomUUID, timingSafeEqual } from "node:crypto";
+import { probe } from "../../../../lib/probe";
+import { SERVICES, serviceUrl } from "../../../../lib/services";
+import { applyResult } from "../../../../lib/state";
+import {
+ COMMIT_SCRIPT,
+ getRedis,
+ LOCK_KEY,
+ RELEASE_SCRIPT,
+ readState,
+ STATE_KEY,
+} from "../../../../lib/store";
+
+export const maxDuration = 50;
+export const dynamic = "force-dynamic";
+
+export async function GET(request: Request) {
+ const secret = process.env.CRON_SECRET;
+ const supplied = Buffer.from(request.headers.get("authorization") ?? "");
+ const expected = Buffer.from(`Bearer ${secret}`);
+ if (
+ !secret ||
+ supplied.length !== expected.length ||
+ !timingSafeEqual(supplied, expected)
+ ) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+ if (process.env.VERCEL_ENV && process.env.VERCEL_ENV !== "production") {
+ return Response.json({ skipped: "Preview deployment" });
+ }
+ try {
+ const redis = getRedis();
+ const owner = randomUUID();
+ if (!(await redis.set(LOCK_KEY, owner, { nx: true, ex: 55 })))
+ return Response.json({ skipped: "Check already running" });
+ try {
+ const state = await readState(redis);
+ const now = Date.now();
+ if (
+ state.checkedAt &&
+ Math.floor(Date.parse(state.checkedAt) / 60_000) >=
+ Math.floor(now / 60_000)
+ ) {
+ return Response.json({ skipped: "Minute already sampled" });
+ }
+ const results = await Promise.all(
+ SERVICES.map(async (service) => {
+ try {
+ const url = serviceUrl(service);
+ return {
+ id: service.id,
+ result: url ? await probe(service, url) : null,
+ };
+ } catch {
+ console.error(`Invalid monitor configuration: ${service.id}`);
+ return { id: service.id, result: null };
+ }
+ }),
+ );
+ for (const { id, result } of results) applyResult(state, id, result, now);
+ state.checkedAt = new Date(now).toISOString();
+ const committed = await redis.eval(
+ COMMIT_SCRIPT,
+ [LOCK_KEY, STATE_KEY],
+ [owner, JSON.stringify(state)],
+ );
+ if (committed !== 1) throw new Error("Monitor lease expired");
+ return Response.json({
+ checkedAt: state.checkedAt,
+ checked: results.filter((entry) => entry.result).length,
+ });
+ } finally {
+ await redis.eval(RELEASE_SCRIPT, [LOCK_KEY], [owner]);
+ }
+ } catch {
+ console.error("Status check could not be saved");
+ return Response.json({ error: "Monitoring unavailable" }, { status: 503 });
+ }
+}
diff --git a/apps/status/app/api/status/route.ts b/apps/status/app/api/status/route.ts
new file mode 100644
index 00000000000..1124daf9542
--- /dev/null
+++ b/apps/status/app/api/status/route.ts
@@ -0,0 +1,21 @@
+import { emptyState, publicSnapshot } from "../../../lib/state";
+import { getRedis, readState } from "../../../lib/store";
+
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+ try {
+ return Response.json(publicSnapshot(await readState(getRedis())), {
+ headers: {
+ "Cache-Control":
+ "public, max-age=0, s-maxage=30, stale-while-revalidate=30",
+ },
+ });
+ } catch {
+ console.error("Status snapshot unavailable");
+ return Response.json(publicSnapshot(emptyState()), {
+ status: 503,
+ headers: { "Cache-Control": "no-store" },
+ });
+ }
+}
diff --git a/apps/status/app/globals.css b/apps/status/app/globals.css
new file mode 100644
index 00000000000..42eef747f56
--- /dev/null
+++ b/apps/status/app/globals.css
@@ -0,0 +1,986 @@
+:root {
+ color-scheme: light;
+ --ink: #111111;
+ --muted: #68737f;
+ --line: #e1e7ee;
+ --green: #428d72;
+ --amber: #b77d24;
+ --red: #c35858;
+}
+
+* {
+ box-sizing: border-box;
+}
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 28px;
+}
+body {
+ margin: 0;
+ background: #ffffff;
+ color: var(--ink);
+ font-family: var(--font-sans), sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+button,
+input {
+ font: inherit;
+}
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+button {
+ color: inherit;
+}
+a {
+ color: inherit;
+ text-decoration: none;
+}
+button {
+ cursor: pointer;
+}
+button:focus-visible,
+a:focus-visible,
+summary:focus-visible {
+ outline: 2px solid #4081c8;
+ outline-offset: 5px;
+}
+h1,
+h2,
+h3,
+p {
+ margin: 0;
+}
+.skip-link {
+ position: fixed;
+ top: -60px;
+ left: 20px;
+ z-index: 20;
+ padding: 12px;
+ background: white;
+}
+.skip-link:focus {
+ top: 12px;
+}
+.site-header {
+ height: 86px;
+ max-width: 1320px;
+ margin: auto;
+ padding: 0 48px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 24px;
+ font-weight: 600;
+ letter-spacing: -1px;
+}
+.brand img {
+ width: 34px;
+ height: 34px;
+}
+.brand-divider {
+ height: 21px;
+ width: 1px;
+ background: #d8dfe5;
+ margin: 0 7px 0 12px;
+ transform: rotate(16deg);
+}
+.brand-context {
+ font-size: 16px;
+ font-weight: 400;
+ letter-spacing: -0.2px;
+ color: #67727c;
+}
+.back-link {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: #4b5865;
+}
+.back-link:hover,
+footer a:hover {
+ color: #3777bd;
+}
+.hero {
+ position: relative;
+ margin: 0 16px;
+ min-height: 357px;
+ overflow: hidden;
+ border-radius: 24px;
+ text-align: center;
+ background: #edf1f6;
+}
+.sky {
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(36% 60% at 72% 0%, #ffe0c0a6, transparent 75%),
+ linear-gradient(180deg, #c8ddf6 0%, #d9e7f8 35%, #edf1f6 100%);
+}
+.sky::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ opacity: 0.18;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Cpath fill='%23888' filter='url(%23n)' opacity='.3' d='M0 0h180v180H0z'/%3E%3C/svg%3E");
+}
+.cloud {
+ height: auto;
+ position: absolute;
+ pointer-events: none;
+ object-fit: contain;
+ opacity: 0.8;
+}
+.cloud-left {
+ width: 430px;
+ left: -130px;
+ top: 88px;
+ transform: rotate(-5deg);
+}
+.cloud-right {
+ width: 510px;
+ right: -120px;
+ top: 118px;
+}
+.hero-content {
+ position: relative;
+ padding: 57px 22px 66px;
+}
+.eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-family: var(--font-mono), monospace;
+ font-size: 10px;
+ letter-spacing: 1.15px;
+ color: #3b5268;
+}
+.eyebrow > span {
+ width: 6px;
+ height: 6px;
+ background: #7aa8d7;
+}
+h1 {
+ margin-top: 25px;
+ font-size: clamp(40px, 4.8vw, 68px);
+ font-weight: 400;
+ letter-spacing: -3.1px;
+ line-height: 1.07;
+}
+.hero-description {
+ margin-top: 17px;
+ font-family: var(--font-serif), Georgia, serif;
+ font-weight: 300;
+ font-size: 18px;
+ color: #4e5d6c;
+ letter-spacing: -0.3px;
+}
+.hero-link {
+ margin-top: 26px;
+ display: inline-flex;
+ gap: 12px;
+ font-size: 12px;
+ color: #64788e;
+}
+.hero-link span {
+ transition: transform 160ms;
+}
+.hero-link:hover span {
+ transform: translateY(3px);
+}
+.content {
+ position: relative;
+ max-width: 940px;
+ padding: 0 30px;
+ margin: -29px auto 0;
+}
+.summary {
+ border: 1px solid #dbe9e2;
+ border-radius: 16px;
+ padding: 25px 28px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 15px;
+ background: linear-gradient(110deg, #f2faf5, #fcfefd);
+ box-shadow:
+ 0 5px 20px -14px #4b6d7140,
+ 0 1px 0 #ffffff inset;
+}
+.summary-main {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+}
+.summary-icon {
+ width: 44px;
+ height: 44px;
+ flex-shrink: 0;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+ background: #e1f1e8;
+ color: #388160;
+ border: 1px solid #cae3d6;
+ box-shadow: inset 0 1px 2px #ffffff;
+}
+.summary h2 {
+ font-size: 20px;
+ font-weight: 500;
+ letter-spacing: -0.6px;
+}
+.summary p {
+ color: #61746a;
+ font-size: 13px;
+ margin-top: 5px;
+}
+.summary.unknown {
+ background: linear-gradient(110deg, #f4f7fb, #fdfefe);
+ border-color: #dce4ec;
+}
+.summary.unknown .summary-icon {
+ color: #526d88;
+ background: #e8eff7;
+ border-color: #d6e1ed;
+}
+.summary.unknown p {
+ color: #64768b;
+}
+.summary.degraded {
+ background: #fffaf0;
+ border-color: #eee0c6;
+}
+.summary.degraded .summary-icon {
+ color: #9c6b27;
+ background: #f9edcf;
+ border-color: #efdfb9;
+}
+.summary.degraded p {
+ color: #78633e;
+}
+.summary.outage {
+ background: #fff7f6;
+ border-color: #f0d9d5;
+}
+.summary.outage .summary-icon {
+ color: #b55951;
+ background: #fae6e2;
+ border-color: #efd4cd;
+}
+.summary.outage p {
+ color: #79564f;
+}
+.live-label {
+ display: inline-flex;
+ gap: 6px;
+ align-items: center;
+ font-family: var(--font-mono), monospace;
+ font-size: 10px;
+ color: #466b58;
+ white-space: nowrap;
+}
+.dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+ background: currentColor;
+}
+.live-dot {
+ color: #528e71;
+ box-shadow: 0 0 0 3px #528e7110;
+}
+.section-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 14px;
+ padding: 41px 0 19px;
+}
+.section-heading h2 {
+ font-size: 22px;
+ letter-spacing: -0.65px;
+ font-weight: 500;
+}
+.section-heading p {
+ margin-top: 7px;
+ color: var(--muted);
+ font-size: 13px;
+}
+.update-label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: #66717c;
+}
+.refresh-icon {
+ font-size: 16px;
+}
+.services-card {
+ border: 1px solid var(--line);
+ border-radius: 16px;
+ background: #ffffff;
+ box-shadow: 0 4px 16px -13px #25395130;
+}
+.service {
+ padding: 21px 25px 19px;
+}
+.service + .service {
+ border-top: 1px solid #edf0f3;
+}
+.service-heading {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ gap: 12px;
+}
+.service-name {
+ border: 0;
+ padding: 0;
+ background: none;
+ display: flex;
+ align-items: center;
+ gap: 11px;
+ text-align: left;
+ font-size: 14px;
+ font-weight: 500;
+}
+.service-icon {
+ width: 31px;
+ height: 31px;
+ display: grid;
+ place-items: center;
+ background: #f3f6fa;
+ border: 1px solid #e8edf4;
+ border-radius: 8px;
+ color: #6988ab;
+}
+.icon-api,
+.icon-render {
+ background: #f5f3fa;
+ border-color: #eeebf5;
+ color: #9282b6;
+}
+.icon-sharing {
+ background: #f0f6f3;
+ border-color: #e5eee8;
+ color: #719c83;
+}
+.icon-media {
+ background: #faf5ef;
+ border-color: #f1eae1;
+ color: #b19674;
+}
+.chevron {
+ font-size: 15px;
+ color: #9aa6b1;
+ margin-left: 1px;
+ transition: transform 160ms;
+}
+.chevron.expanded {
+ transform: rotate(180deg);
+}
+.status-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ white-space: nowrap;
+}
+.status-label.operational {
+ color: #3e8167;
+}
+.status-label.degraded {
+ color: #926515;
+}
+.status-label.outage {
+ color: #ad4848;
+}
+.status-label.unknown {
+ color: #66717c;
+}
+.history {
+ position: relative;
+}
+.bars {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ display: flex;
+ gap: 3px;
+ height: 26px;
+}
+.bar {
+ flex: 1;
+ min-width: 0;
+ border: 0;
+ padding: 0;
+ border-radius: 2px;
+ transition:
+ filter 120ms,
+ transform 120ms;
+}
+.bar:hover,
+.bar:focus-visible {
+ filter: brightness(0.88);
+ transform: scaleY(1.12);
+ outline-offset: 2px;
+}
+.bar.operational,
+.key-swatch.operational {
+ background: #88b7a3;
+}
+.bar.degraded,
+.key-swatch.degraded {
+ background: #dfb867;
+}
+.bar.outage,
+.key-swatch.outage {
+ background: #d68a83;
+}
+.bar.unknown,
+.key-swatch.unknown {
+ background: repeating-linear-gradient(
+ 135deg,
+ #e5eaf0 0,
+ #e5eaf0 3px,
+ #f1f4f7 3px,
+ #f1f4f7 5px
+ );
+}
+.history-tooltip {
+ position: absolute;
+ z-index: 5;
+ bottom: 36px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ border: 1px solid #dce3ea;
+ box-shadow: 0 7px 24px #203b5d16;
+ background: white;
+ border-radius: 9px;
+ padding: 10px 14px;
+ font-size: 11px;
+ white-space: nowrap;
+ pointer-events: none;
+}
+.history-tooltip span {
+ color: var(--muted);
+}
+.history-caption {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 9px;
+ font-family: var(--font-mono), monospace;
+ font-size: 9px;
+ color: #66717c;
+}
+.uptime {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+}
+.uptime strong {
+ font-weight: 400;
+ color: #697681;
+}
+.caption-divider {
+ margin: 0 5px;
+ color: #bdc7cf;
+}
+.service-details {
+ font-size: 12px;
+ color: #66717c;
+ padding: 17px 0 2px;
+ line-height: 1.7;
+}
+.service-details > div {
+ display: flex;
+ gap: 24px;
+ margin-top: 8px;
+ flex-wrap: wrap;
+}
+.service-details strong {
+ font-weight: 400;
+ color: #495a6b;
+ margin-left: 7px;
+}
+.detail-note {
+ margin-top: 8px;
+ color: #9b742f;
+}
+.history-key {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ padding: 15px 1px 0;
+ font-size: 10px;
+ color: #66717c;
+}
+.history-key > div {
+ display: flex;
+ gap: 13px;
+}
+.history-key > div > span {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+.key-swatch {
+ display: inline-block;
+ width: 7px;
+ height: 7px;
+ border-radius: 2px;
+}
+.incidents .section-heading {
+ padding-top: 44px;
+}
+.period-label {
+ font-family: var(--font-mono), monospace;
+ font-size: 9px;
+ letter-spacing: 0.5px;
+ color: #66717c;
+}
+.empty-incidents {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ padding: 28px 25px;
+ background: #fafcfd;
+}
+.empty-icon {
+ display: grid;
+ place-items: center;
+ width: 36px;
+ height: 36px;
+ flex-shrink: 0;
+ border: 1px solid #e2e9ee;
+ border-radius: 50%;
+ background: #f1f5f7;
+ color: #819d91;
+}
+.empty-incidents h3 {
+ font-weight: 400;
+ font-size: 14px;
+ letter-spacing: -0.15px;
+}
+.empty-incidents p {
+ margin-top: 6px;
+ font-size: 11px;
+ color: #66717c;
+}
+.empty-flower {
+ font-size: 36px;
+ color: #73879b;
+ margin-left: auto;
+ font-weight: 300;
+}
+.incident-list {
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ overflow: hidden;
+}
+.incident {
+ display: flex;
+ gap: 28px;
+ padding: 26px;
+ font-size: 13px;
+}
+.incident + .incident {
+ border-top: 1px solid var(--line);
+}
+.incident-date {
+ flex-shrink: 0;
+ width: 58px;
+ color: #66717c;
+ font-family: var(--font-mono), monospace;
+ font-size: 11px;
+ padding-top: 3px;
+}
+.incident-tag {
+ font-size: 10px;
+ padding: 3px 7px;
+ border-radius: 4px;
+}
+.incident-tag.operational {
+ background: #e9f3ed;
+ color: #477b60;
+}
+.incident-tag.outage {
+ background: #fae9e6;
+ color: #97483f;
+}
+.incident-tag.degraded {
+ background: #faf0d9;
+ color: #886020;
+}
+.incident h3 {
+ font-size: 15px;
+ font-weight: 500;
+ margin-top: 10px;
+}
+.incident p {
+ font-size: 12px;
+ line-height: 1.6;
+ margin-top: 7px;
+ color: #66717c;
+}
+.incident .incident-time {
+ font-family: var(--font-mono), monospace;
+ font-size: 10px;
+ margin-top: 12px;
+}
+.more-incidents {
+ border: 1px solid var(--line);
+ border-radius: 7px;
+ padding: 9px 13px;
+ background: white;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 12px;
+ margin: 18px auto 0;
+}
+.about-monitoring {
+ margin: 27px 0 35px;
+ border-bottom: 1px solid var(--line);
+ padding-bottom: 25px;
+ color: #66717c;
+ font-size: 12px;
+}
+.about-monitoring summary {
+ list-style: none;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.about-monitoring summary::-webkit-details-marker {
+ display: none;
+}
+.about-monitoring summary span {
+ font-size: 17px;
+ color: #9ca6b0;
+}
+.about-monitoring p {
+ padding: 15px 35px 0 0;
+ line-height: 1.9;
+}
+footer {
+ padding-bottom: 34px;
+ font-size: 10px;
+ color: #66717c;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 15px;
+}
+footer > div {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+}
+.footer-dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: #9aabb9;
+}
+footer nav {
+ display: flex;
+ gap: 22px;
+}
+footer a {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+footer svg {
+ width: 12px;
+ height: 12px;
+}
+.loading-services {
+ padding: 100px 20px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 14px;
+}
+
+@media (min-width: 1500px) {
+ .hero {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 1468px;
+ }
+}
+
+@media (max-width: 640px) {
+ .site-header {
+ height: 72px;
+ padding: 0 23px;
+ }
+ .brand {
+ font-size: 22px;
+ gap: 7px;
+ }
+ .brand img {
+ width: 31px;
+ height: 31px;
+ }
+ .brand-divider {
+ margin-left: 9px;
+ margin-right: 4px;
+ height: 17px;
+ }
+ .brand-context {
+ font-size: 14px;
+ }
+ .back-link {
+ font-size: 12px;
+ gap: 4px;
+ }
+ .hero {
+ margin: 0 9px;
+ border-radius: 19px;
+ min-height: 305px;
+ }
+ .hero-content {
+ padding: 44px 16px 66px;
+ }
+ .eyebrow {
+ font-size: 9px;
+ }
+ h1 {
+ letter-spacing: -1.8px;
+ margin-top: 23px;
+ font-size: clamp(32px, 8.5vw, 48px);
+ }
+ .hero-description {
+ font-size: 15px;
+ line-height: 1.5;
+ max-width: 240px;
+ margin: 16px auto 0;
+ }
+ .hero-link {
+ font-size: 11px;
+ margin-top: 22px;
+ }
+ .cloud-left {
+ width: 260px;
+ left: -175px;
+ top: 107px;
+ opacity: 0.7;
+ }
+ .cloud-right {
+ width: 310px;
+ right: -205px;
+ top: 120px;
+ opacity: 0.7;
+ }
+ .content {
+ padding: 0 21px;
+ margin-top: -24px;
+ }
+ .summary {
+ padding: 19px 17px;
+ gap: 10px;
+ border-radius: 13px;
+ }
+ .summary-main {
+ gap: 11px;
+ align-items: flex-start;
+ }
+ .summary-icon {
+ width: 32px;
+ height: 32px;
+ margin-top: 2px;
+ }
+ .summary-icon svg {
+ width: 18px;
+ height: 18px;
+ }
+ .summary h2 {
+ font-size: 16px;
+ letter-spacing: -0.4px;
+ }
+ .summary p {
+ font-size: 11px;
+ line-height: 1.6;
+ max-width: 220px;
+ }
+ .live-label {
+ font-size: 0;
+ gap: 0;
+ align-self: flex-start;
+ padding-top: 13px;
+ }
+ .live-label .dot {
+ width: 5px;
+ height: 5px;
+ }
+ .section-heading {
+ padding-top: 32px;
+ padding-bottom: 17px;
+ align-items: flex-start;
+ }
+ .section-heading h2 {
+ font-size: 20px;
+ }
+ .section-heading p {
+ font-size: 11px;
+ margin-top: 6px;
+ }
+ .update-label {
+ font-size: 9px;
+ padding-top: 5px;
+ }
+ .refresh-icon {
+ display: none;
+ }
+ .service {
+ padding: 18px 16px 16px;
+ }
+ .service-heading {
+ margin-bottom: 13px;
+ gap: 6px;
+ }
+ .service-name {
+ font-size: 12px;
+ gap: 7px;
+ }
+ .service-icon {
+ width: 27px;
+ height: 27px;
+ border-radius: 7px;
+ }
+ .service-icon svg {
+ width: 16px;
+ height: 16px;
+ }
+ .chevron {
+ display: none;
+ }
+ .status-label {
+ font-size: 9px;
+ gap: 4px;
+ }
+ .status-label .dot {
+ width: 4px;
+ height: 4px;
+ }
+ .bars {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ gap: 1.5px;
+ height: 24px;
+ }
+ .bar {
+ border-radius: 1px;
+ }
+ .history-caption {
+ font-size: 8px;
+ margin-top: 8px;
+ }
+ .caption-divider {
+ margin: 0 3px;
+ }
+ .history-key {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ font-size: 9px;
+ }
+ .history-key > div {
+ gap: 14px;
+ }
+ .incidents .section-heading {
+ padding-top: 34px;
+ }
+ .period-label {
+ font-size: 8px;
+ padding-top: 7px;
+ white-space: nowrap;
+ }
+ .empty-incidents {
+ padding: 20px 16px;
+ gap: 10px;
+ }
+ .empty-incidents h3 {
+ font-size: 12px;
+ line-height: 1.6;
+ max-width: 240px;
+ }
+ .empty-incidents p {
+ font-size: 10px;
+ line-height: 1.7;
+ }
+ .empty-icon {
+ width: 30px;
+ height: 30px;
+ }
+ .empty-icon svg {
+ width: 18px;
+ height: 18px;
+ }
+ .empty-flower {
+ display: none;
+ }
+ .incident {
+ gap: 15px;
+ padding: 20px 16px;
+ }
+ .incident-date {
+ width: 45px;
+ font-size: 10px;
+ }
+ .incident h3 {
+ font-size: 14px;
+ }
+ .incident p {
+ font-size: 11px;
+ }
+ .about-monitoring {
+ margin-top: 23px;
+ padding-bottom: 22px;
+ margin-bottom: 25px;
+ font-size: 11px;
+ }
+ footer {
+ align-items: flex-start;
+ flex-direction: column;
+ font-size: 9px;
+ gap: 16px;
+ padding-bottom: 30px;
+ }
+ footer nav {
+ gap: 24px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+ .bar,
+ .chevron,
+ .hero-link span {
+ transition: none;
+ }
+}
diff --git a/apps/status/app/layout.tsx b/apps/status/app/layout.tsx
new file mode 100644
index 00000000000..90855e538c9
--- /dev/null
+++ b/apps/status/app/layout.tsx
@@ -0,0 +1,41 @@
+import type { Metadata } from "next";
+import { DM_Mono, Instrument_Sans, Source_Serif_4 } from "next/font/google";
+import type { ReactNode } from "react";
+import "./globals.css";
+
+const sans = Instrument_Sans({
+ subsets: ["latin"],
+ weight: ["400", "500", "600"],
+ variable: "--font-sans",
+ display: "swap",
+});
+const serif = Source_Serif_4({
+ subsets: ["latin"],
+ weight: ["300", "400"],
+ variable: "--font-serif",
+ display: "swap",
+});
+const mono = DM_Mono({
+ subsets: ["latin"],
+ weight: "400",
+ variable: "--font-mono",
+ display: "swap",
+});
+
+export const metadata: Metadata = {
+ metadataBase: new URL("https://status.cap.so"),
+ title: "Cap Status — A little peace of mind.",
+ description: "Live service health, uptime, and incident history for Cap.",
+ icons: { icon: "/cap-icon.svg" },
+};
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
{children}
+
+ );
+}
diff --git a/apps/status/app/page.tsx b/apps/status/app/page.tsx
new file mode 100644
index 00000000000..51fca27a4a3
--- /dev/null
+++ b/apps/status/app/page.tsx
@@ -0,0 +1,5 @@
+import { StatusPage } from "../components/status-page";
+
+export default function Page() {
+ return ;
+}
diff --git a/apps/status/components/icons.tsx b/apps/status/components/icons.tsx
new file mode 100644
index 00000000000..ef4f012e262
--- /dev/null
+++ b/apps/status/components/icons.tsx
@@ -0,0 +1,68 @@
+export function Arrow({ diagonal = false }: { diagonal?: boolean }) {
+ return (
+
+ {diagonal ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export function Check() {
+ return (
+
+
+
+ );
+}
+
+export function ServiceIcon({ id }: { id: string }) {
+ const paths: Record = {
+ website:
+ "M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM3 12h18M12 3c3 3 3 15 0 18-3-3-3-15 0-18Z",
+ api: "m8 7-5 5 5 5m8-10 5 5-5 5m-3-12-2 14",
+ sharing: "M15 9h3a4 4 0 0 1 0 8h-4m-5-2H6a4 4 0 0 1 0-8h4m-2 5h8",
+ storage:
+ "M12 3c5 0 9 2 9 4s-4 4-9 4-9-2-9-4 4-4 9-4ZM3 7v10c0 2 4 4 9 4s9-2 9-4V7M3 12c0 2 4 4 9 4s9-2 9-4",
+ media: "M5 4h14v16H5zM5 8h14M5 16h14M9 4v4m6-4v4M9 16v4m6-4v4",
+ render:
+ "M7 18H6a4 4 0 0 1-1-8 7 7 0 0 1 13-2 5 5 0 0 1 0 10h-1m-8-4 3-3 3 3m-3-3v10",
+ };
+ return (
+
+
+
+ );
+}
diff --git a/apps/status/components/service-row.tsx b/apps/status/components/service-row.tsx
new file mode 100644
index 00000000000..bf2b44c55ac
--- /dev/null
+++ b/apps/status/components/service-row.tsx
@@ -0,0 +1,172 @@
+"use client";
+
+import { useId, useState } from "react";
+import { availability, dayStatus } from "../lib/state";
+import type { Day, Health, PublicService } from "../lib/types";
+import { ServiceIcon } from "./icons";
+
+export const LABELS: Record = {
+ operational: "Operational",
+ degraded: "Degraded",
+ outage: "Outage",
+ unknown: "No recent data",
+};
+
+export function formatUptime(days: Day[]): string {
+ const value = availability(days);
+ return value === null ? "—" : `${value === 100 ? "100" : value.toFixed(2)}%`;
+}
+
+export function formatDate(date: string): string {
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ timeZone: "UTC",
+ });
+}
+
+function dayDescription(day: Day) {
+ const checks = day.operational + day.degraded + day.outage;
+ return checks
+ ? `${formatUptime([day])} observed uptime · ${checks.toLocaleString()} checks`
+ : "No monitoring data";
+}
+
+export function ServiceRow({ service }: { service: PublicService }) {
+ const [expanded, setExpanded] = useState(false);
+ const [selected, setSelected] = useState(service.days.length - 1);
+ const [tooltip, setTooltip] = useState(false);
+ const detailsId = useId();
+ const activeDay = service.days[selected];
+ return (
+
+
+ setExpanded(!expanded)}
+ aria-expanded={expanded}
+ aria-controls={detailsId}
+ >
+
+
+
+ {service.name}
+
+ ⌄
+
+
+
+
+ {LABELS[service.status]}
+
+
+
+
setTooltip(false)}
+ aria-label={`${service.name}: daily uptime history. Use arrow keys to explore.`}
+ >
+ {service.days.map((day, index) => (
+ {
+ setSelected(index);
+ setTooltip(true);
+ }}
+ onFocus={() => {
+ setSelected(index);
+ setTooltip(true);
+ }}
+ onBlur={() => setTooltip(false)}
+ onClick={() => {
+ setSelected(index);
+ setTooltip(true);
+ }}
+ onKeyDown={(event) => {
+ if (
+ ![
+ "ArrowLeft",
+ "ArrowRight",
+ "Home",
+ "End",
+ "Escape",
+ ].includes(event.key)
+ )
+ return;
+ event.preventDefault();
+ if (event.key === "Escape") {
+ setTooltip(false);
+ return;
+ }
+ const next =
+ event.key === "Home"
+ ? 0
+ : event.key === "End"
+ ? service.days.length - 1
+ : Math.min(
+ service.days.length - 1,
+ Math.max(
+ 0,
+ index + (event.key === "ArrowRight" ? 1 : -1),
+ ),
+ );
+ (
+ event.currentTarget.parentElement?.children[
+ next
+ ] as HTMLButtonElement
+ )?.focus();
+ }}
+ />
+ ))}
+
+ {tooltip && activeDay && (
+
+ {formatDate(activeDay.date)} UTC
+ {dayDescription(activeDay)}
+
+ )}
+
+
+ 90 days ago
+
+ {formatUptime(service.days)} uptime
+ · Today
+
+
+
+
{service.description}
+
+
+ Latest response{" "}
+
+ {service.latencyMs === null ? "—" : `${service.latencyMs} ms`}
+
+
+
+ Last checked{" "}
+
+ {service.checkedAt
+ ? `${new Date(service.checkedAt).toLocaleTimeString("en-GB", {
+ timeZone: "UTC",
+ })} UTC`
+ : "Awaiting first check"}
+
+
+
+ {!service.configured && (
+
+ Monitoring has not been configured for this service yet.
+
+ )}
+
+
+ );
+}
diff --git a/apps/status/components/status-page.tsx b/apps/status/components/status-page.tsx
new file mode 100644
index 00000000000..8cdd29ddf27
--- /dev/null
+++ b/apps/status/components/status-page.tsx
@@ -0,0 +1,388 @@
+"use client";
+
+import Image from "next/image";
+import { useEffect, useId, useState } from "react";
+import { emptyState, isStale, publicSnapshot } from "../lib/state";
+import type { Health, Snapshot } from "../lib/types";
+import { Arrow, Check } from "./icons";
+import { formatDate, ServiceRow } from "./service-row";
+
+const MESSAGES: Record = {
+ operational: {
+ title: "All systems operational",
+ description: "Everything is running smoothly. Happy recording.",
+ },
+ degraded: {
+ title: "Some services are degraded",
+ description: "You may experience slower responses on the services below.",
+ },
+ outage: {
+ title: "Some services are unavailable",
+ description:
+ "We’re detecting an interruption. See affected services below.",
+ },
+ unknown: {
+ title: "Waiting for a clear picture",
+ description: "Some services don’t have recent monitoring data yet.",
+ },
+};
+
+function overallStatus(statuses: Health[]): Health {
+ return statuses.includes("outage")
+ ? "outage"
+ : statuses.includes("degraded")
+ ? "degraded"
+ : statuses.includes("unknown")
+ ? "unknown"
+ : "operational";
+}
+
+export function StatusPage() {
+ const titleId = useId();
+ const servicesId = useId();
+ const incidentsId = useId();
+ const [snapshot, setSnapshot] = useState(null);
+ const [unavailable, setUnavailable] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [now, setNow] = useState(null);
+ const [showAll, setShowAll] = useState(false);
+
+ useEffect(() => {
+ let active = true;
+ let inFlight = false;
+ let controller: AbortController | null = null;
+ async function refresh() {
+ if (inFlight || document.hidden) return;
+ inFlight = true;
+ controller = new AbortController();
+ const timeout = setTimeout(() => controller?.abort(), 10_000);
+ try {
+ const response = await fetch("/api/status", {
+ signal: controller.signal,
+ });
+ if (!response.ok) throw new Error("Status unavailable");
+ const data: Snapshot = await response.json();
+ if (active) {
+ setSnapshot(data);
+ setUnavailable(false);
+ }
+ } catch {
+ if (active) setUnavailable(true);
+ } finally {
+ clearTimeout(timeout);
+ inFlight = false;
+ if (active) {
+ setLoading(false);
+ setNow(Date.now());
+ }
+ }
+ }
+ setNow(Date.now());
+ void refresh();
+ const poll = setInterval(() => {
+ if (document.hidden) return;
+ setNow(Date.now());
+ void refresh();
+ }, 60_000);
+ const clock = setInterval(() => {
+ if (!document.hidden) setNow(Date.now());
+ }, 15_000);
+ document.addEventListener("visibilitychange", refresh);
+ return () => {
+ active = false;
+ controller?.abort();
+ clearInterval(poll);
+ clearInterval(clock);
+ document.removeEventListener("visibilitychange", refresh);
+ };
+ }, []);
+
+ const data = snapshot ?? (now ? publicSnapshot(emptyState(), now) : null);
+ const services =
+ data?.services.map((service) => ({
+ ...service,
+ status:
+ unavailable || isStale(service.checkedAt, now ?? 0)
+ ? ("unknown" as const)
+ : service.status,
+ })) ?? [];
+ const overall = overallStatus(
+ services.length ? services.map((service) => service.status) : ["unknown"],
+ );
+ const message = MESSAGES[overall];
+ const incidents = data?.incidents ?? [];
+ const visibleIncidents = showAll ? incidents : incidents.slice(0, 5);
+ const fresh = Boolean(
+ snapshot?.checkedAt &&
+ !isStale(snapshot.checkedAt, now ?? 0) &&
+ !unavailable,
+ );
+ const secondsAgo =
+ snapshot?.checkedAt && now
+ ? Math.max(0, Math.floor((now - Date.parse(snapshot.checkedAt)) / 1_000))
+ : null;
+
+ return (
+ <>
+
+ Skip to service status
+
+
+
+
+
+
+
+
+ {overall === "operational" ? (
+
+ ) : (
+
+ {overall === "unknown" ? "· · ·" : "!"}
+
+ )}
+
+
+
+ {loading
+ ? "Checking system status"
+ : unavailable
+ ? "Status updates are unavailable"
+ : message.title}
+
+
+ {loading
+ ? "Getting the latest service health."
+ : unavailable
+ ? "We can’t reach the monitor right now. Service health is unknown."
+ : message.description}
+
+
+
+
+
+ {fresh ? "Live" : loading ? "Connecting" : "Awaiting data"}
+
+
+
+
+
Service status
+
Small checks. A clearer picture.
+
+
+
+ ↻
+
+ {secondsAgo !== null && fresh
+ ? `Updated ${secondsAgo < 60 ? "just now" : `${Math.floor(secondsAgo / 60)}m ago`}`
+ : "Updates every minute"}
+
+
+
+ {services.map((service) => (
+
+ ))}
+ {!data && (
+ Loading service health…
+ )}
+
+
+
Daily uptime · last 90 days · UTC
+
+
+
+ Operational
+
+
+
+ Degraded
+
+
+
+ Outage
+
+
+
+ No data
+
+
+
+
+
+
+
Incident history
+
A little context, if something goes off track.
+
+
PAST 90 DAYS
+
+ {visibleIncidents.length ? (
+
+ {visibleIncidents.map((incident) => {
+ const service = services.find(
+ (entry) => entry.id === incident.serviceId,
+ );
+ return (
+
+
+ {formatDate(incident.startedAt)}
+
+
+
+ {incident.resolvedAt ? "Resolved" : "Detected"}
+
+
+ {service?.name ?? "Service"}{" "}
+ {incident.severity === "outage"
+ ? "interruption"
+ : "degraded performance"}
+
+
+ Automated checks detected{" "}
+ {incident.severity === "outage"
+ ? "an availability issue"
+ : "reduced service health"}
+ .
+ {incident.resolvedAt
+ ? " Service recovered after two successful checks."
+ : " Recovery has not yet been confirmed."}
+
+
+ {new Date(incident.startedAt).toLocaleTimeString(
+ "en-GB",
+ {
+ hour: "2-digit",
+ minute: "2-digit",
+ timeZone: "UTC",
+ },
+ )}{" "}
+ UTC
+ {incident.resolvedAt
+ ? ` · Recovered ${formatDate(incident.resolvedAt)} at ${new Date(incident.resolvedAt).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: "UTC" })} UTC`
+ : " · Ongoing"}
+
+
+
+ );
+ })}
+
+ ) : (
+
+
+
+
+
+
+ {snapshot?.checkedAt
+ ? "Nothing to report. Just the way we like it."
+ : "Your incident history starts here."}
+
+
+ {snapshot?.checkedAt
+ ? "No incidents recorded in the monitoring history available."
+ : "Incidents will appear here once monitoring begins."}
+
+
+
+ ✳︎
+
+
+ )}
+ {incidents.length > 5 && (
+ setShowAll(!showAll)}
+ >
+ {showAll
+ ? "Show fewer incidents"
+ : `View all ${incidents.length} incidents`}{" "}
+
+
+ )}
+
+
+
+ How we keep an eye on things +
+
+
+ Lightweight checks run every minute, independently of this page.
+ We retry an unhealthy response before recording it, confirm
+ incidents across two checks, and confirm recovery twice. Grey bars
+ mean no data; gaps are never counted as uptime. Percentages
+ describe successful observed checks, not unmonitored time. Select
+ a service or a day to explore its history. All times are UTC.
+
+
+
+
+
+ >
+ );
+}
diff --git a/apps/status/lib/probe.ts b/apps/status/lib/probe.ts
new file mode 100644
index 00000000000..4408e90a71c
--- /dev/null
+++ b/apps/status/lib/probe.ts
@@ -0,0 +1,79 @@
+import type { ProbeResult, ServiceDefinition } from "./types";
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+export function validateHealth(
+ service: ServiceDefinition,
+ body: unknown,
+): boolean {
+ if (!isRecord(body)) return false;
+ if (service.validation === "media") return body.status === "ok";
+ if (service.validation === "render")
+ return (
+ body.ok === true &&
+ typeof body.workers === "number" &&
+ body.workers > 0 &&
+ typeof body.slots === "number" &&
+ body.slots > 0
+ );
+ if (service.validation === "auth")
+ return Object.values(body).some(
+ (provider) =>
+ isRecord(provider) &&
+ typeof provider.id === "string" &&
+ typeof provider.type === "string",
+ );
+ return true;
+}
+
+async function attempt(
+ service: ServiceDefinition,
+ url: string,
+ fetcher: typeof fetch,
+): Promise {
+ const start = performance.now();
+ try {
+ const response = await fetcher(url, {
+ method: service.method,
+ redirect: "manual",
+ cache: "no-store",
+ signal: AbortSignal.timeout(8_000),
+ headers: {
+ "User-Agent": "Cap-Status/1.0",
+ Accept: service.validation ? "application/json" : "*/*",
+ },
+ });
+ let status: ProbeResult["status"] = response.ok ? "operational" : "outage";
+ if (
+ response.ok &&
+ service.validation &&
+ !validateHealth(service, await response.json())
+ )
+ status = "degraded";
+ if (!response.bodyUsed) await response.body?.cancel();
+ const latencyMs = Math.round(performance.now() - start);
+ return {
+ status:
+ status === "operational" && latencyMs > 3_000 ? "degraded" : status,
+ latencyMs,
+ };
+ } catch {
+ return {
+ status: "outage",
+ latencyMs: Math.round(performance.now() - start),
+ };
+ }
+}
+
+export async function probe(
+ service: ServiceDefinition,
+ url: string,
+ fetcher = fetch,
+): Promise {
+ const first = await attempt(service, url, fetcher);
+ return first.status === "operational"
+ ? first
+ : attempt(service, url, fetcher);
+}
diff --git a/apps/status/lib/services.ts b/apps/status/lib/services.ts
new file mode 100644
index 00000000000..60e423289b7
--- /dev/null
+++ b/apps/status/lib/services.ts
@@ -0,0 +1,69 @@
+import type { ServiceDefinition } from "./types";
+
+export const SERVICES: ServiceDefinition[] = [
+ {
+ id: "website",
+ name: "Website",
+ description: "The Cap website and downloads.",
+ env: "STATUS_WEBSITE_URL",
+ defaultUrl: "https://cap.so",
+ method: "HEAD",
+ },
+ {
+ id: "api",
+ name: "API & authentication",
+ description: "Availability of Cap’s authentication API.",
+ env: "STATUS_API_URL",
+ defaultUrl: "https://cap.so/api/auth/providers",
+ method: "GET",
+ validation: "auth",
+ },
+ {
+ id: "sharing",
+ name: "Video sharing",
+ description: "Opening and viewing shared recordings.",
+ env: "STATUS_SHARING_URL",
+ method: "HEAD",
+ },
+ {
+ id: "storage",
+ name: "Storage & delivery",
+ description: "Access to recordings on Cap’s storage and CDN.",
+ env: "STATUS_STORAGE_URL",
+ method: "HEAD",
+ },
+ {
+ id: "media",
+ name: "Media processing",
+ description: "The service that processes and converts your recordings.",
+ env: "STATUS_MEDIA_URL",
+ method: "GET",
+ validation: "media",
+ },
+ {
+ id: "render",
+ name: "Cloud rendering",
+ description: "The render coordinator and its available workers.",
+ env: "STATUS_RENDER_URL",
+ method: "GET",
+ validation: "render",
+ },
+];
+
+export function serviceUrl(service: ServiceDefinition): string | undefined {
+ const value = process.env[service.env]?.trim() || service.defaultUrl;
+ if (!value) return undefined;
+ const url = new URL(value);
+ if (url.protocol !== "https:" || url.username || url.password) {
+ throw new Error(`Invalid HTTPS endpoint for ${service.id}`);
+ }
+ return url.toString();
+}
+
+export function isConfigured(service: ServiceDefinition): boolean {
+ try {
+ return Boolean(serviceUrl(service));
+ } catch {
+ return false;
+ }
+}
diff --git a/apps/status/lib/state.ts b/apps/status/lib/state.ts
new file mode 100644
index 00000000000..02cb8a74470
--- /dev/null
+++ b/apps/status/lib/state.ts
@@ -0,0 +1,189 @@
+import { isConfigured, SERVICES } from "./services";
+import type {
+ Day,
+ Health,
+ MonitorState,
+ ProbeResult,
+ ServiceState,
+ Snapshot,
+} from "./types";
+
+export const STALE_AFTER_MS = 180_000;
+export const HISTORY_DAYS = 90;
+const DAY_MS = 86_400_000;
+
+export function emptyState(): MonitorState {
+ return { version: 1, checkedAt: null, services: {}, incidents: [] };
+}
+
+export function emptyService(): ServiceState {
+ return {
+ status: "unknown",
+ checkedAt: null,
+ latencyMs: null,
+ failures: 0,
+ successes: 0,
+ issueStartedAt: null,
+ incidentId: null,
+ days: [],
+ };
+}
+
+export function dates(now: number): string[] {
+ return Array.from({ length: HISTORY_DAYS }, (_, index) =>
+ new Date(now - (HISTORY_DAYS - 1 - index) * DAY_MS)
+ .toISOString()
+ .slice(0, 10),
+ );
+}
+
+export function isStale(checkedAt: string | null, now: number): boolean {
+ return !checkedAt || now - Date.parse(checkedAt) > STALE_AFTER_MS;
+}
+
+export function dayStatus(day: Day): Health {
+ if (day.outage) return "outage";
+ if (day.degraded) return "degraded";
+ return day.operational ? "operational" : "unknown";
+}
+
+export function availability(days: Day[]): number | null {
+ const observed = days.reduce(
+ (sum, day) => sum + day.operational + day.degraded + day.outage,
+ 0,
+ );
+ return observed
+ ? (days.reduce((sum, day) => sum + day.operational, 0) / observed) * 100
+ : null;
+}
+
+export function applyResult(
+ state: MonitorState,
+ id: string,
+ result: ProbeResult | null,
+ now: number,
+): void {
+ const previous = state.services[id] ?? emptyService();
+ const time = new Date(now).toISOString();
+ const cutoff = dates(now)[0];
+ previous.days = previous.days.filter((day) => day.date >= cutoff);
+ if (!result) {
+ previous.status = "unknown";
+ previous.checkedAt = null;
+ previous.latencyMs = null;
+ previous.failures = 0;
+ previous.successes = 0;
+ previous.issueStartedAt = null;
+ state.services[id] = previous;
+ return;
+ }
+ if (
+ previous.checkedAt &&
+ Math.floor(Date.parse(previous.checkedAt) / 60_000) >=
+ Math.floor(now / 60_000)
+ )
+ return;
+ if (isStale(previous.checkedAt, now)) {
+ previous.failures = 0;
+ previous.successes = 0;
+ previous.issueStartedAt = null;
+ previous.status = "unknown";
+ }
+ const day = previous.days.find(
+ (entry) => entry.date === time.slice(0, 10),
+ ) ?? { date: time.slice(0, 10), operational: 0, degraded: 0, outage: 0 };
+ if (!previous.days.includes(day)) previous.days.push(day);
+ day[result.status]++;
+ previous.checkedAt = time;
+ previous.latencyMs = result.latencyMs;
+ if (result.status === "operational") {
+ previous.successes++;
+ previous.failures = 0;
+ previous.issueStartedAt = null;
+ if (!previous.incidentId || previous.successes >= 2) {
+ previous.status = "operational";
+ const incident = state.incidents.find(
+ (entry) => entry.id === previous.incidentId,
+ );
+ if (incident) incident.resolvedAt = time;
+ previous.incidentId = null;
+ }
+ } else {
+ previous.successes = 0;
+ previous.failures++;
+ previous.issueStartedAt ??= time;
+ if (previous.failures >= 2) {
+ previous.status = result.status;
+ if (!previous.incidentId) {
+ previous.incidentId = `${id}-${now}`;
+ state.incidents.unshift({
+ id: previous.incidentId,
+ serviceId: id,
+ severity: result.status,
+ startedAt: previous.issueStartedAt,
+ resolvedAt: null,
+ });
+ } else {
+ const incident = state.incidents.find(
+ (entry) => entry.id === previous.incidentId,
+ );
+ if (incident && result.status === "outage")
+ incident.severity = "outage";
+ }
+ } else if (previous.status === "operational") {
+ previous.status = "degraded";
+ }
+ }
+ state.services[id] = previous;
+ state.incidents = state.incidents
+ .filter((entry) => !entry.resolvedAt)
+ .concat(
+ state.incidents
+ .filter(
+ (entry) =>
+ entry.resolvedAt &&
+ Date.parse(entry.resolvedAt) >= now - HISTORY_DAYS * DAY_MS,
+ )
+ .slice(0, 500),
+ )
+ .sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
+}
+
+export function publicSnapshot(
+ state: MonitorState,
+ now = Date.now(),
+): Snapshot {
+ return {
+ checkedAt: state.checkedAt,
+ services: SERVICES.map((service) => {
+ const saved = state.services[service.id] ?? emptyService();
+ const configured = isConfigured(service);
+ return {
+ id: service.id,
+ name: service.name,
+ description: service.description,
+ configured,
+ status:
+ !configured || isStale(saved.checkedAt, now)
+ ? "unknown"
+ : saved.status,
+ checkedAt: saved.checkedAt,
+ latencyMs: saved.latencyMs,
+ days: dates(now).map(
+ (date) =>
+ saved.days.find((day) => day.date === date) ?? {
+ date,
+ operational: 0,
+ degraded: 0,
+ outage: 0,
+ },
+ ),
+ };
+ }),
+ incidents: state.incidents.filter(
+ (entry) =>
+ !entry.resolvedAt ||
+ Date.parse(entry.resolvedAt) >= now - HISTORY_DAYS * DAY_MS,
+ ),
+ };
+}
diff --git a/apps/status/lib/store.ts b/apps/status/lib/store.ts
new file mode 100644
index 00000000000..e363cfc9ce2
--- /dev/null
+++ b/apps/status/lib/store.ts
@@ -0,0 +1,37 @@
+import { Redis } from "@upstash/redis";
+import { emptyState } from "./state";
+import type { MonitorState } from "./types";
+
+export const STATE_KEY = "cap:status:v1:state";
+export const LOCK_KEY = "cap:status:v1:lock";
+
+export function getRedis(): Redis {
+ const url = process.env.UPSTASH_REDIS_REST_URL;
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN;
+ if (!url || !token) throw new Error("Status storage is not configured");
+ return new Redis({
+ url,
+ token,
+ retry: { retries: 1 },
+ signal: () => AbortSignal.timeout(5_000),
+ });
+}
+
+export async function readState(redis: Redis): Promise {
+ const state = await redis.get(STATE_KEY);
+ if (state && state.version !== 1)
+ throw new Error("Unsupported status data version");
+ return state ?? emptyState();
+}
+
+export const COMMIT_SCRIPT = `
+if redis.call("GET", KEYS[1]) ~= ARGV[1] then return 0 end
+redis.call("SET", KEYS[2], ARGV[2])
+redis.call("DEL", KEYS[1])
+return 1
+`;
+
+export const RELEASE_SCRIPT = `
+if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) end
+return 0
+`;
diff --git a/apps/status/lib/types.ts b/apps/status/lib/types.ts
new file mode 100644
index 00000000000..e71d65b62da
--- /dev/null
+++ b/apps/status/lib/types.ts
@@ -0,0 +1,66 @@
+export type Health = "operational" | "degraded" | "outage" | "unknown";
+
+export type Day = {
+ date: string;
+ operational: number;
+ degraded: number;
+ outage: number;
+};
+
+export type Incident = {
+ id: string;
+ serviceId: string;
+ severity: "degraded" | "outage";
+ startedAt: string;
+ resolvedAt: string | null;
+};
+
+export type ServiceState = {
+ status: Health;
+ checkedAt: string | null;
+ latencyMs: number | null;
+ failures: number;
+ successes: number;
+ issueStartedAt: string | null;
+ incidentId: string | null;
+ days: Day[];
+};
+
+export type MonitorState = {
+ version: 1;
+ checkedAt: string | null;
+ services: Record;
+ incidents: Incident[];
+};
+
+export type ServiceDefinition = {
+ id: string;
+ name: string;
+ description: string;
+ env: string;
+ defaultUrl?: string;
+ method: "GET" | "HEAD";
+ validation?: "auth" | "media" | "render";
+};
+
+export type PublicService = Pick<
+ ServiceDefinition,
+ "id" | "name" | "description"
+> & {
+ status: Health;
+ checkedAt: string | null;
+ latencyMs: number | null;
+ configured: boolean;
+ days: Day[];
+};
+
+export type Snapshot = {
+ checkedAt: string | null;
+ services: PublicService[];
+ incidents: Incident[];
+};
+
+export type ProbeResult = {
+ status: Exclude;
+ latencyMs: number;
+};
diff --git a/apps/status/next.config.ts b/apps/status/next.config.ts
new file mode 100644
index 00000000000..7eca8d8ebe7
--- /dev/null
+++ b/apps/status/next.config.ts
@@ -0,0 +1,19 @@
+import type { NextConfig } from "next";
+
+const config: NextConfig = {
+ poweredByHeader: false,
+ async headers() {
+ return [
+ {
+ source: "/:path*",
+ headers: [
+ { key: "X-Content-Type-Options", value: "nosniff" },
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
+ { key: "X-Frame-Options", value: "DENY" },
+ ],
+ },
+ ];
+ },
+};
+
+export default config;
diff --git a/apps/status/package.json b/apps/status/package.json
new file mode 100644
index 00000000000..54fdc563cc1
--- /dev/null
+++ b/apps/status/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@cap/status",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --port 3002",
+ "build": "next build",
+ "start": "next start --port 3002",
+ "typecheck": "next typegen && tsc --noEmit",
+ "test": "bun test tests"
+ },
+ "dependencies": {
+ "@upstash/redis": "1.39.0",
+ "next": "16.3.0",
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.5",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/apps/status/public/cap-icon.svg b/apps/status/public/cap-icon.svg
new file mode 100644
index 00000000000..7071770d13d
--- /dev/null
+++ b/apps/status/public/cap-icon.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/status/public/cloud-left.webp b/apps/status/public/cloud-left.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a66987358223fb3a82630f5139bb3848b7d6b5e6
GIT binary patch
literal 36836
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%0AjvG?Ai;oHGfbY2g*8>uE
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
1VkECpvY;JRw!86F>cs7~t-V^ee
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