# System Health & Status Page

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 73  
**Tier:** Public (no auth) + SUPER_ADMIN for incident management  
**Depends on:** `foundation-monorepo`, `admin-dashboard`  
**Referenced by:** `admin-dashboard`, `zync-www-marketing-site`

---

## Overview

Public status page at `status.zync.is` (or `zync.is/status`) showing real-time and historical system health. SUPER_ADMIN can post and update incident notices. Tenant users are directed here during outages.

---

## Public Page: `status.zync.is`

Static-first Astro page with hybrid SSR for incident overlay.

```
┌────────────────────────────────────────────────────────────┐
│  Zync Status                           Updated: 2 min ago  │
│                                                            │
│  ● All systems operational                                 │
│                                                            │
│  Services                                                  │
│  ─────────────────────────────────────────────────────── │
│  ● App (app.zync.is)            Operational                │
│  ● API                          Operational                │
│  ● File uploads (R2)            Operational                │
│  ● Email delivery               Operational                │
│  ● Payment processing           Operational                │
│  ● AI assistant                 Operational                │
│  ● Customer portals             Operational                │
│                                                            │
│  Past 90 days uptime:  99.8%                               │
│  ██████████████████████████████████░  (bar chart)         │
│                                                            │
│  No recent incidents.                                      │
│                                                            │
│  [Subscribe to updates]    [RSS feed]                      │
└────────────────────────────────────────────────────────────┘
```

During an active incident:

```
┌────────────────────────────────────────────────────────────┐
│  ⚠ Investigating an issue                                  │
│                                                            │
│  ● App (app.zync.is)            Operational                │
│  ⚠ Email delivery               Degraded performance       │
│  ● All other services           Operational                │
│                                                            │
│  ── Active Incident ──────────────────────────────────── │
│  Email delivery delays — Investigating                     │
│  Started: 2026-05-31 14:22 UTC                            │
│                                                            │
│  Updates:                                                  │
│  14:35  We've identified the issue with our email         │
│         provider and are working on a fix.                 │
│  14:22  Investigating reports of email delivery delays.   │
└────────────────────────────────────────────────────────────┘
```

---

## Data Model

```sql
CREATE TABLE system_incidents (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title           TEXT NOT NULL,
  status          TEXT NOT NULL,   -- 'investigating' | 'identified' | 'monitoring' | 'resolved'
  impact          TEXT NOT NULL,   -- 'minor' | 'major' | 'critical'
  affected_services TEXT[],        -- e.g. ['email_delivery', 'api']
  created_at      TIMESTAMPTZ DEFAULT now(),
  resolved_at     TIMESTAMPTZ,
  created_by      TEXT NOT NULL    -- SUPER_ADMIN identifier (email); admin staff are not in the tenant users table
);

CREATE TABLE system_incident_updates (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  incident_id     UUID NOT NULL REFERENCES system_incidents(id),
  body            TEXT NOT NULL,
  status          TEXT NOT NULL,   -- same enum as incidents.status
  created_at      TIMESTAMPTZ DEFAULT now(),
  created_by      TEXT NOT NULL    -- SUPER_ADMIN identifier (email)
);
```

Service status is derived from active incidents: if `system_incidents.affected_services` contains a service name and `resolved_at` is null → that service is `degraded` or `outage` based on `impact`.

---

## SUPER_ADMIN Incident Management (`/admin/incidents`)

```
┌────────────────────────────────────────────────────────────┐
│  System Incidents                    [+ New Incident]      │
│                                                            │
│  Active:                                                   │
│  ⚠ Email delivery delays — Investigating (since 14:22)    │
│  [Post update]  [Resolve]                                  │
│                                                            │
│  Past (last 30 days):                                      │
│  ✓ Database maintenance window — 2026-05-28 (resolved)     │
│  ✓ API latency spike — 2026-05-20 (resolved)              │
└────────────────────────────────────────────────────────────┘
```

"New Incident" → form: title, status, impact (minor/major/critical), affected services (multi-select), initial update text.

"Post update" → adds to `system_incident_updates` with new status + message.

"Resolve" → sets `resolved_at`, `status = 'resolved'`, prompts for final update text.

---

## Email Subscription

"Subscribe to updates" → email form (no account needed). Stores email in `status_subscribers` table. Notification sent on incident create, updates, and resolve via Resend.

```sql
CREATE TABLE status_subscribers (
  id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email      TEXT NOT NULL UNIQUE,
  subscribed_at TIMESTAMPTZ DEFAULT now(),
  unsubscribe_token TEXT NOT NULL UNIQUE DEFAULT encode(gen_random_bytes(16), 'hex')
);
```

Unsubscribe link in every notification email: `GET /status/unsubscribe?token={unsubscribeToken}`.

---

## API Endpoints

```
GET  /api/status                    → public; current service status + active incidents
GET  /api/status/history?days=90   → incident history for uptime bar chart

POST /api/admin/incidents          → create incident (SUPER_ADMIN)
POST /api/admin/incidents/:id/update → add update (SUPER_ADMIN)
POST /api/admin/incidents/:id/resolve → resolve (SUPER_ADMIN)
```

Status page data cached in KV (`STATUS_KV` binding) with 30-second TTL. On incident change: cache invalidated immediately.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Status page at `status.zync.is` | Separate subdomain | If `app.zync.is` is down, the status page must still be accessible from a different origin/deployment |
| Incidents in Neon DB | Not a third-party status service | Avoids dependency on Atlassian Statuspage/Instatus; simpler; custom incident taxonomy matches Zync's services |
| Service status derived from incidents | Not a separate health check table | Real-time pinging (Worker → self) introduces circular dependency; incident-driven is simpler and always accurate |
| 30-second KV cache | Not uncached DB query | Status page gets traffic spikes during outages (exactly when DB may be stressed); KV cache decouples the status page from the DB |
