Vibecode Instatus
track this build4 phases, 12 steps, beginner friendly0%The page itself is the easy part: components, colored dots, an incident timeline, a 90 day uptime bar; an agent will hand you that in an afternoon. The notification fan-out is where it gets annoying: email is a Resend account and a loop, but you inherit double opt-in, unsubscribes, bounce handling, and explaining why the incident email landed in spam during the outage. Hosting is the other half: a status page on the same infra as the thing it reports on is decoration, so you need a separate host and domain and the discipline to keep it boring. Add the workflow Instatus quietly gives you, writing an update from your phone at 2am, per-component subscriber preferences, Slack and RSS and webhooks. Buildable in a weekend for a personal project; not the thing to hand-roll if a real customer contract mentions the words 'status page'.
You are building a lean indie version of Instatus. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Instatus · indie build A status page and incident communication tool where the interesting constraints are all availability and honesty: independent infrastructure, a page that degrades to static, notifications that go out through a path unaffected by your outage, and a subscriber list that is a liability if handled carelessly. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Hosting | Static generation to object storage with a CDN, on a separate account | a page with no application server has very little that can fail | | Authoring | A small application, deliberately elsewhere, that publishes the static page | the authoring tool going down must not take the page with it | | Checks | External probes from a third party or a separate region | a check running inside the system it monitors reports success during an outage | | Notifications | Email and webhooks through a provider unrelated to your product | notifying subscribers must not depend on the thing that is broken | | Data | A small database for the authoring side only | the published page is files, so the database being down does not affect readers | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the authoring application and the static generator. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A hosting account separate from your product** · pennies per month for static hosting - Why: This is the whole build. A status page on the same account as the product fails at exactly the moment it is needed. - Get it: A second account with a different provider if possible, with its own billing so a payment problem cannot take down both. - Verify: A written list shows no shared dependency between the page and the product - [ ] **A domain or subdomain with independent DNS** · the price of a domain, or free for a delegated subdomain - Why: Sharing a DNS provider with the product means a DNS incident removes both, and DNS incidents are a common cause of total outages. - Get it: A subdomain delegated to a different provider, or a separate domain entirely. - Verify: Resolving the status hostname does not depend on the product's DNS provider - [ ] **A notification provider unrelated to the product** (optional) · free tiers cover a small subscriber list - Why: Notifying subscribers through the mail infrastructure that is currently broken sends nothing. - Get it: Any transactional email provider, with credentials stored somewhere retrievable during an outage. - Verify: A test notification sends and the credentials are retrievable without the product being up - [ ] **What each component status means and who may declare one** · free - Why: During an incident nobody has time to decide whether this is degraded or partial, and inconsistent statuses are what make a page untrustworthy. - Get it: Write one sentence defining each status, and name who may post. Do it before the first incident, not during it. - Verify: A written definition exists for every status level and the page links to it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. - Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test - Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box - Per-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about - The polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API - Real independence, unless you actually do the work to host the page away from your own stack If one of those is essential to you, that is the reason to keep paying for Instatus, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Instatus The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a status page that stays up when everything else is down to replace Instatus. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A status page has exactly one requirement that outranks every feature: it must be available when your product is not. That is an architectural constraint, not an operational aspiration. It means the status page shares nothing with the system it reports on. Not the same hosting account, not the same database, not the same DNS provider if you can avoid it, not the same certificate renewal, and not the same deployment pipeline. If your provider has a bad day and both go down together, you have a status page that works precisely when it is not needed. The corollary is that automatic status must be conservative. A page confidently reporting that all systems are operational while customers cannot log in is worse than no page at all, because it tells people the problem is theirs. Automation may open an incident. It should not close one. ### Stack (fixed, do not substitute) - Hosting: Static generation to object storage with a CDN, on a separate account · a page with no application server has very little that can fail - Authoring: A small application, deliberately elsewhere, that publishes the static page · the authoring tool going down must not take the page with it - Checks: External probes from a third party or a separate region · a check running inside the system it monitors reports success during an outage - Notifications: Email and webhooks through a provider unrelated to your product · notifying subscribers must not depend on the thing that is broken - Data: A small database for the authoring side only · the published page is files, so the database being down does not affect readers ### Data model (create this before Phase 1) - components(id, name, description, ordinal, current_status) · status is operational, degraded, partial or major - incidents(id, title, impact, started_at, resolved_at, created_by) - updates(id, incident_id, status, body, at) · investigating, identified, monitoring or resolved. Append only - incident_components(incident_id, component_id, impact) - maintenance(id, title, scheduled_start, scheduled_end, body, notified_at) - subscribers(id, channel, address_hash, address_encrypted, confirmed_at, unsubscribed_at) · a list of your customers, so treated as sensitive - publishes(id, at, revision, source) · every publish of the static page, so you can prove what it said and when - Updates are append only, because editing what a status page said during an incident is how a company loses the benefit of having one. ### Phase 1 · A page that is nearly impossible to take down Build: Generate the page as static files, publish to object storage behind a CDN, on a separate provider account from your product. Separate DNS and certificate paths. Prove the page survives the authoring application being entirely offline. Trap: Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. Done when: The page serves as static files with no application server in the request path. Stopping the authoring application entirely leaves the page serving, tested by shutting it down. The page is on a separate provider account with independent DNS, verified by listing what each depends on. The page loads with JavaScript disabled. Do not build yet: No incidents, checks or notifications. ### Phase 2 · Incidents, written once and never rewritten Build: Components with statuses, incidents with append-only updates, and a publish on every change. Show the timeline with timestamps. Correcting a mistake adds an update rather than editing the previous one. Trap: Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. Done when: An incident posts and publishes within seconds. Updates are append only and a correction appears as a new update, verified by attempting to edit one. The timeline shows every update with its time. A resolved incident remains in the history permanently. Do not build yet: No checks or notifications yet. ### Phase 3 · Checks that can see what customers see Build: External checks from a separate region or provider against real user-facing endpoints. Require several consecutive failures before acting. Allow automation to open an incident and change a component to degraded, never to resolve one. Trap: A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. Done when: Checks run from outside the monitored infrastructure, verified by listing what the prober depends on. Several consecutive failures are required before an incident opens, tested with a single injected failure producing nothing. Automation can open but never resolve, verified by attempting it. A check failing because the prober itself is broken is distinguishable. Do not build yet: No subscriber notifications yet. ### Phase 4 · Telling people, through a path that still works Build: Email and webhook notifications through a provider unrelated to the product. Double opt-in with confirmation, one-click unsubscribe, and per-component subscription. Rate limit so a flapping component cannot send twenty messages. Trap: Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. Done when: Notifications send through a provider unrelated to the product, verified by listing the dependency. Double opt-in is required and unsubscribing works in one click. A component changing status repeatedly does not send more than the rate limit allows, tested by flapping it. Subscriber addresses are encrypted at rest. Do not build yet: Maintenance and export come last. ### Phase 5 · Maintenance, uptime figures and proving what you said Build: Scheduled maintenance announced in advance and reflected on the page. Uptime computed from incident records with the method stated. Keep every published revision so you can show what the page said at any past moment. Trap: An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. Done when: Maintenance announces in advance and appears on the page before it starts. Uptime is computed from incidents with the method stated beside the figure. Every publish is retained and a past state can be shown, tested by retrieving one. Exporting incident history produces a complete record. Do not build yet: Nothing after this. ### Out of scope (and why) - Deep integrations with monitoring platforms, which is most of what the hosted products actually sell. - Multi-tenant hosting for other companies, which turns an internal tool into a product with an availability obligation to strangers. - SMS and phone notifications, which carry per-message cost and carrier complications well beyond email. - Private status pages with per-customer authentication, which reintroduces an application server into the read path. ### README must contain - That the page shares no account, DNS or deployment path with the product it reports on - That the page is static and serves with the authoring application entirely offline - That automation may open an incident but never resolve one - That incident updates are append only and corrections are visible - That uptime states its calculation method and what it excludes ===== AGENTS.md ===== # Agent instructions · Instatus indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Static generation to object storage with a CDN, on a separate account, A small application, deliberately elsewhere, that publishes the static page, External probes from a third party or a separate region, Email and webhooks through a provider unrelated to your product, A small database for the authoring side only. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ===== BUILD_PLAN.md ===== # Build plan · Instatus A status page and incident communication tool where the interesting constraints are all availability and honesty: independent infrastructure, a page that degrades to static, notifications that go out through a path unaffected by your outage, and a subscriber list that is a liability if handled carelessly. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A page that is nearly impossible to take down The status page is served as static files from infrastructure you share with nothing. ### Steps 1. Generate static and publish to storage No application server in the read path. Almost nothing left to break. Files: `src/publish.ts` 2. Separate the account, DNS and certificates Write down every shared dependency and remove the ones you can. Files: `docs/independence.md` 3. Prove independence by shutting the authoring side down The page must keep serving. Test it rather than assuming it. Files: `tests/independence.md` ### Done when - [ ] The page serves as static files with no server - [ ] Stopping the authoring application leaves it serving - [ ] Account, DNS and certificates are independent - [ ] The page works with JavaScript disabled ### Watch out - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. ## Phase 2 · Incidents, written once and never rewritten An incident is posted, updated as it develops, and the history is permanent. ### Steps 1. Model updates as append only Enforced in the schema, not by convention. Files: `src/incidents.ts` 2. Publish on every change An incident update that has not published has not happened. Files: `src/publish.ts` 3. Keep resolved incidents visible The history is the point. Hiding it is hiding your record. Files: `src/history.ts` ### Done when - [ ] An incident publishes within seconds - [ ] Updates are append only and corrections are visible - [ ] The timeline shows every update with its time - [ ] Resolved incidents stay in the history ### Watch out - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. ## Phase 3 · Checks that can see what customers see Automated probes run from outside and can open an incident but never close one. ### Steps 1. Probe from outside, against real endpoints A health endpoint returning true is not what customers experience. Files: `src/checks.ts` 2. Require consecutive failures One failure is noise. Acting on it produces a page nobody trusts. Files: `src/checks.ts` 3. Never let automation resolve Only a person closes an incident. A premature all-clear is worse than the outage. Files: `src/automation.ts`, `src/automation.test.ts` ### Done when - [ ] Checks run from outside the monitored system - [ ] A single failure produces no incident - [ ] Automation can open but not resolve - [ ] A broken prober is distinguishable from a real failure ### Watch out - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. ## Phase 4 · Telling people, through a path that still works Subscribers hear about the incident even though your product is down. ### Steps 1. Use an unrelated notification provider With credentials retrievable without your product being up. Files: `src/notify.ts` 2. Double opt-in and one-click unsubscribe A subscriber list is a list of your customers. Treat it accordingly. Files: `src/subscribers.ts` 3. Rate limit per component A flapping check must not send twenty emails. That is how people unsubscribe. Files: `src/ratelimit.ts` ### Done when - [ ] Notifications use an unrelated provider - [ ] Double opt-in and one-click unsubscribe work - [ ] A flapping component respects the rate limit - [ ] Subscriber addresses are encrypted at rest ### Watch out - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ## Not in this build - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. ## After v1, if you want it - A machine-readable status endpoint, which is what other people's tooling wants - Incident templates, since nobody writes well at three in the morning - Post-incident notes attached to the incident, which is where a status page earns trust ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Authoring side only. The published page does not read it, which is the point. DATABASE_URL=postgres://localhost:5432/status # Required. Where the static page is published. On the separate account. PUBLISH_BUCKET=s3://status-example-com # Required · secret. Scoped to the bucket only, and belonging to the account that shares nothing with the product. PUBLISH_CREDENTIALS=a key for the separate account # Optional · secret. The unrelated notification provider. Store it where you can reach it during an outage. NOTIFY_PROVIDER_KEY=a provider key # Required. Consecutive failures before automation opens an incident. One failure is noise. CHECK_FAILURES_BEFORE_INCIDENT=3 # Required. Minimum gap between notifications for one component, so flapping does not spam. NOTIFY_RATE_LIMIT_MINUTES=15
You are building a lean indie version of Instatus. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Instatus · indie build A status page and incident communication tool where the interesting constraints are all availability and honesty: independent infrastructure, a page that degrades to static, notifications that go out through a path unaffected by your outage, and a subscriber list that is a liability if handled carelessly. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Hosting | Static generation to object storage with a CDN, on a separate account | a page with no application server has very little that can fail | | Authoring | A small application, deliberately elsewhere, that publishes the static page | the authoring tool going down must not take the page with it | | Checks | External probes from a third party or a separate region | a check running inside the system it monitors reports success during an outage | | Notifications | Email and webhooks through a provider unrelated to your product | notifying subscribers must not depend on the thing that is broken | | Data | A small database for the authoring side only | the published page is files, so the database being down does not affect readers | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the authoring application and the static generator. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A hosting account separate from your product** · pennies per month for static hosting - Why: This is the whole build. A status page on the same account as the product fails at exactly the moment it is needed. - Get it: A second account with a different provider if possible, with its own billing so a payment problem cannot take down both. - Verify: A written list shows no shared dependency between the page and the product - [ ] **A domain or subdomain with independent DNS** · the price of a domain, or free for a delegated subdomain - Why: Sharing a DNS provider with the product means a DNS incident removes both, and DNS incidents are a common cause of total outages. - Get it: A subdomain delegated to a different provider, or a separate domain entirely. - Verify: Resolving the status hostname does not depend on the product's DNS provider - [ ] **A notification provider unrelated to the product** (optional) · free tiers cover a small subscriber list - Why: Notifying subscribers through the mail infrastructure that is currently broken sends nothing. - Get it: Any transactional email provider, with credentials stored somewhere retrievable during an outage. - Verify: A test notification sends and the credentials are retrievable without the product being up - [ ] **What each component status means and who may declare one** · free - Why: During an incident nobody has time to decide whether this is degraded or partial, and inconsistent statuses are what make a page untrustworthy. - Get it: Write one sentence defining each status, and name who may post. Do it before the first incident, not during it. - Verify: A written definition exists for every status level and the page links to it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. - Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test - Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box - Per-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about - The polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API - Real independence, unless you actually do the work to host the page away from your own stack If one of those is essential to you, that is the reason to keep paying for Instatus, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Instatus The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a status page that stays up when everything else is down to replace Instatus. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A status page has exactly one requirement that outranks every feature: it must be available when your product is not. That is an architectural constraint, not an operational aspiration. It means the status page shares nothing with the system it reports on. Not the same hosting account, not the same database, not the same DNS provider if you can avoid it, not the same certificate renewal, and not the same deployment pipeline. If your provider has a bad day and both go down together, you have a status page that works precisely when it is not needed. The corollary is that automatic status must be conservative. A page confidently reporting that all systems are operational while customers cannot log in is worse than no page at all, because it tells people the problem is theirs. Automation may open an incident. It should not close one. ### Stack (fixed, do not substitute) - Hosting: Static generation to object storage with a CDN, on a separate account · a page with no application server has very little that can fail - Authoring: A small application, deliberately elsewhere, that publishes the static page · the authoring tool going down must not take the page with it - Checks: External probes from a third party or a separate region · a check running inside the system it monitors reports success during an outage - Notifications: Email and webhooks through a provider unrelated to your product · notifying subscribers must not depend on the thing that is broken - Data: A small database for the authoring side only · the published page is files, so the database being down does not affect readers ### Data model (create this before Phase 1) - components(id, name, description, ordinal, current_status) · status is operational, degraded, partial or major - incidents(id, title, impact, started_at, resolved_at, created_by) - updates(id, incident_id, status, body, at) · investigating, identified, monitoring or resolved. Append only - incident_components(incident_id, component_id, impact) - maintenance(id, title, scheduled_start, scheduled_end, body, notified_at) - subscribers(id, channel, address_hash, address_encrypted, confirmed_at, unsubscribed_at) · a list of your customers, so treated as sensitive - publishes(id, at, revision, source) · every publish of the static page, so you can prove what it said and when - Updates are append only, because editing what a status page said during an incident is how a company loses the benefit of having one. ### Phase 1 · A page that is nearly impossible to take down Build: Generate the page as static files, publish to object storage behind a CDN, on a separate provider account from your product. Separate DNS and certificate paths. Prove the page survives the authoring application being entirely offline. Trap: Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. Done when: The page serves as static files with no application server in the request path. Stopping the authoring application entirely leaves the page serving, tested by shutting it down. The page is on a separate provider account with independent DNS, verified by listing what each depends on. The page loads with JavaScript disabled. Do not build yet: No incidents, checks or notifications. ### Phase 2 · Incidents, written once and never rewritten Build: Components with statuses, incidents with append-only updates, and a publish on every change. Show the timeline with timestamps. Correcting a mistake adds an update rather than editing the previous one. Trap: Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. Done when: An incident posts and publishes within seconds. Updates are append only and a correction appears as a new update, verified by attempting to edit one. The timeline shows every update with its time. A resolved incident remains in the history permanently. Do not build yet: No checks or notifications yet. ### Phase 3 · Checks that can see what customers see Build: External checks from a separate region or provider against real user-facing endpoints. Require several consecutive failures before acting. Allow automation to open an incident and change a component to degraded, never to resolve one. Trap: A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. Done when: Checks run from outside the monitored infrastructure, verified by listing what the prober depends on. Several consecutive failures are required before an incident opens, tested with a single injected failure producing nothing. Automation can open but never resolve, verified by attempting it. A check failing because the prober itself is broken is distinguishable. Do not build yet: No subscriber notifications yet. ### Phase 4 · Telling people, through a path that still works Build: Email and webhook notifications through a provider unrelated to the product. Double opt-in with confirmation, one-click unsubscribe, and per-component subscription. Rate limit so a flapping component cannot send twenty messages. Trap: Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. Done when: Notifications send through a provider unrelated to the product, verified by listing the dependency. Double opt-in is required and unsubscribing works in one click. A component changing status repeatedly does not send more than the rate limit allows, tested by flapping it. Subscriber addresses are encrypted at rest. Do not build yet: Maintenance and export come last. ### Phase 5 · Maintenance, uptime figures and proving what you said Build: Scheduled maintenance announced in advance and reflected on the page. Uptime computed from incident records with the method stated. Keep every published revision so you can show what the page said at any past moment. Trap: An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. Done when: Maintenance announces in advance and appears on the page before it starts. Uptime is computed from incidents with the method stated beside the figure. Every publish is retained and a past state can be shown, tested by retrieving one. Exporting incident history produces a complete record. Do not build yet: Nothing after this. ### Out of scope (and why) - Deep integrations with monitoring platforms, which is most of what the hosted products actually sell. - Multi-tenant hosting for other companies, which turns an internal tool into a product with an availability obligation to strangers. - SMS and phone notifications, which carry per-message cost and carrier complications well beyond email. - Private status pages with per-customer authentication, which reintroduces an application server into the read path. ### README must contain - That the page shares no account, DNS or deployment path with the product it reports on - That the page is static and serves with the authoring application entirely offline - That automation may open an incident but never resolve one - That incident updates are append only and corrections are visible - That uptime states its calculation method and what it excludes ===== AGENTS.md ===== # Agent instructions · Instatus indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Static generation to object storage with a CDN, on a separate account, A small application, deliberately elsewhere, that publishes the static page, External probes from a third party or a separate region, Email and webhooks through a provider unrelated to your product, A small database for the authoring side only. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ===== BUILD_PLAN.md ===== # Build plan · Instatus A status page and incident communication tool where the interesting constraints are all availability and honesty: independent infrastructure, a page that degrades to static, notifications that go out through a path unaffected by your outage, and a subscriber list that is a liability if handled carelessly. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A page that is nearly impossible to take down The status page is served as static files from infrastructure you share with nothing. ### Steps 1. Generate static and publish to storage No application server in the read path. Almost nothing left to break. Files: `src/publish.ts` 2. Separate the account, DNS and certificates Write down every shared dependency and remove the ones you can. Files: `docs/independence.md` 3. Prove independence by shutting the authoring side down The page must keep serving. Test it rather than assuming it. Files: `tests/independence.md` ### Done when - [ ] The page serves as static files with no server - [ ] Stopping the authoring application leaves it serving - [ ] Account, DNS and certificates are independent - [ ] The page works with JavaScript disabled ### Watch out - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. ## Phase 2 · Incidents, written once and never rewritten An incident is posted, updated as it develops, and the history is permanent. ### Steps 1. Model updates as append only Enforced in the schema, not by convention. Files: `src/incidents.ts` 2. Publish on every change An incident update that has not published has not happened. Files: `src/publish.ts` 3. Keep resolved incidents visible The history is the point. Hiding it is hiding your record. Files: `src/history.ts` ### Done when - [ ] An incident publishes within seconds - [ ] Updates are append only and corrections are visible - [ ] The timeline shows every update with its time - [ ] Resolved incidents stay in the history ### Watch out - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. ## Phase 3 · Checks that can see what customers see Automated probes run from outside and can open an incident but never close one. ### Steps 1. Probe from outside, against real endpoints A health endpoint returning true is not what customers experience. Files: `src/checks.ts` 2. Require consecutive failures One failure is noise. Acting on it produces a page nobody trusts. Files: `src/checks.ts` 3. Never let automation resolve Only a person closes an incident. A premature all-clear is worse than the outage. Files: `src/automation.ts`, `src/automation.test.ts` ### Done when - [ ] Checks run from outside the monitored system - [ ] A single failure produces no incident - [ ] Automation can open but not resolve - [ ] A broken prober is distinguishable from a real failure ### Watch out - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. ## Phase 4 · Telling people, through a path that still works Subscribers hear about the incident even though your product is down. ### Steps 1. Use an unrelated notification provider With credentials retrievable without your product being up. Files: `src/notify.ts` 2. Double opt-in and one-click unsubscribe A subscriber list is a list of your customers. Treat it accordingly. Files: `src/subscribers.ts` 3. Rate limit per component A flapping check must not send twenty emails. That is how people unsubscribe. Files: `src/ratelimit.ts` ### Done when - [ ] Notifications use an unrelated provider - [ ] Double opt-in and one-click unsubscribe work - [ ] A flapping component respects the rate limit - [ ] Subscriber addresses are encrypted at rest ### Watch out - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ## Not in this build - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. ## After v1, if you want it - A machine-readable status endpoint, which is what other people's tooling wants - Incident templates, since nobody writes well at three in the morning - Post-incident notes attached to the incident, which is where a status page earns trust ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Authoring side only. The published page does not read it, which is the point. DATABASE_URL=postgres://localhost:5432/status # Required. Where the static page is published. On the separate account. PUBLISH_BUCKET=s3://status-example-com # Required · secret. Scoped to the bucket only, and belonging to the account that shares nothing with the product. PUBLISH_CREDENTIALS=a key for the separate account # Optional · secret. The unrelated notification provider. Store it where you can reach it during an outage. NOTIFY_PROVIDER_KEY=a provider key # Required. Consecutive failures before automation opens an incident. One failure is noise. CHECK_FAILURES_BEFORE_INCIDENT=3 # Required. Minimum gap between notifications for one component, so flapping does not spam. NOTIFY_RATE_LIMIT_MINUTES=15
You are building a production product version of Instatus. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Instatus · product brief ## Problem The page itself is the easy part: components, colored dots, an incident timeline, a 90 day uptime bar; an agent will hand you that in an afternoon. The notification fan-out is where it gets annoying: email is a Resend account and a loop, but you inherit double opt-in, unsubscribes, bounce handling, and explaining why the incident email landed in spam during the outage. Hosting is the other half: a status page on the same infra as the thing it reports on is decoration, so you need a separate host and domain and the discipline to keep it boring. Add the workflow Instatus quietly gives you, writing an update from your phone at 2am, per-component subscriber preferences, Slack and RSS and webhooks. Buildable in a weekend for a personal project; not the thing to hand-roll if a real customer contract mentions the words 'status page'. ## Product outcome A status page that is still serving when everything else is down, whose history cannot be quietly rewritten, and which never tells customers everything is fine while they cannot log in. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - A host that is not the infrastructure you are reporting on, ideally a different provider entirely - A separate domain or subdomain on separate DNS - A transactional email provider with a verified sending domain, e.g. Resend or Postmark - Somewhere to store subscribers that survives a reboot ## Explicit non-goals for v1 - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. - Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test - Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box - Per-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about - The polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API - Real independence, unless you actually do the work to host the page away from your own stack ## Success criteria - The page is confirmed to serve with the authoring application stopped - No shared dependency with the product is confirmed by review - Automation is confirmed unable to resolve an incident - A single check failure is confirmed to produce no incident - Incident updates are confirmed append only ===== BRIEF.md ===== # Build brief · Instatus The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a status page that stays up when everything else is down to replace Instatus. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A status page has exactly one requirement that outranks every feature: it must be available when your product is not. That is an architectural constraint, not an operational aspiration. It means the status page shares nothing with the system it reports on. Not the same hosting account, not the same database, not the same DNS provider if you can avoid it, not the same certificate renewal, and not the same deployment pipeline. If your provider has a bad day and both go down together, you have a status page that works precisely when it is not needed. The corollary is that automatic status must be conservative. A page confidently reporting that all systems are operational while customers cannot log in is worse than no page at all, because it tells people the problem is theirs. Automation may open an incident. It should not close one. ### Stack (fixed, do not substitute) - Hosting: Static generation to object storage with a CDN, on a separate account · a page with no application server has very little that can fail - Authoring: A small application, deliberately elsewhere, that publishes the static page · the authoring tool going down must not take the page with it - Checks: External probes from a third party or a separate region · a check running inside the system it monitors reports success during an outage - Notifications: Email and webhooks through a provider unrelated to your product · notifying subscribers must not depend on the thing that is broken - Data: A small database for the authoring side only · the published page is files, so the database being down does not affect readers ### Data model (create this before Phase 1) - components(id, name, description, ordinal, current_status) · status is operational, degraded, partial or major - incidents(id, title, impact, started_at, resolved_at, created_by) - updates(id, incident_id, status, body, at) · investigating, identified, monitoring or resolved. Append only - incident_components(incident_id, component_id, impact) - maintenance(id, title, scheduled_start, scheduled_end, body, notified_at) - subscribers(id, channel, address_hash, address_encrypted, confirmed_at, unsubscribed_at) · a list of your customers, so treated as sensitive - publishes(id, at, revision, source) · every publish of the static page, so you can prove what it said and when - Updates are append only, because editing what a status page said during an incident is how a company loses the benefit of having one. ### Phase 1 · A page that is nearly impossible to take down Build: Generate the page as static files, publish to object storage behind a CDN, on a separate provider account from your product. Separate DNS and certificate paths. Prove the page survives the authoring application being entirely offline. Trap: Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. Done when: The page serves as static files with no application server in the request path. Stopping the authoring application entirely leaves the page serving, tested by shutting it down. The page is on a separate provider account with independent DNS, verified by listing what each depends on. The page loads with JavaScript disabled. Do not build yet: No incidents, checks or notifications. ### Phase 2 · Incidents, written once and never rewritten Build: Components with statuses, incidents with append-only updates, and a publish on every change. Show the timeline with timestamps. Correcting a mistake adds an update rather than editing the previous one. Trap: Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. Done when: An incident posts and publishes within seconds. Updates are append only and a correction appears as a new update, verified by attempting to edit one. The timeline shows every update with its time. A resolved incident remains in the history permanently. Do not build yet: No checks or notifications yet. ### Phase 3 · Checks that can see what customers see Build: External checks from a separate region or provider against real user-facing endpoints. Require several consecutive failures before acting. Allow automation to open an incident and change a component to degraded, never to resolve one. Trap: A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. Done when: Checks run from outside the monitored infrastructure, verified by listing what the prober depends on. Several consecutive failures are required before an incident opens, tested with a single injected failure producing nothing. Automation can open but never resolve, verified by attempting it. A check failing because the prober itself is broken is distinguishable. Do not build yet: No subscriber notifications yet. ### Phase 4 · Telling people, through a path that still works Build: Email and webhook notifications through a provider unrelated to the product. Double opt-in with confirmation, one-click unsubscribe, and per-component subscription. Rate limit so a flapping component cannot send twenty messages. Trap: Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. Done when: Notifications send through a provider unrelated to the product, verified by listing the dependency. Double opt-in is required and unsubscribing works in one click. A component changing status repeatedly does not send more than the rate limit allows, tested by flapping it. Subscriber addresses are encrypted at rest. Do not build yet: Maintenance and export come last. ### Phase 5 · Maintenance, uptime figures and proving what you said Build: Scheduled maintenance announced in advance and reflected on the page. Uptime computed from incident records with the method stated. Keep every published revision so you can show what the page said at any past moment. Trap: An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. Done when: Maintenance announces in advance and appears on the page before it starts. Uptime is computed from incidents with the method stated beside the figure. Every publish is retained and a past state can be shown, tested by retrieving one. Exporting incident history produces a complete record. Do not build yet: Nothing after this. ### Out of scope (and why) - Deep integrations with monitoring platforms, which is most of what the hosted products actually sell. - Multi-tenant hosting for other companies, which turns an internal tool into a product with an availability obligation to strangers. - SMS and phone notifications, which carry per-message cost and carrier complications well beyond email. - Private status pages with per-customer authentication, which reintroduces an application server into the read path. ### README must contain - That the page shares no account, DNS or deployment path with the product it reports on - That the page is static and serves with the authoring application entirely offline - That automation may open an incident but never resolve one - That incident updates are append only and corrections are visible - That uptime states its calculation method and what it excludes ===== ARCHITECTURE.md ===== # Architecture · Instatus ## Stack | Part | Choice | Why | | --- | --- | --- | | Hosting | Static generation to object storage with a CDN, on a separate account | a page with no application server has very little that can fail | | Authoring | A small application, deliberately elsewhere, that publishes the static page | the authoring tool going down must not take the page with it | | Checks | External probes from a third party or a separate region | a check running inside the system it monitors reports success during an outage | | Notifications | Email and webhooks through a provider unrelated to your product | notifying subscribers must not depend on the thing that is broken | | Data | A small database for the authoring side only | the published page is files, so the database being down does not affect readers | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Publishing | Static generation to independent hosting. | Any static host, provided it shares no account or dependency with the product. | | Incidents | Append-only updates and permanent history. | Any store. Append-only is enforced in the schema rather than by convention. | | Checks | External probing with consecutive-failure thresholds. | Any prober outside the monitored system. Never one inside it. | | Notifications | Delivery through an unrelated provider. | Any provider, provided it is not the one your product depends on. | | Uptime | Computation with a stated method. | Any method, provided it is stated beside the number including exclusions. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Authoring side only. The published page does not read it, which is the point. - `PUBLISH_BUCKET` · required · Where the static page is published. On the separate account. - `PUBLISH_CREDENTIALS` · required, secret · Scoped to the bucket only, and belonging to the account that shares nothing with the product. - `NOTIFY_PROVIDER_KEY` · optional, secret · The unrelated notification provider. Store it where you can reach it during an outage. - `CHECK_FAILURES_BEFORE_INCIDENT` · required · Consecutive failures before automation opens an incident. One failure is noise. - `NOTIFY_RATE_LIMIT_MINUTES` · required · Minimum gap between notifications for one component, so flapping does not spam. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Instatus product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Static generation to object storage with a CDN, on a separate account, A small application, deliberately elsewhere, that publishes the static page, External probes from a third party or a separate region, Email and webhooks through a provider unrelated to your product, A small database for the authoring side only. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. - An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. ===== MILESTONES.md ===== # Delivery milestones · Instatus Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A page that is nearly impossible to take down The status page is served as static files from infrastructure you share with nothing. ### Steps 1. Generate static and publish to storage No application server in the read path. Almost nothing left to break. Files: `src/publish.ts` 2. Separate the account, DNS and certificates Write down every shared dependency and remove the ones you can. Files: `docs/independence.md` 3. Prove independence by shutting the authoring side down The page must keep serving. Test it rather than assuming it. Files: `tests/independence.md` ### Done when - [ ] The page serves as static files with no server - [ ] Stopping the authoring application leaves it serving - [ ] Account, DNS and certificates are independent - [ ] The page works with JavaScript disabled ### Watch out - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. ## M2 · Incidents, written once and never rewritten An incident is posted, updated as it develops, and the history is permanent. ### Steps 1. Model updates as append only Enforced in the schema, not by convention. Files: `src/incidents.ts` 2. Publish on every change An incident update that has not published has not happened. Files: `src/publish.ts` 3. Keep resolved incidents visible The history is the point. Hiding it is hiding your record. Files: `src/history.ts` ### Done when - [ ] An incident publishes within seconds - [ ] Updates are append only and corrections are visible - [ ] The timeline shows every update with its time - [ ] Resolved incidents stay in the history ### Watch out - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. ## M3 · Checks that can see what customers see Automated probes run from outside and can open an incident but never close one. ### Steps 1. Probe from outside, against real endpoints A health endpoint returning true is not what customers experience. Files: `src/checks.ts` 2. Require consecutive failures One failure is noise. Acting on it produces a page nobody trusts. Files: `src/checks.ts` 3. Never let automation resolve Only a person closes an incident. A premature all-clear is worse than the outage. Files: `src/automation.ts`, `src/automation.test.ts` ### Done when - [ ] Checks run from outside the monitored system - [ ] A single failure produces no incident - [ ] Automation can open but not resolve - [ ] A broken prober is distinguishable from a real failure ### Watch out - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. ## M4 · Telling people, through a path that still works Subscribers hear about the incident even though your product is down. ### Steps 1. Use an unrelated notification provider With credentials retrievable without your product being up. Files: `src/notify.ts` 2. Double opt-in and one-click unsubscribe A subscriber list is a list of your customers. Treat it accordingly. Files: `src/subscribers.ts` 3. Rate limit per component A flapping check must not send twenty emails. That is how people unsubscribe. Files: `src/ratelimit.ts` ### Done when - [ ] Notifications use an unrelated provider - [ ] Double opt-in and one-click unsubscribe work - [ ] A flapping component respects the rate limit - [ ] Subscriber addresses are encrypted at rest ### Watch out - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ## M5 · Maintenance, uptime figures and proving what you said (production only) Scheduled work is announced, uptime is computed honestly, and history is provable. ### Steps 1. Announce maintenance ahead of time With notification, since unannounced maintenance is indistinguishable from an outage. Files: `src/maintenance.ts` 2. State the uptime method beside the number Including what is excluded. An unqualified percentage is marketing. Files: `src/uptime.ts` 3. Retain every published revision So you can show what the page said during an incident. Files: `src/publishes.ts` ### Done when - [ ] Maintenance announces and appears before it starts - [ ] Uptime states its method and exclusions - [ ] A past published state can be retrieved - [ ] Incident history exports completely ### Watch out - An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. ===== OPERATIONS.md ===== # Operations · Instatus ## Backup Incident history is the record you may need to produce months later. Back up the authoring database and keep every published revision, since those are what prove what the page said at a given moment. ## Restore Restore and confirm the incident history and publish revisions are complete. A restore that loses updates leaves a history that reads as though less was communicated than actually was. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monitor the status page itself, from somewhere else again, since a status page that is down has no way to tell you. Also watch publish success, because an incident update that failed to publish is invisible. ## Incident checklist The failure that matters is the page being down during an outage, and the cause is almost always a dependency shared with the product that nobody wrote down. The independence document is the control and should be reviewed after every provider change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The page is confirmed to serve with the authoring application stopped - [ ] No shared dependency with the product is confirmed by review - [ ] Automation is confirmed unable to resolve an incident - [ ] A single check failure is confirmed to produce no incident - [ ] Incident updates are confirmed append only ## Launch constraint Do not market omitted Instatus capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Authoring side only. The published page does not read it, which is the point. DATABASE_URL=postgres://localhost:5432/status # Required. Where the static page is published. On the separate account. PUBLISH_BUCKET=s3://status-example-com # Required · secret. Scoped to the bucket only, and belonging to the account that shares nothing with the product. PUBLISH_CREDENTIALS=a key for the separate account # Optional · secret. The unrelated notification provider. Store it where you can reach it during an outage. NOTIFY_PROVIDER_KEY=a provider key # Required. Consecutive failures before automation opens an incident. One failure is noise. CHECK_FAILURES_BEFORE_INCIDENT=3 # Required. Minimum gap between notifications for one component, so flapping does not spam. NOTIFY_RATE_LIMIT_MINUTES=15
# Instatus · indie build A status page and incident communication tool where the interesting constraints are all availability and honesty: independent infrastructure, a page that degrades to static, notifications that go out through a path unaffected by your outage, and a subscriber list that is a liability if handled carelessly. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Hosting | Static generation to object storage with a CDN, on a separate account | a page with no application server has very little that can fail | | Authoring | A small application, deliberately elsewhere, that publishes the static page | the authoring tool going down must not take the page with it | | Checks | External probes from a third party or a separate region | a check running inside the system it monitors reports success during an outage | | Notifications | Email and webhooks through a provider unrelated to your product | notifying subscribers must not depend on the thing that is broken | | Data | A small database for the authoring side only | the published page is files, so the database being down does not affect readers | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the authoring application and the static generator. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A hosting account separate from your product** · pennies per month for static hosting - Why: This is the whole build. A status page on the same account as the product fails at exactly the moment it is needed. - Get it: A second account with a different provider if possible, with its own billing so a payment problem cannot take down both. - Verify: A written list shows no shared dependency between the page and the product - [ ] **A domain or subdomain with independent DNS** · the price of a domain, or free for a delegated subdomain - Why: Sharing a DNS provider with the product means a DNS incident removes both, and DNS incidents are a common cause of total outages. - Get it: A subdomain delegated to a different provider, or a separate domain entirely. - Verify: Resolving the status hostname does not depend on the product's DNS provider - [ ] **A notification provider unrelated to the product** (optional) · free tiers cover a small subscriber list - Why: Notifying subscribers through the mail infrastructure that is currently broken sends nothing. - Get it: Any transactional email provider, with credentials stored somewhere retrievable during an outage. - Verify: A test notification sends and the credentials are retrievable without the product being up - [ ] **What each component status means and who may declare one** · free - Why: During an incident nobody has time to decide whether this is degraded or partial, and inconsistent statuses are what make a page untrustworthy. - Get it: Write one sentence defining each status, and name who may post. Do it before the first incident, not during it. - Verify: A written definition exists for every status level and the page links to it ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. - Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test - Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box - Per-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about - The polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API - Real independence, unless you actually do the work to host the page away from your own stack If one of those is essential to you, that is the reason to keep paying for Instatus, and the README should say so rather than pretend.
# Build brief · Instatus The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a status page that stays up when everything else is down to replace Instatus. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A status page has exactly one requirement that outranks every feature: it must be available when your product is not. That is an architectural constraint, not an operational aspiration. It means the status page shares nothing with the system it reports on. Not the same hosting account, not the same database, not the same DNS provider if you can avoid it, not the same certificate renewal, and not the same deployment pipeline. If your provider has a bad day and both go down together, you have a status page that works precisely when it is not needed. The corollary is that automatic status must be conservative. A page confidently reporting that all systems are operational while customers cannot log in is worse than no page at all, because it tells people the problem is theirs. Automation may open an incident. It should not close one. ### Stack (fixed, do not substitute) - Hosting: Static generation to object storage with a CDN, on a separate account · a page with no application server has very little that can fail - Authoring: A small application, deliberately elsewhere, that publishes the static page · the authoring tool going down must not take the page with it - Checks: External probes from a third party or a separate region · a check running inside the system it monitors reports success during an outage - Notifications: Email and webhooks through a provider unrelated to your product · notifying subscribers must not depend on the thing that is broken - Data: A small database for the authoring side only · the published page is files, so the database being down does not affect readers ### Data model (create this before Phase 1) - components(id, name, description, ordinal, current_status) · status is operational, degraded, partial or major - incidents(id, title, impact, started_at, resolved_at, created_by) - updates(id, incident_id, status, body, at) · investigating, identified, monitoring or resolved. Append only - incident_components(incident_id, component_id, impact) - maintenance(id, title, scheduled_start, scheduled_end, body, notified_at) - subscribers(id, channel, address_hash, address_encrypted, confirmed_at, unsubscribed_at) · a list of your customers, so treated as sensitive - publishes(id, at, revision, source) · every publish of the static page, so you can prove what it said and when - Updates are append only, because editing what a status page said during an incident is how a company loses the benefit of having one. ### Phase 1 · A page that is nearly impossible to take down Build: Generate the page as static files, publish to object storage behind a CDN, on a separate provider account from your product. Separate DNS and certificate paths. Prove the page survives the authoring application being entirely offline. Trap: Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. Done when: The page serves as static files with no application server in the request path. Stopping the authoring application entirely leaves the page serving, tested by shutting it down. The page is on a separate provider account with independent DNS, verified by listing what each depends on. The page loads with JavaScript disabled. Do not build yet: No incidents, checks or notifications. ### Phase 2 · Incidents, written once and never rewritten Build: Components with statuses, incidents with append-only updates, and a publish on every change. Show the timeline with timestamps. Correcting a mistake adds an update rather than editing the previous one. Trap: Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. Done when: An incident posts and publishes within seconds. Updates are append only and a correction appears as a new update, verified by attempting to edit one. The timeline shows every update with its time. A resolved incident remains in the history permanently. Do not build yet: No checks or notifications yet. ### Phase 3 · Checks that can see what customers see Build: External checks from a separate region or provider against real user-facing endpoints. Require several consecutive failures before acting. Allow automation to open an incident and change a component to degraded, never to resolve one. Trap: A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. Done when: Checks run from outside the monitored infrastructure, verified by listing what the prober depends on. Several consecutive failures are required before an incident opens, tested with a single injected failure producing nothing. Automation can open but never resolve, verified by attempting it. A check failing because the prober itself is broken is distinguishable. Do not build yet: No subscriber notifications yet. ### Phase 4 · Telling people, through a path that still works Build: Email and webhook notifications through a provider unrelated to the product. Double opt-in with confirmation, one-click unsubscribe, and per-component subscription. Rate limit so a flapping component cannot send twenty messages. Trap: Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. Done when: Notifications send through a provider unrelated to the product, verified by listing the dependency. Double opt-in is required and unsubscribing works in one click. A component changing status repeatedly does not send more than the rate limit allows, tested by flapping it. Subscriber addresses are encrypted at rest. Do not build yet: Maintenance and export come last. ### Phase 5 · Maintenance, uptime figures and proving what you said Build: Scheduled maintenance announced in advance and reflected on the page. Uptime computed from incident records with the method stated. Keep every published revision so you can show what the page said at any past moment. Trap: An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. Done when: Maintenance announces in advance and appears on the page before it starts. Uptime is computed from incidents with the method stated beside the figure. Every publish is retained and a past state can be shown, tested by retrieving one. Exporting incident history produces a complete record. Do not build yet: Nothing after this. ### Out of scope (and why) - Deep integrations with monitoring platforms, which is most of what the hosted products actually sell. - Multi-tenant hosting for other companies, which turns an internal tool into a product with an availability obligation to strangers. - SMS and phone notifications, which carry per-message cost and carrier complications well beyond email. - Private status pages with per-customer authentication, which reintroduces an application server into the read path. ### README must contain - That the page shares no account, DNS or deployment path with the product it reports on - That the page is static and serves with the authoring application entirely offline - That automation may open an incident but never resolve one - That incident updates are append only and corrections are visible - That uptime states its calculation method and what it excludes
# Agent instructions · Instatus indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Static generation to object storage with a CDN, on a separate account, A small application, deliberately elsewhere, that publishes the static page, External probes from a third party or a separate region, Email and webhooks through a provider unrelated to your product, A small database for the authoring side only. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve.
# Build plan · Instatus A status page and incident communication tool where the interesting constraints are all availability and honesty: independent infrastructure, a page that degrades to static, notifications that go out through a path unaffected by your outage, and a subscriber list that is a liability if handled carelessly. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A page that is nearly impossible to take down The status page is served as static files from infrastructure you share with nothing. ### Steps 1. Generate static and publish to storage No application server in the read path. Almost nothing left to break. Files: `src/publish.ts` 2. Separate the account, DNS and certificates Write down every shared dependency and remove the ones you can. Files: `docs/independence.md` 3. Prove independence by shutting the authoring side down The page must keep serving. Test it rather than assuming it. Files: `tests/independence.md` ### Done when - [ ] The page serves as static files with no server - [ ] Stopping the authoring application leaves it serving - [ ] Account, DNS and certificates are independent - [ ] The page works with JavaScript disabled ### Watch out - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. ## Phase 2 · Incidents, written once and never rewritten An incident is posted, updated as it develops, and the history is permanent. ### Steps 1. Model updates as append only Enforced in the schema, not by convention. Files: `src/incidents.ts` 2. Publish on every change An incident update that has not published has not happened. Files: `src/publish.ts` 3. Keep resolved incidents visible The history is the point. Hiding it is hiding your record. Files: `src/history.ts` ### Done when - [ ] An incident publishes within seconds - [ ] Updates are append only and corrections are visible - [ ] The timeline shows every update with its time - [ ] Resolved incidents stay in the history ### Watch out - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. ## Phase 3 · Checks that can see what customers see Automated probes run from outside and can open an incident but never close one. ### Steps 1. Probe from outside, against real endpoints A health endpoint returning true is not what customers experience. Files: `src/checks.ts` 2. Require consecutive failures One failure is noise. Acting on it produces a page nobody trusts. Files: `src/checks.ts` 3. Never let automation resolve Only a person closes an incident. A premature all-clear is worse than the outage. Files: `src/automation.ts`, `src/automation.test.ts` ### Done when - [ ] Checks run from outside the monitored system - [ ] A single failure produces no incident - [ ] Automation can open but not resolve - [ ] A broken prober is distinguishable from a real failure ### Watch out - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. ## Phase 4 · Telling people, through a path that still works Subscribers hear about the incident even though your product is down. ### Steps 1. Use an unrelated notification provider With credentials retrievable without your product being up. Files: `src/notify.ts` 2. Double opt-in and one-click unsubscribe A subscriber list is a list of your customers. Treat it accordingly. Files: `src/subscribers.ts` 3. Rate limit per component A flapping check must not send twenty emails. That is how people unsubscribe. Files: `src/ratelimit.ts` ### Done when - [ ] Notifications use an unrelated provider - [ ] Double opt-in and one-click unsubscribe work - [ ] A flapping component respects the rate limit - [ ] Subscriber addresses are encrypted at rest ### Watch out - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ## Not in this build - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. ## After v1, if you want it - A machine-readable status endpoint, which is what other people's tooling wants - Incident templates, since nobody writes well at three in the morning - Post-incident notes attached to the incident, which is where a status page earns trust
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Authoring side only. The published page does not read it, which is the point. DATABASE_URL=postgres://localhost:5432/status # Required. Where the static page is published. On the separate account. PUBLISH_BUCKET=s3://status-example-com # Required · secret. Scoped to the bucket only, and belonging to the account that shares nothing with the product. PUBLISH_CREDENTIALS=a key for the separate account # Optional · secret. The unrelated notification provider. Store it where you can reach it during an outage. NOTIFY_PROVIDER_KEY=a provider key # Required. Consecutive failures before automation opens an incident. One failure is noise. CHECK_FAILURES_BEFORE_INCIDENT=3 # Required. Minimum gap between notifications for one component, so flapping does not spam. NOTIFY_RATE_LIMIT_MINUTES=15
# Instatus · product brief ## Problem The page itself is the easy part: components, colored dots, an incident timeline, a 90 day uptime bar; an agent will hand you that in an afternoon. The notification fan-out is where it gets annoying: email is a Resend account and a loop, but you inherit double opt-in, unsubscribes, bounce handling, and explaining why the incident email landed in spam during the outage. Hosting is the other half: a status page on the same infra as the thing it reports on is decoration, so you need a separate host and domain and the discipline to keep it boring. Add the workflow Instatus quietly gives you, writing an update from your phone at 2am, per-component subscriber preferences, Slack and RSS and webhooks. Buildable in a weekend for a personal project; not the thing to hand-roll if a real customer contract mentions the words 'status page'. ## Product outcome A status page that is still serving when everything else is down, whose history cannot be quietly rewritten, and which never tells customers everything is fine while they cannot log in. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - A host that is not the infrastructure you are reporting on, ideally a different provider entirely - A separate domain or subdomain on separate DNS - A transactional email provider with a verified sending domain, e.g. Resend or Postmark - Somewhere to store subscribers that survives a reboot ## Explicit non-goals for v1 - Deep integrations with monitoring platforms. - Multi-tenant hosting for other companies. - SMS and phone notifications. - Private status pages with per-customer authentication. - Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test - Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box - Per-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about - The polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API - Real independence, unless you actually do the work to host the page away from your own stack ## Success criteria - The page is confirmed to serve with the authoring application stopped - No shared dependency with the product is confirmed by review - Automation is confirmed unable to resolve an incident - A single check failure is confirmed to produce no incident - Incident updates are confirmed append only
# Build brief · Instatus The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a status page that stays up when everything else is down to replace Instatus. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A status page has exactly one requirement that outranks every feature: it must be available when your product is not. That is an architectural constraint, not an operational aspiration. It means the status page shares nothing with the system it reports on. Not the same hosting account, not the same database, not the same DNS provider if you can avoid it, not the same certificate renewal, and not the same deployment pipeline. If your provider has a bad day and both go down together, you have a status page that works precisely when it is not needed. The corollary is that automatic status must be conservative. A page confidently reporting that all systems are operational while customers cannot log in is worse than no page at all, because it tells people the problem is theirs. Automation may open an incident. It should not close one. ### Stack (fixed, do not substitute) - Hosting: Static generation to object storage with a CDN, on a separate account · a page with no application server has very little that can fail - Authoring: A small application, deliberately elsewhere, that publishes the static page · the authoring tool going down must not take the page with it - Checks: External probes from a third party or a separate region · a check running inside the system it monitors reports success during an outage - Notifications: Email and webhooks through a provider unrelated to your product · notifying subscribers must not depend on the thing that is broken - Data: A small database for the authoring side only · the published page is files, so the database being down does not affect readers ### Data model (create this before Phase 1) - components(id, name, description, ordinal, current_status) · status is operational, degraded, partial or major - incidents(id, title, impact, started_at, resolved_at, created_by) - updates(id, incident_id, status, body, at) · investigating, identified, monitoring or resolved. Append only - incident_components(incident_id, component_id, impact) - maintenance(id, title, scheduled_start, scheduled_end, body, notified_at) - subscribers(id, channel, address_hash, address_encrypted, confirmed_at, unsubscribed_at) · a list of your customers, so treated as sensitive - publishes(id, at, revision, source) · every publish of the static page, so you can prove what it said and when - Updates are append only, because editing what a status page said during an incident is how a company loses the benefit of having one. ### Phase 1 · A page that is nearly impossible to take down Build: Generate the page as static files, publish to object storage behind a CDN, on a separate provider account from your product. Separate DNS and certificate paths. Prove the page survives the authoring application being entirely offline. Trap: Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. Done when: The page serves as static files with no application server in the request path. Stopping the authoring application entirely leaves the page serving, tested by shutting it down. The page is on a separate provider account with independent DNS, verified by listing what each depends on. The page loads with JavaScript disabled. Do not build yet: No incidents, checks or notifications. ### Phase 2 · Incidents, written once and never rewritten Build: Components with statuses, incidents with append-only updates, and a publish on every change. Show the timeline with timestamps. Correcting a mistake adds an update rather than editing the previous one. Trap: Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. Done when: An incident posts and publishes within seconds. Updates are append only and a correction appears as a new update, verified by attempting to edit one. The timeline shows every update with its time. A resolved incident remains in the history permanently. Do not build yet: No checks or notifications yet. ### Phase 3 · Checks that can see what customers see Build: External checks from a separate region or provider against real user-facing endpoints. Require several consecutive failures before acting. Allow automation to open an incident and change a component to degraded, never to resolve one. Trap: A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. Done when: Checks run from outside the monitored infrastructure, verified by listing what the prober depends on. Several consecutive failures are required before an incident opens, tested with a single injected failure producing nothing. Automation can open but never resolve, verified by attempting it. A check failing because the prober itself is broken is distinguishable. Do not build yet: No subscriber notifications yet. ### Phase 4 · Telling people, through a path that still works Build: Email and webhook notifications through a provider unrelated to the product. Double opt-in with confirmation, one-click unsubscribe, and per-component subscription. Rate limit so a flapping component cannot send twenty messages. Trap: Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. Done when: Notifications send through a provider unrelated to the product, verified by listing the dependency. Double opt-in is required and unsubscribing works in one click. A component changing status repeatedly does not send more than the rate limit allows, tested by flapping it. Subscriber addresses are encrypted at rest. Do not build yet: Maintenance and export come last. ### Phase 5 · Maintenance, uptime figures and proving what you said Build: Scheduled maintenance announced in advance and reflected on the page. Uptime computed from incident records with the method stated. Keep every published revision so you can show what the page said at any past moment. Trap: An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded. Done when: Maintenance announces in advance and appears on the page before it starts. Uptime is computed from incidents with the method stated beside the figure. Every publish is retained and a past state can be shown, tested by retrieving one. Exporting incident history produces a complete record. Do not build yet: Nothing after this. ### Out of scope (and why) - Deep integrations with monitoring platforms, which is most of what the hosted products actually sell. - Multi-tenant hosting for other companies, which turns an internal tool into a product with an availability obligation to strangers. - SMS and phone notifications, which carry per-message cost and carrier complications well beyond email. - Private status pages with per-customer authentication, which reintroduces an application server into the read path. ### README must contain - That the page shares no account, DNS or deployment path with the product it reports on - That the page is static and serves with the authoring application entirely offline - That automation may open an incident but never resolve one - That incident updates are append only and corrections are visible - That uptime states its calculation method and what it excludes
# Architecture · Instatus ## Stack | Part | Choice | Why | | --- | --- | --- | | Hosting | Static generation to object storage with a CDN, on a separate account | a page with no application server has very little that can fail | | Authoring | A small application, deliberately elsewhere, that publishes the static page | the authoring tool going down must not take the page with it | | Checks | External probes from a third party or a separate region | a check running inside the system it monitors reports success during an outage | | Notifications | Email and webhooks through a provider unrelated to your product | notifying subscribers must not depend on the thing that is broken | | Data | A small database for the authoring side only | the published page is files, so the database being down does not affect readers | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Publishing | Static generation to independent hosting. | Any static host, provided it shares no account or dependency with the product. | | Incidents | Append-only updates and permanent history. | Any store. Append-only is enforced in the schema rather than by convention. | | Checks | External probing with consecutive-failure thresholds. | Any prober outside the monitored system. Never one inside it. | | Notifications | Delivery through an unrelated provider. | Any provider, provided it is not the one your product depends on. | | Uptime | Computation with a stated method. | Any method, provided it is stated beside the number including exclusions. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Authoring side only. The published page does not read it, which is the point. - `PUBLISH_BUCKET` · required · Where the static page is published. On the separate account. - `PUBLISH_CREDENTIALS` · required, secret · Scoped to the bucket only, and belonging to the account that shares nothing with the product. - `NOTIFY_PROVIDER_KEY` · optional, secret · The unrelated notification provider. Store it where you can reach it during an outage. - `CHECK_FAILURES_BEFORE_INCIDENT` · required · Consecutive failures before automation opens an incident. One failure is noise. - `NOTIFY_RATE_LIMIT_MINUTES` · required · Minimum gap between notifications for one component, so flapping does not spam. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Instatus product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Static generation to object storage with a CDN, on a separate account, A small application, deliberately elsewhere, that publishes the static page, External probes from a third party or a separate region, Email and webhooks through a provider unrelated to your product, A small database for the authoring side only. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. - An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded.
# Delivery milestones · Instatus Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A page that is nearly impossible to take down The status page is served as static files from infrastructure you share with nothing. ### Steps 1. Generate static and publish to storage No application server in the read path. Almost nothing left to break. Files: `src/publish.ts` 2. Separate the account, DNS and certificates Write down every shared dependency and remove the ones you can. Files: `docs/independence.md` 3. Prove independence by shutting the authoring side down The page must keep serving. Test it rather than assuming it. Files: `tests/independence.md` ### Done when - [ ] The page serves as static files with no server - [ ] Stopping the authoring application leaves it serving - [ ] Account, DNS and certificates are independent - [ ] The page works with JavaScript disabled ### Watch out - Hosting the status page in the same account, region or cluster as the product means a provider incident takes out both, which is the single most common way status pages fail their one job. Separate accounts at minimum, and a different provider if you can accept the extra work. ## M2 · Incidents, written once and never rewritten An incident is posted, updated as it develops, and the history is permanent. ### Steps 1. Model updates as append only Enforced in the schema, not by convention. Files: `src/incidents.ts` 2. Publish on every change An incident update that has not published has not happened. Files: `src/publish.ts` 3. Keep resolved incidents visible The history is the point. Hiding it is hiding your record. Files: `src/history.ts` ### Done when - [ ] An incident publishes within seconds - [ ] Updates are append only and corrections are visible - [ ] The timeline shows every update with its time - [ ] Resolved incidents stay in the history ### Watch out - Editing a previous update to soften what was said, or quietly changing a start time, destroys the only thing a status page is actually for, which is being a record people can trust. Append only, with the correction visible as a correction. ## M3 · Checks that can see what customers see Automated probes run from outside and can open an incident but never close one. ### Steps 1. Probe from outside, against real endpoints A health endpoint returning true is not what customers experience. Files: `src/checks.ts` 2. Require consecutive failures One failure is noise. Acting on it produces a page nobody trusts. Files: `src/checks.ts` 3. Never let automation resolve Only a person closes an incident. A premature all-clear is worse than the outage. Files: `src/automation.ts`, `src/automation.test.ts` ### Done when - [ ] Checks run from outside the monitored system - [ ] A single failure produces no incident - [ ] Automation can open but not resolve - [ ] A broken prober is distinguishable from a real failure ### Watch out - A check running inside your own infrastructure passes happily during an outage that affects everyone outside it, which is the exact scenario you needed to detect. Probe from somewhere that shares nothing with the system, and treat a single failure as noise rather than an incident. ## M4 · Telling people, through a path that still works Subscribers hear about the incident even though your product is down. ### Steps 1. Use an unrelated notification provider With credentials retrievable without your product being up. Files: `src/notify.ts` 2. Double opt-in and one-click unsubscribe A subscriber list is a list of your customers. Treat it accordingly. Files: `src/subscribers.ts` 3. Rate limit per component A flapping check must not send twenty emails. That is how people unsubscribe. Files: `src/ratelimit.ts` ### Done when - [ ] Notifications use an unrelated provider - [ ] Double opt-in and one-click unsubscribe work - [ ] A flapping component respects the rate limit - [ ] Subscriber addresses are encrypted at rest ### Watch out - Sending notifications through the same mail infrastructure your product uses means an outage that includes that infrastructure silences exactly the notification you needed to send. Use a separate provider, and hold the credentials somewhere that does not depend on your product being up to retrieve. ## M5 · Maintenance, uptime figures and proving what you said (production only) Scheduled work is announced, uptime is computed honestly, and history is provable. ### Steps 1. Announce maintenance ahead of time With notification, since unannounced maintenance is indistinguishable from an outage. Files: `src/maintenance.ts` 2. State the uptime method beside the number Including what is excluded. An unqualified percentage is marketing. Files: `src/uptime.ts` 3. Retain every published revision So you can show what the page said during an incident. Files: `src/publishes.ts` ### Done when - [ ] Maintenance announces and appears before it starts - [ ] Uptime states its method and exclusions - [ ] A past published state can be retrieved - [ ] Incident history exports completely ### Watch out - An uptime percentage computed by excluding maintenance windows, partial outages and anything you decided was not really an incident is marketing rather than measurement, and customers notice the discrepancy with their own experience. State the method beside the number, including what is excluded.
# Operations · Instatus ## Backup Incident history is the record you may need to produce months later. Back up the authoring database and keep every published revision, since those are what prove what the page said at a given moment. ## Restore Restore and confirm the incident history and publish revisions are complete. A restore that loses updates leaves a history that reads as though less was communicated than actually was. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monitor the status page itself, from somewhere else again, since a status page that is down has no way to tell you. Also watch publish success, because an incident update that failed to publish is invisible. ## Incident checklist The failure that matters is the page being down during an outage, and the cause is almost always a dependency shared with the product that nobody wrote down. The independence document is the control and should be reviewed after every provider change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The page is confirmed to serve with the authoring application stopped - [ ] No shared dependency with the product is confirmed by review - [ ] Automation is confirmed unable to resolve an incident - [ ] A single check failure is confirmed to produce no incident - [ ] Incident updates are confirmed append only ## Launch constraint Do not market omitted Instatus capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Authoring side only. The published page does not read it, which is the point. DATABASE_URL=postgres://localhost:5432/status # Required. Where the static page is published. On the separate account. PUBLISH_BUCKET=s3://status-example-com # Required · secret. Scoped to the bucket only, and belonging to the account that shares nothing with the product. PUBLISH_CREDENTIALS=a key for the separate account # Optional · secret. The unrelated notification provider. Store it where you can reach it during an outage. NOTIFY_PROVIDER_KEY=a provider key # Required. Consecutive failures before automation opens an incident. One failure is noise. CHECK_FAILURES_BEFORE_INCIDENT=3 # Required. Minimum gap between notifications for one component, so flapping does not spam. NOTIFY_RATE_LIMIT_MINUTES=15
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because a status page is the one page that must work when nothing else does, and paying a third party is the cheapest way to buy that independence. It is also a page customers and auditors look at, so it being slightly ugly or slightly late costs more trust than the subscription costs money. Most teams that build their own end up with a page that is technically theirs and practically unmaintained, with an incident feed whose last entry is from fourteen months ago.
xDeliverability someone else warms up and monitors: your first incident email is also your first send reputation test
xNotification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box
xPer-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about
xThe polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API
xReal independence, unless you actually do the work to host the page away from your own stack
Nothing worth pointing at. That's why the prompt exists.
Vibecode Instatus
Kinda. The core of Instatus is buildable in a weekend with the prompt on this page, but there are real gaps: Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test, Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box. Read the honest list above before committing.
How much does Instatus cost?
Instatus's pricing is usage-based or varies by plan · AI-drafted from a site submission; price unverified..
What do I lose by replacing Instatus?
Honestly: Deliverability someone else warms up and monitors: your first incident email is also your first send reputation test; Notification channels beyond email: SMS, Slack app, Teams, Discord, native webhooks, RSS, all of which Instatus ships out of the box; Per-component subscriptions, unsubscribe handling, and double opt-in that you do not have to think about; The polish: scheduled maintenance windows, incident templates, timezone handling, embeddable widgets, public API; Real independence, unless you actually do the work to host the page away from your own stack. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Instatus?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.