Public status page for the Experiential Labs platform, served at status.experientiallabs.ai, powered by Upptime (MIT).
Checks run every 5 minutes from GitHub Actions and the page is a static site served by Vercel, so the status page shares no infrastructure with the platform: no Porter cluster, no Supabase, no shared pooler. If the platform is down, this page stays up and says so.
| Component | Check | Healthy when |
|---|---|---|
| Web Dashboard | GET platform.experientiallabs.ai/signin (light, always-public) |
200 |
| API | GET api.experientiallabs.ai/v1/models, unauthenticated |
401; the app rejecting the request proves edge + gateway worker + auth are alive, and a 5xx is down |
| Docs | GET platform.experientiallabs.ai/docs |
200 |
| Gateway (authenticated) | GET api.experientiallabs.ai/v1/models with the status-monitor org key |
200; proves key auth, the gateway's Postgres path, and the catalog serve a signed-in caller |
| API, live traffic | Gateway ledger, last 15 minutes of real customer requests | Rendered on the API row; gateway-owned error rate and volume vs baseline (below) |
| Gateway Completions | POST /v1/chat/completions, a real 1-token completion |
200; disabled until the status-monitor org is funded (below) |
All checks live in .upptimerc.yml. That file is the single
source of truth: the workflows in .github/workflows are generated from it by
Upptime's update-template command, and Setup CI regenerates them on every push
that touches the config. Change the config, not the workflows.
The page's 90-day bars, uptime percentages, and dated Past Incidents section all
read ONE record: assets/status-ui/incidents.json (curated) merged with the
checker's live status-labelled issues opened after the record's
liveIssuesSince. Edits to the JSON take effect on the next page load (it is
fetched from raw main); no redeploy.
-
Automatic: when a check fails, Upptime opens an issue labeled
statusand the component slug, assigns it, and the page lists it from itscreated_atuntil it is closed. When the check recovers the issue is closed automatically. If an issue turns out to be a monitoring artifact (revoked probe key, runner network blip, an issue that lingered open after recovery), add its number toannulledIssuesin the JSON and remove itsstatuslabel so neither the page nor Upptime counts it. -
Curated incident (the normal way to record what actually happened): add an entry to
incidentswithcomponent(api,web,docs),severity(down= not serving, counts against uptime;degraded= serving with elevated errors or latency, shown in yellow, not counted), UTCstart/endset to when the effect actually started and stopped (from the gateway ledger, deploy runs, or logs — not when an issue was opened or closed), a shorttitle, and a one- or two-sentence technicaldescription(what failed, why, what fixed it, with the fix time). Keep entries minimal and factual. -
Manual incident via issue: open an issue with the
statuslabel and the component slug label. The issue title is the headline; comments are updates. Close the issue to resolve it; the page shows the open-to-close window as the duration, so close it when the effect ends, not later. -
Scheduled maintenance: open an issue with the
maintenancelabel and put the window in an HTML comment in the body (slugs are comma-separated):<!-- start: 2026-09-01T06:00:00Z end: 2026-09-01T06:30:00Z expectedDown: api, gateway -->During the window the listed components fail without opening a new incident, and Upptime closes and locks the issue automatically once
endpasses.
Synthetic checks prove the door opens; they cannot see whether the requests
customers are actually sending succeed. traffic-health.yml (hand-maintained,
every 5 minutes) reads the production gateway ledger through the read-only
PROD_OPS_AGENT_DB_URL role and writes assets/status-ui/traffic-health.json,
which the status-ui overlay renders on the API row as
"Live traffic: X% failed inside the gateway (N requests, last 15 min)". When
the verdict is degraded or down, the API row's pill and the summary banner say
so ("Degraded (live traffic)"), because the checker can be green while real
requests fail (2026-09-05: 24% of requests failed for 35 minutes with
/v1/models answering normally). The uptime percentages and 90-day bars stay on
Upptime's incident data, so the two figures can legitimately disagree: uptime is
"could the API be reached", live traffic is "did requests succeed right now".
- Gateway errors count only failures the gateway owns (terminal classes
internalandunavailable). Customer rejections (quota, invalid request) and upstream provider errors are excluded on purpose: a customer out of credits is not an outage, and a provider incident is reported by the provider. - Baseline is the 7-day median request count for the same 15-minute slot at this hour, so a traffic collapse is detected even when nothing errors.
- Verdicts. Down: 25% or more gateway errors over at least 20 finished requests, zero requests against a baseline of 20 or more, or the database unreachable on two consecutive checks (the gateway's readiness gates on a live database ping, so that is an API outage). Degraded: 5% or more gateway errors, or under a fifth of the baseline volume. Thresholds live at the top of the workflow's Python step.
- Alerts. A non-ok verdict opens one GitHub issue labeled
traffic-alert(assigned to the owner) and posts to Slack; recovery closes the issue and posts again. These issues carry a different label from Upptime'sstatusincidents, so the uptime percentages and 90-day bars keep their single source.
Two independent alert paths, both to the same Slack incoming webhook:
- Checker incidents (Upptime): set both repository secrets together,
NOTIFICATION_SLACK=trueandNOTIFICATION_SLACK_WEBHOOK_URL=<url>. Both names are on thesecretsallowlist in.upptimerc.yml. Upptime posts on down, degraded, and recovery, and still opens the incident issue and assigns the owner (GitHub emails the assignee) whether or not Slack is configured. - Traffic alerts (this repo's workflow): reads the same
NOTIFICATION_SLACK_WEBHOOK_URL; with it unset, the GitHub issue is the alert.
GitHub runs a scheduled workflow only when it has capacity. On this repo the
checker's */5 cron landed about once every two hours (measured 2026-09-04/05),
which would let an eight-minute API outage pass unseen. So the schedule is a
fallback only: a Vercel Cron (vercel/vercel.json, every 5 minutes, Pro plan)
calls /api/cron/uptime on :00/:05/... and /api/cron/traffic two minutes
later (vercel/api/cron/*.js, copied into the served root by
deploy-vercel.yml); each sends one repository_dispatch event, uptime or
traffic-health. The offset matters: both workflows commit to main and
Upptime's checker pushes without rebasing, so firing them together made the
checker lose the push race. Vercel project env (production):
CRON_SECRET (Vercel presents it as the bearer token; anything else is 401) and
GH_DISPATCH_TOKEN (a GitHub token with repo scope on this repo). The local
copy of CRON_SECRET sits next to the probe key in the owner's
~/.gateway-secrets/status-monitor.env.
Known gap: GH_PAT lacks the workflow scope, so Upptime's Setup CI cannot
push regenerated workflows (its push of graphs.yml is rejected). Until the PAT
is re-scoped, SECRETS_CONTEXT in the generated workflows is mirrored by hand
when the secrets allowlist changes.
Gateway (authenticated) is currently DISABLED (commented out in
.upptimerc.yml since 2026-09-07): its probe key was revoked on 2026-09-05 and
no replacement has been minted, so the row would only report a false down.
When enabled, the status-monitor organization on the platform (slug
status-monitor, key named "status-page authenticated probe") holds the
STATUS_GATEWAY_API_KEY secret. Never point this at a customer or
house org key: the key sits in this repo's Actions secrets and is sent from
GitHub runners.
The commented Gateway Completions site (a real completion through the
serving path) additionally needs the status-monitor org funded with a small
credit grant; an unfunded request is a 429, not a 200. Once funded, uncomment
the site and push; Setup CI regenerates the workflows.
Rotate the key like any other production credential (revoke it in the platform,
mint a new one under the same org, gh secret set STATUS_GATEWAY_API_KEY --repo experientiallabs/status); it is referenced only as
$STATUS_GATEWAY_API_KEY in config and never appears in the repo, the page, or
committed history.
Check the probe key before assuming an outage. This row sends
STATUS_GATEWAY_API_KEY to /v1/models and expects 200; a 401 means the
key was rejected, i.e. revoked or rotated without updating this repo's secret
— a monitoring-config problem, not a platform outage. The tell: the API row
(same URL, no auth, expects 401) stays green, and the live-traffic line on the
API row shows real customer requests succeeding. This exact case put the row in
a permanent false down from 2026-09-05: the probe key was revoked during a
manual rotation that never ran gh secret set.
Confirm and fix:
# 1. Is the key actually rejected? (401 = bad/revoked key, not an outage)
curl -s -o /dev/null -w '%{http_code}
' -H "Authorization: Bearer $STATUS_GATEWAY_API_KEY" https://api.experientiallabs.ai/v1/models
# 2. Mint a fresh key under the SAME status-monitor org. Dashboard: sign in as a
# platform admin, open the status-monitor org's API keys, create
# "status-page authenticated probe". Or via the admin API with an xpladmin_ key:
curl -s -X POST https://api.experientiallabs.ai/api/admin/orgs/1a7ba1ee-b847-46b2-a871-e517637ced41/keys -H "Authorization: Bearer $XPLADMIN_KEY" -H 'Content-Type: application/json' -d '{"name":"status-page authenticated probe"}' | jq -r .key
# 3. Store it and re-run the checker; the row recovers and the open incident closes.
gh secret set STATUS_GATEWAY_API_KEY --repo experientiallabs/status --body '<new xpl_ key>'
gh workflow run "Uptime CI" --repo experientiallabs/statusThe status-monitor org (1a7ba1ee-b847-46b2-a871-e517637ced41) is unverified and
memberless by design; a /v1/models listing returns 200 for any live key of
that org regardless of spend-unlock, so a fresh key is all that is needed.
Upptime derives each component's per-day downtime and its uptime percentage from
the open-to-close duration of the status-labelled GitHub incident issue,
not from the individual 5-minute checks. So an issue that stays open after the
service already recovered (a slow auto-close, or one left open by hand) is
counted as continuous downtime — a brief blip whose issue lingered a day reads as
~1,440 minutes down and drags the weekly uptime figure down with it. Keep the
uptime numbers honest by closing recovered incidents promptly: the checker
auto-closes on the next passing run, but verify stale status issues after any
monitoring hiccup rather than leaving them open.
The site is built on GitHub and served by Vercel:
- Static Site CI exports the page onto the
gh-pagesbranch (the artifact branch; keep it). GitHub Pages serving is deliberately NOT used: its Let's Encrypt issuance forstatus.experientiallabs.aiwas terminally stuck inbad_authzdespite verified-correct DNS/CAA, so the custom domain moved to Vercel. - Deploy to Vercel (
deploy-vercel.yml, hand-maintained, not Upptime-generated) runs after every successful Static Site CI (or manually via workflow_dispatch) and ships thegh-pagestree to the Vercel projectstatus(teamexperiential-labs) with a pinnedvercel@59.5.0CLI. Between deploys the page still updates live: uptime numbers and incidents are fetched client-side from the GitHub API. - Deployment protection is disabled on the Vercel project on purpose: a public status page must be reachable by anyone.
- Statuspage-style UI (90-day per-day uptime bars under each component,
and a dated Past Incidents section including "No incidents reported." days)
is one client-side file,
assets/status-ui/uptime-bars.js, which the deploy workflow overlays into the served tree at/ui/(referenced fromcustomHeadHtml). Its data needs no pipeline of its own: the bars read thedailyMinutesDownmap Upptime already computes from incident issues and commits intohistory/summary.json, and the incident list reads the repo'sstatus-labeled issues, the same sources Upptime's own uptime numbers use. Bar colors: green = no downtime, orange = under an hour down, red = an hour or more. ThedailyMinutesDownmap is the single source for both the bars and the legend percentage, so they cannot diverge. Days before monitoring began (2026-08-25) render operational: the owner decided (Aug 2026) that pre-monitoring incident history belongs in the ops timeline, not on this page. The favicon is the vendoredassets/logo.svg/logo-192.png. - DNS: Namecheap CNAME record, host
status, valuecname.vercel-dns.com.(Vercel issues and renews the TLS certificate automatically.)
| Secret | Why |
|---|---|
GH_PAT |
The enterprise forces the default GITHUB_TOKEN to read-only; without a fine-grained PAT (this repo only; Contents + Issues read/write) Upptime cannot commit history, open incident issues, or push gh-pages. |
VERCEL_TOKEN |
Deploys the site; scoped to the experiential-labs Vercel team. |
VERCEL_ORG_ID |
Team id written into .vercel/project.json at deploy time. |
PROD_OPS_AGENT_DB_URL |
Read-only, connection-capped prod role for the server-latency and traffic-health workflows. |
STATUS_GATEWAY_API_KEY |
The status-monitor org's key behind the authenticated gateway probe. |
NOTIFICATION_SLACK, NOTIFICATION_SLACK_WEBHOOK_URL |
Optional, set together: Slack alerts from the checker and from traffic-health. |
- Monitor workflows are pinned to
upptime/uptime-monitor@v1.43.15. update-template.ymlandupdates.ymlare Upptime's own self-update lane (they track upstreammasterby design and rewrite the generated workflows on a daily schedule). To freeze the version entirely, disable those two workflows in the Actions UI and bump the pin manually via a config push.- Response-time history and graphs are committed to this repo by the bot; that is expected and is what powers the history on the page.