01
The typed idea
35 words, typed in. This is the only input Murmur got.
Lightweight uptime monitor + status page for indie SaaS. Auto-detect downtime, create incidents, alert via email/Slack, embeddable badge. No bloated enterprise features. Target indie SaaS. $9-29/mo. Talk to 15. Easy. $1k MRR with 40-100 users.
02
Competitor map
Live web search for who's already building nearby, with a source link for every competitor.
- 14 competitors
- 5 table stakes
- 3 ways to differentiate
Market summary
The market is crowded with operating products covering the whole job: automatic uptime checks, alerts and a status page. Closest to the indie SaaS target are StillOnline (HTTP checks, incidents, Telegram/Slack/email alerts, $0/$9/$29 plans), SiteReady (HTTP checks, email alerts, public status page, from $0), PingPing (30-second checks, Slack/email/SMS, status pages), Pulsetic (multi-region checks, many alert channels, status pages from $9/mo) and Cronaut (automatic incident creation on a status page, email/Slack alerts). Larger or broader tools such as UptimeRobot, StatusCake, Oh Dear, Better Stack, Instatus and openstatus cover the same parts at broader scope, and Gatus and Uptime Kuma cover them as self-hosted open source. StatusPal covers status pages and integrated monitoring. No claim is made that no tool does this job; the phrase searches returned incomplete results. No named-tool or phrase searches failed.
Competitors 14
StillOnline
stillonline.techHosted uptime monitoring and status pages for indie SaaS, solo founders and small SaaS teams, with no heavy observability stack or self-hosting.
PriceFree $0 (1 project, 1 URL, 5-minute checks); Pro $9/mo (up to 10 projects, private pages, REST API, MCP); Ultimate $29/mo (up to 100 projects) (stillonline.tech), checked Nov 14, 2026
+3 strengths /−1 weaknesses
Strengths
- Explicitly targets indie SaaS
- Plan prices of $0, $9 and $29 match the target price band
- Free plan includes 1 public status page with up to 50 email subscribers
Weaknesses
- Free plan is limited to 1 URL with 5-minute checks and 24-hour history, per the pricing page
SiteReady
siteready.appUptime monitoring and status pages for indie makers: no bloat, no enterprise pricing.
PriceFree $0/mo (2 monitors, 1-minute checks, email alerts, public status page); Starter $1/mo for first 3 months, then $5/mo (10 monitors, unlimited status pages, 30-day history) (siteready.app), checked Nov 14, 2026
+3 strengths /−1 weaknesses
Strengths
- Very low prices
- Free plan includes 2 monitors, 1-minute checks and a public status page
- Setup time stated as 30s
Weaknesses
- Only email alerts are stated on the homepage
PingPing
pingping.ioThe simplest uptime monitoring service, built for solo founders, with 30-second checks and multi-language status pages.
Pricenot found (tried pingping.io), checked Nov 14, 2026
+3 strengths /−1 weaknesses
Strengths
- Fast 30-second checks
- Status page included on all plans
- Designed for solo founders
Weaknesses
- No prices found on fetched pages
Pulsetic
pulsetic.comUptime monitoring and alerts where the user decides whether to declare an incident on a status page.
PriceFree $0/month; Solo $9/month; Team $19/month; add-ons such as $0.20 per monitor and $8 per teammate (pulsetic.com), checked Nov 14, 2026
+3 strengths /−1 weaknesses
Strengths
- Many alert channels
- Status page subscribers get email on incident open, update and resolve
- Entry paid plan at $9/month
Weaknesses
- Homepage says the user makes the call to declare an incident; automatic incident creation not stated
Cronaut
cronaut.devOne tool for uptime, cron and SSL checks with automatic status page incidents, priced for indie hackers.
Pricenot found (tried cronaut.dev), checked Nov 14, 2026
+2 strengths /−1 weaknesses
Strengths
- Automatically opens and closes incidents on the status page
- Targets indie hackers explicitly
Weaknesses
- No prices found on fetched pages
Instatus
instatus.comStatus page and uptime monitoring with a free forever plan.
PriceFree plan; Pro from $20/mo per the page title; fetched page lists monitors and check intervals per tier (instatus.com), checked Nov 14, 2026
+2 strengths /−0 weaknesses
Strengths
- Free forever plan stated
- Setup stated as taking 30 seconds
Weaknesses
openstatus
openstatus.devOpen-source-style monitoring, incident management and status pages with Slack agent and MCP support.
Price14-day free Starter trial; Starter $30/month; Pro $100/month; Scale $500/month; extras such as additional status pages $20/mo each (openstatus.dev), checked Nov 14, 2026
+2 strengths /−1 weaknesses
Strengths
- Strong Slack incident workflow
- Used by named product teams such as Cal.com
Weaknesses
- Paid plans start at $30/mo, above the $9-29 band, per its pricing page
Oh Dear
ohdear.appWebsite health monitoring (uptime, SSL, DNS, cron, broken links) aimed at people managing client sites.
PriceSolo €15/mo (2 sites); Solo Plus €29/mo (5); Freelance €49/mo (10); Studio €99/mo (25); Agency €149/mo (50); up to Scale €399/mo (200); 10-day trial (ohdear.app), checked Nov 14, 2026
+2 strengths /−1 weaknesses
Strengths
- Broad monitoring scope
- Every feature on every plan, per its about text
Weaknesses
- No free tier stated; cheapest listed plan is €15/mo for 2 sites
UptimeRobot
uptimerobot.comLarge uptime monitoring service with a free tier, public status pages and many integrations.
PriceFree $0/month; Solo $13 or $12/month; Team $46 or $39/month; Scale $98 or $79/month; Enterprise by demo (uptimerobot.com), checked Nov 14, 2026
+2 strengths /−1 weaknesses
Strengths
- 50 monitors free
- Large user base stated (3.5M+ users)
Weaknesses
- Free plan has 5-minute interval and basic status pages per fetched pricing page
StatusCake
statuscake.comUptime, page speed, SSL, domain and server monitoring plus StatusCake Pages for customer communication.
PriceFree $0/month; Superior from $20.41/month; Business from $66.66/month; Enterprise (statuscake.com), checked Nov 14, 2026
+2 strengths /−1 weaknesses
Strengths
- Broad monitoring scope
- Free plan exists
Weaknesses
- Paid tiers from $20.41/mo and a broad enterprise-oriented suite beyond the lightweight job
Better Stack
betterstack.comAI-native observability stack with uptime monitoring, incident management and status pages.
PriceResponder $34/month ($29 yearly); team member $0; free tier for personal projects; additional public status page $15/month (betterstack.com), checked Nov 14, 2026
+1 strengths /−1 weaknesses
Strengths
- Uptime monitoring with free tier of 10 monitors and a status page
Weaknesses
- Scope extends well beyond the job into observability, which the lightweight product aims to avoid
Gatus
github.comDeveloper-oriented, self-hosted health dashboard and automated status page with alerting and incident support.
Pricenot verified, checked Nov 14, 2026
+1 strengths /−1 weaknesses
Strengths
- Open source with an active community
Weaknesses
- Self-hosted via Docker; a managed option is referenced at Gatus.io
Uptime Kuma
github.comEasy-to-use self-hosted monitoring tool with status pages.
Pricenot verified, checked Nov 14, 2026
+2 strengths /−1 weaknesses
Strengths
- Large community (94k+ stars)
- Broad notification support
Weaknesses
- Self-hosted install via Docker is required
StatusPal
statuspal.ioIncident communication, monitoring and status pages in one platform, with an enterprise-grade classic option.
PriceUnlimited public status pages for individuals: $46/month (statuspal.io), checked Nov 14, 2026
+1 strengths /−0 weaknesses
Strengths
- Certified to ISO/IEC 27001:2022
Weaknesses
Table stakes 5
- Automatic HTTP/HTTPS uptime checks
- Email alerts on downtime and recovery
- Slack or chat alerts (most competitors state them)
- Hosted public status page
- Free tier or low entry price
Where to differentiate 3
- Embeddable status badge: not established from the sources reviewed, since none of the fetched pages mention one
- Automatic incident creation and resolution paired with email/Slack alerts at $9-29/mo: only Cronaut states automatic incident opening, and its pricing was not found
- Tighter indie-SaaS focus with no enterprise features: StillOnline already occupies the $0/$9/$29 band, so differentiation must come from product experience rather than price alone
A lightweight uptime monitor and status page for indie SaaS that detects outages, auto-creates incidents, alerts via email/Slack and offers an embeddable badge, without enterprise bloat. It must stand apart from other indie-focused tools like StillOnline, SiteReady and Cronaut on speed of setup and incident automation.
03
Product requirements document
Every feature carries a rationale tied back to the memo or the research.
- 7 must
- 3 should
- 4 could
- 8 won't
- 7 success metrics
One-liner
A lightweight hosted web app for indie SaaS that auto-detects downtime, opens and closes incidents on a status page, alerts by email and Slack, and offers an embeddable badge, for $9-29/mo with no enterprise bloat.
Problem
Indie SaaS founders find out about outages from angry customers or stitch together a monitor, a status page and manual incident posts. The transcript describes the need as detecting downtime, creating incidents and alerting by email or Slack, without enterprise features. Competitors cover the parts but leave a gap: Pulsetic leaves the incident declaration to the user, and the only tool that states automatic incident opening and closing, Cronaut, has no pricing found. StillOnline already sits in the $0/$9/$29 band, and SiteReady is cheaper but states only email alerts. Gaps remain in one-step detect-to-incident automation with email and Slack alerts, an embeddable status badge that none of the reviewed pages mention, and a no-bloat experience at $9-29/mo.
Target user
Solo founders and tiny teams running a paid indie SaaS who want to use a hosted web app to monitor their own product and keep customers informed, without configuring an observability stack or paying enterprise prices.
Features
Open a card for the full description and rationale
Must have 7
The user adds a URL and the service checks it on a fixed interval, recording up/down state and failure reason.
WhyFounder: 'auto-detect downtime'; Research: automatic HTTP/HTTPS checks are table stakes.
When a check confirms downtime, an incident opens on the status page without user action, and it closes when the service recovers.
WhyFounder: 'create incidents'; Research: only Cronaut states automatic incident opening, and Pulsetic leaves the call to the user.
The owner receives an email when an incident opens and when it resolves.
WhyFounder: 'alert via email/Slack'; Research: email alerts are table stakes.
The owner connects a Slack channel and gets incident-open and incident-resolved messages.
WhyFounder: 'alert via email/Slack'; Research: most competitors state Slack alerts.
Each project gets a public page showing current status and the incident history generated by the product.
WhyFounder: 'status page'; Research: a hosted public status page is table stakes.
The user copies a snippet that embeds a live up/down badge in their site or README.
WhyFounder: 'embeddable badge'; Research: no reviewed competitor page mentions a badge.
Self-serve account creation and card checkout for plans priced within $9-29/mo, with plan limits on monitors.
WhyRecommended: the founder's $1k MRR goal needs a way to charge, and the first-version flow can't earn without accounts and billing.
Should have 3
Confirm a failure with retries from a second check before opening an incident, so one dropped request doesn't create one.
WhyResearch: Pulsetic confirms across regions and Cronaut does flap detection, and auto-created incidents lose trust if they are wrong.
A guided flow from signup to a live monitor, status page and badge, with sensible defaults and no required config.
WhyResearch: the positioning must beat StillOnline, SiteReady and Cronaut on speed of setup, and SiteReady and Instatus both state about 30 seconds.
The owner can add a short update message to an open incident on the status page.
WhyRecommended: automatic incidents need a human explanation, which Pulsetic's incident updates already offer.
Could have 4
Store and display response time per check on the status page.
WhyResearch: PingPing, SiteReady and Pulsetic show response times, but the founder asks only for downtime detection.
Warn the owner by email or Slack before the certificate expires.
WhyResearch: SiteReady and PingPing include SSL checks, and indie owners often miss renewals.
Serve the status page from the customer's own subdomain.
WhyResearch: PingPing, Pulsetic and openstatus offer custom domains.
Visitors subscribe to incident emails from the status page.
WhyResearch: Pulsetic and StillOnline offer subscribers, but the founder lists only owner alerts.
Won't have 8
No paid telephony channels in the first version.
WhyFounder: alerts are email and Slack only; Research: Pulsetic and PingPing add SMS but it adds cost and complexity.
No rotas, escalation policies or paging tools.
WhyFounder: 'No bloated enterprise features'; Research: Better Stack and openstatus target teams.
No selectable check regions, such as openstatus's 28 regions.
WhyResearch: broad global networks are an enterprise-style competitor strength outside the lightweight job.
Only HTTP/HTTPS URL checks are supported.
WhyResearch: Cronaut, UptimeRobot and Gatus cover these, but the founder wants a lightweight monitor.
No log, trace, page-speed or server monitoring.
WhyResearch: Better Stack and StatusCake compete on breadth, which the founder rejects as bloat.
Single owner per account, with no agency features or branded client reports.
WhyFounder: target is indie SaaS; Research: Oh Dear and StatusPal serve agencies and enterprises.
No developer API, AI-agent integration or self-install option.
WhyResearch: StillOnline and openstatus offer API or MCP, and Gatus and Uptime Kuma are self-hosted, but none was requested.
No native app and no translated pages in the first version.
WhyRecommended: neither is needed to prove the core idea.
Success metrics 7
Customer discovery conversations held with indie SaaS founders
15 conversations completed within 30 days of starting
Paying customers (blended ARPU about $10-25/mo, reconciling with $1k MRR)
40 paying users within 6 months of launch [assumption: timeframe]
Monthly recurring revenue from subscriptions
$1,000 MRR within 6 months of launch (reached at 40 users at $25 blended or 100 users at $10 blended) [assumption: timeframe]
Share of auto-created incidents that the owner confirms in a one-click email prompt as a real outage (the core output quality risk)
≥95% of incidents rated real within the first 60 days [assumption]
Detection-to-alert latency, measured as check timestamp to alert send timestamp for confirmed downtime
≥95% of alerts sent within 3 minutes of the first failed check, measured over the first 60 days [assumption]
Activation rate: signups that create a monitor and publish a status page, tracked by event within the first session
≥60% within the first 30 days after launch [assumption]
Gross margin after check-infrastructure and email costs, calculated monthly
≥80% gross margin by month 6 [assumption]
Non-goals 5
- Not an observability or APM platform
- Not an enterprise incident-response or on-call tool
- Not a multi-user team product in the first version: one owner per account
- Not a self-hosted or open-source product
- Not competing on check frequency or number of check regions
Risks 4
- Crowded market: StillOnline already occupies the $0/$9/$29 band, and SiteReady, PingPing and Cronaut serve the same audience, so differentiation rests on incident automation and the badge.
- False-positive incidents posted publicly on a customer's status page could destroy trust in automatic incident creation.
- Low prices put pressure on margin because check infrastructure scales with monitors, and free-tier competitors such as UptimeRobot and Better Stack set price expectations.
- Reaching 40-100 paying users depends on a distribution channel the founder hasn't described beyond talking to 15 people.
Open questions 5
- Product name: Pagewell is a working name; the founder has not named the product — founder to confirm
- The transcript says 'Talk to 15. Easy.': does it mean interviewing 15 indie SaaS founders before building, or a target of 15 early customers? — founder to confirm
- Which plan tiers and limits fall within $9-29/mo, and is there a free tier? Feature priority is not tied to plan, so this is a paid-versus-free decision — founder to confirm
- What should the badge show (current status only, or also uptime percentage)? — Recommended: current up/down status only (smallest working form); founder to confirm
- What check interval does the first version run? — Recommended: 1-minute checks (matches SiteReady's free tier without the cost of 30-second checks); founder to confirm
Want this structure for your own PRD? Start from the free product requirements document template, with a guide to each section.
04
Brand kit
Name notes, tagline, values, palette, type and a logo direction, written from the PRD.
- 4 core colors
- 5 brand values
- Bricolage Grotesque headings
Know when you're down. Tell customers automatically.
- Primary#0F766E
- Secondary#F4EFE6
- Accent#F2994A
- Neutral#2B2F33
Bricolage Grotesque / DM Sans / JetBrains Mono5 brand values
Brand values 5
- Calm under pressure
- Honest status, plainly told
- Small on purpose
- Fast to set up
- Fair, transparent pricing
Semantic colors
- error #CD2D2D
- success #247A47
- warning #8A6300
Name notes
- Pagewell is flagged in the PRD as a working name that the founder has not confirmed, so treat it as provisional and not final until the founder signs off.
- Possible alternatives to explore: Stillwell (calm, reassuring tone) or Upnote (suggests status updates).
- No trademark or domain check has been done for Pagewell or the alternatives, so run both before committing.
Logo direction
- Form
- Symbol plus lowercase wordmark 'pagewell', usable as a compact app icon and favicon
- Style
- Friendly, minimal geometric mark with a warm, approachable feel; flat, single-color capable
- Symbol concept
- A rounded page or card shape with a small status dot in the corner, where the dot doubles as a gentle pulse or a well-like circular opening, suggesting a page that is healthy and watched over
- Iconography
- Rounded 1.75px stroke line icons with soft corners and a single filled status-dot accent in orange; friendly and simple, 24px grid, consistent geometry, no sharp alarm or warning clichés
05
Tech design
The engineering brief: architecture, data flow, data models, schema, stack and the first tasks.
- 9 components
- 14 data models
- 14 first tasks
Overview
Pagewell is two deployable pieces on one Postgres database. (1) A Next.js web app on Vercel serves the signup flow, dashboard, public status pages, the embeddable badge, and billing webhooks. (2) A small always-on Node worker on a container host runs the checks, applies the false-positive guard, opens and closes incidents, and sends alerts. Checks run in a long-lived worker instead of Vercel functions because 1-minute checks across many monitors need a reliable scheduler and short retries. Serverless cron and function duration limits depend on plan and must be confirmed before anyone moves this onto Vercel. All user-facing incident text (titles, alert bodies, status-page entries) comes from deterministic templates filled with stored values. No LLM is used anywhere. The product lives or dies on whether an auto-opened incident is real, so the detection and incident engine gets the deepest design.
Data flow
Owner signs up (Supabase Auth), creates a project and a monitor URL via the web app, which writes projects, monitors and alert_channels. The worker claims due monitors, runs the HTTP check, and writes a checks row and updates the monitor counters. After 3 consecutive failures with a healthy canary, one transaction opens the incident, its first incident_updates row, notifications rows and feedback links. The dispatcher sends email and Slack and marks notifications sent. The status page and badge read incidents and monitors, so they show the outage without any owner action. After 2 consecutive successes, a second transaction resolves the incident and queues 'resolved' alerts. Stripe webhooks update subscriptions, and the worker only checks monitors whose owner subscription is entitled.
Tech stack 7
- frontend
- Next.js (App Router); status page and badge server-rendered.
- backend
- Next.js route handlers for app, badge, billing webhooks. Separate always-on Node worker for checks and alerts, because 1-minute checks with 15-second retries need a persistent scheduler, not serverless functions.
- database
- Supabase Postgres. Row-level security on every table; the worker and public pages use the server-side service role.
- auth
- Supabase Auth, email and password or magic link.
- llm
- Not required. Detection, the retry and recovery rules, incident open and close, and every alert, status-page and badge message are deterministic logic and templates filled with stored values. A model would add cost and risk of wrong public statements.
- infra
- Vercel for the web app. Worker on a small container host (Fly.io or Railway) [assumption], with at least 2 worker instances later; the claim lease makes this safe. Transactional email provider (Resend or Postmark) [assumption]. Billing: Stripe [assumption] behind a BillingProvider interface because plans and pricing are still open. Third parties receiving user data: Supabase, Vercel, the worker host, the email provider, Stripe, and Slack (only the owner's own channel). Slack webhook URLs are encrypted in alert_channel_secrets and readable only by the service role.
- devtools
- TypeScript, Supabase CLI migrations, Vitest, Playwright, a local mock HTTP server for failure scenarios.
Components 9
Detection and incident engine (worker, core job)
INPUTS: monitors that are due (next_check_at <= now(), paused_reason IS NULL, owner subscription status in trialing/active/past_due) and the worker canary result. OUTPUTS: checks rows, monitors state updates, incidents, incident_updates, notifications rows (an outbox), incident_feedback_links. APPROACH: (a) Claim due monitors in batches with a single UPDATE ... WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED) that sets locked_until, so two workers never check the same monitor. (b) Run each check with a timeout of timeout_seconds [assumption: 10s]. Up means a final response of 2xx or 3xx after at most 5 redirects [assumption]; anything else is down with failure_reason in timeout, dns_error, connection_error, tls_error, http_status or too_many_redirects. (c) False-positive guard: on a failed check increment consecutive_failures, set first_failure_at if null, and reschedule the same monitor in 15s [assumption] instead of waiting a full interval. Open an incident only when consecutive_failures reaches 3 [assumption: first check plus 2 retries], so one dropped request never opens one. Any success resets consecutive_failures to 0. (d) Open incident in ONE transaction: insert the incident (started_at = first_failure_at, so alert latency is measured from the first failed check), set open_monitor_id, set monitors.current_state='down', insert an incident_updates row (kind 'auto_opened', templated message), insert one notifications row per enabled alert channel (event_kind 'opened', status 'pending', idempotency_key = incident_id:channel_id:opened), and insert incident_feedback_links. (e) Resolve: while current_state='down', 2 consecutive successes [assumption] set status='resolved', resolved_at, open_monitor_id=NULL, and add an 'auto_resolved' update plus 'resolved' notifications, all in one transaction. One success is not enough, to avoid flapping. (f) Canary: each cycle the worker checks 2 stable public endpoints [assumption]. If both fail, the worker's own network is suspect, so it records worker_heartbeats.canary_ok=false, does not count that cycle's failures, and does not open or resolve incidents. (g) Targets that resolve to private, loopback or link-local addresses are never fetched (SSRF block, DNS answer pinned for the request, re-validated on every redirect). Such a monitor gets paused_reason='blocked_target' and never counts as downtime. NEVER VIOLATE: an incident opens only after 3 consecutive confirmed failures with a healthy canary; at most one open incident per monitor (enforced by UNIQUE open_monitor_id); a monitor is never resolved or opened from stale data (a worker that has missed its heartbeat does not decide anything); the same alert is never sent twice. This logic is fully deterministic. KNOWN LIMIT: one worker region means a regional network problem between the worker and the customer can look like downtime. Multi-region checks are out of scope, so the canary and retries are the only protections.
Alert dispatcher (worker)
Polls notifications with status 'pending' and sends them. Email goes to the owner's account email through a transactional email provider. Slack goes to the channel's webhook URL, decrypted from alert_channel_secrets at send time. Success sets status='sent' and sent_at. Failures are retried with attempts incremented up to a cap [assumption: 5], then status='failed' with last_error. Email and Slack are independent, so a Slack failure never blocks the email. The unique idempotency_key makes retries and worker restarts safe. Message bodies are templates filled with monitor name, URL, failure_reason, started_at and resolved_at. The open email includes one-click 'real outage' and 'false alarm' links.
Status page renderer
Public server-rendered page at /s/{slug} (path scheme is an assumption). It shows overall status and the incident history from incidents and incident_updates, including owner notes. Overall status is derived deterministically: any open incident means down; all monitors up means operational; otherwise no data yet. Reads use the service role with an explicit column list, never exposing alert channels, secrets, or owner data. Owner note text is rendered as escaped plain text.
Embeddable badge endpoint
GET /badge/{slug}.svg returns a small SVG showing current up/down status only (default for the open badge question), derived the same way as the status page. Short cache headers [assumption: 60 seconds]. The dashboard shows copyable Markdown and HTML snippets. Image proxies such as the ones READMEs use may cache longer, so the badge is 'near-live', not instant.
Dashboard and guided onboarding
Signup, then project name and slug, then first monitor URL, then a live status page link and badge snippet, in one guided flow with defaults (60s interval, email channel auto-created). Sets profiles.onboarding_completed_at. Also manages monitors, pausing, alert channels (including connecting Slack), and adding an owner note to an open incident.
Incident notes
Lets the owner add a short plain-text update to an open or resolved incident. It writes an incident_updates row with kind 'owner_note', shown on the status page.
Feedback link handler
Handles one-click verdict links from the open-incident email. The raw token is only in the email; incident_feedback_links stores token_hash. Visiting a valid, unused, unexpired link sets incidents.owner_verdict and used_at. This produces the real-outage rate success metric.
Billing and entitlements
Self-serve checkout and plan changes behind a BillingProvider interface (default Stripe [assumption]). A webhook handler inserts billing_events (unique provider + provider_event_id, so replays do nothing) and updates subscriptions. Monitor creation enforces plans.monitor_limit. A canceled subscription stops checks and alerts via the worker eligibility rule.
Worker health watchdog
Worker writes worker_heartbeats.last_beat_at each loop. An external uptime ping on the worker's health endpoint (a separate service from Pagewell itself) alerts the founder if beats stop, because a dead worker silently produces no incidents at all.
Data models 14
Open a model for its fields
profiles3 fields
- iduuid
- onboarding_completed_attimestamptz
- created_attimestamptz
- has one subscriptions
- has many projects
plans8 fields
- iduuid
- codetext
- nametext
- price_centsinteger
- monitor_limitinteger
- min_interval_secondsinteger
- is_activeboolean
- created_attimestamptz
- has many subscriptions
subscriptions11 fields
- iduuid
- owner_iduuid
- plan_iduuid
- statustext
- providertext
- provider_customer_idtext
- provider_subscription_idtext
- trial_ends_attimestamptz
- current_period_endtimestamptz
- created_attimestamptz
- updated_attimestamptz
- belongs to profiles
- belongs to plans
billing_events7 fields
- iduuid
- providertext
- provider_event_idtext
- event_typetext
- payloadjsonb
- processed_attimestamptz
- received_attimestamptz
- none; applied to subscriptions by provider ids
projects5 fields
- iduuid
- owner_iduuid
- nametext
- slugtext
- created_attimestamptz
- belongs to profiles
- has many monitors, alert_channels, incidents
monitors15 fields
- iduuid
- project_iduuid
- nametext
- urltext
- interval_secondsinteger
- timeout_secondsinteger
- current_statetext
- consecutive_failuresinteger
- consecutive_successesinteger
- first_failure_attimestamptz
- next_check_attimestamptz
- locked_untiltimestamptz
- last_checked_attimestamptz
- paused_reasontext
- created_attimestamptz
- belongs to projects
- has many checks and incidents
checks6 fields
- idbigint
- monitor_iduuid
- checked_attimestamptz
- okboolean
- status_codeinteger
- failure_reasontext
- belongs to monitors
alert_channels5 fields
- iduuid
- project_iduuid
- kindtext
- enabledboolean
- created_attimestamptz
- belongs to projects
- has one alert_channel_secrets (slack only)
- has many notifications
alert_channel_secrets3 fields
- channel_iduuid
- ciphertexttext
- created_attimestamptz
- belongs to alert_channels
incidents12 fields
- iduuid
- project_iduuid
- monitor_iduuid
- statustext
- open_monitor_iduuid
- titletext
- failure_reasontext
- started_attimestamptz
- resolved_attimestamptz
- owner_verdicttext
- owner_verdict_attimestamptz
- created_attimestamptz
- belongs to projects and monitors
- has many incident_updates, notifications, incident_feedback_links
incident_updates5 fields
- iduuid
- incident_iduuid
- kindtext
- messagetext
- created_attimestamptz
- belongs to incidents
notifications10 fields
- iduuid
- incident_iduuid
- channel_iduuid
- event_kindtext
- statustext
- idempotency_keytext
- attemptsinteger
- last_errortext
- queued_attimestamptz
- sent_attimestamptz
- belongs to incidents and alert_channels
incident_feedback_links6 fields
- iduuid
- incident_iduuid
- token_hashtext
- verdicttext
- expires_attimestamptz
- used_attimestamptz
- belongs to incidents
worker_heartbeats3 fields
- worker_idtext
- last_beat_attimestamptz
- canary_okboolean
- none
SQL schema (excerpt)
View the SQL14 lines
CREATE TABLE profiles (id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, onboarding_completed_at timestamptz, created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE plans (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), code text NOT NULL UNIQUE, name text NOT NULL, price_cents integer NOT NULL CHECK (price_cents >= 0), monitor_limit integer NOT NULL CHECK (monitor_limit > 0), min_interval_seconds integer NOT NULL DEFAULT 60 CHECK (min_interval_seconds >= 30), is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE subscriptions (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_id uuid NOT NULL UNIQUE REFERENCES profiles(id) ON DELETE CASCADE, plan_id uuid NOT NULL REFERENCES plans(id), status text NOT NULL CHECK (status IN ('trialing','active','past_due','canceled')), provider text NOT NULL DEFAULT 'stripe', provider_customer_id text, provider_subscription_id text UNIQUE, trial_ends_at timestamptz, current_period_end timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE billing_events (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), provider text NOT NULL, provider_event_id text NOT NULL, event_type text NOT NULL, payload jsonb NOT NULL, processed_at timestamptz, received_at timestamptz NOT NULL DEFAULT now(), UNIQUE (provider, provider_event_id));
CREATE TABLE projects (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_id uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, name text NOT NULL, slug text NOT NULL UNIQUE CHECK (slug ~ '^[a-z0-9-]{3,40}$'), created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE monitors (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name text NOT NULL, url text NOT NULL CHECK (url ~* '^https?://'), interval_seconds integer NOT NULL DEFAULT 60 CHECK (interval_seconds >= 30), timeout_seconds integer NOT NULL DEFAULT 10 CHECK (timeout_seconds > 0), current_state text NOT NULL DEFAULT 'unknown' CHECK (current_state IN ('unknown','up','down')), consecutive_failures integer NOT NULL DEFAULT 0, consecutive_successes integer NOT NULL DEFAULT 0, first_failure_at timestamptz, next_check_at timestamptz NOT NULL DEFAULT now(), locked_until timestamptz, last_checked_at timestamptz, paused_reason text CHECK (paused_reason IN ('user','blocked_target')), created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE checks (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, monitor_id uuid NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, checked_at timestamptz NOT NULL DEFAULT now(), ok boolean NOT NULL, status_code integer, failure_reason text CHECK (failure_reason IN ('timeout','dns_error','connection_error','tls_error','http_status','too_many_redirects')), CHECK (ok = (failure_reason IS NULL)));
CREATE TABLE alert_channels (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, kind text NOT NULL CHECK (kind IN ('email','slack')), enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (project_id, kind));
CREATE TABLE alert_channel_secrets (channel_id uuid PRIMARY KEY REFERENCES alert_channels(id) ON DELETE CASCADE, ciphertext text NOT NULL, created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE incidents (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, monitor_id uuid NOT NULL REFERENCES monitors(id) ON DELETE CASCADE, status text NOT NULL CHECK (status IN ('open','resolved')), open_monitor_id uuid UNIQUE REFERENCES monitors(id) ON DELETE CASCADE, title text NOT NULL, failure_reason text, started_at timestamptz NOT NULL, resolved_at timestamptz, owner_verdict text CHECK (owner_verdict IN ('real','false_alarm')), owner_verdict_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), CHECK ((status = 'open') = (open_monitor_id IS NOT NULL)), CHECK ((status = 'resolved') = (resolved_at IS NOT NULL)));
CREATE TABLE incident_updates (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, kind text NOT NULL CHECK (kind IN ('auto_opened','auto_resolved','owner_note')), message text NOT NULL, created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE notifications (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, channel_id uuid NOT NULL REFERENCES alert_channels(id) ON DELETE CASCADE, event_kind text NOT NULL CHECK (event_kind IN ('opened','resolved')), status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sent','failed')), idempotency_key text NOT NULL UNIQUE, attempts integer NOT NULL DEFAULT 0, last_error text, queued_at timestamptz NOT NULL DEFAULT now(), sent_at timestamptz);
CREATE TABLE incident_feedback_links (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), incident_id uuid NOT NULL REFERENCES incidents(id) ON DELETE CASCADE, token_hash text NOT NULL UNIQUE, verdict text NOT NULL CHECK (verdict IN ('real','false_alarm')), expires_at timestamptz NOT NULL, used_at timestamptz);
CREATE TABLE worker_heartbeats (worker_id text PRIMARY KEY, last_beat_at timestamptz NOT NULL DEFAULT now(), canary_ok boolean NOT NULL DEFAULT true);First engineering tasks 14
Apply schemaSql as migrations. Enable RLS on all tables. Owner-only policies on profiles, projects, monitors, checks, alert_channels, incidents, incident_updates and subscriptions (via project or owner_id). No client policies at all on alert_channel_secrets, billing_events, notifications, incident_feedback_links and worker_heartbeats. Seed plans with placeholder tiers [assumption].
Supabase Auth signup and login. A trigger or server action creates the profiles row and a subscriptions row with status 'trialing' [assumption: trial without card] when a user first signs up. Protect dashboard routes.
Vercel project for the web app with env vars. Container deploy for the worker with a health endpoint and service-role database connection. Secrets (service role key, Slack encryption key, email and billing keys) live only in server and worker env vars. External uptime ping on the worker health endpoint. Staging and production environments.
Build the checker: GET with timeout_seconds, up to 5 redirects [assumption], classification into failure_reason values timeout, dns_error, connection_error, tls_error, http_status, too_many_redirects. Resolve DNS first and refuse private, loopback, link-local and metadata addresses, pin the resolved IP for the request, and re-validate on every redirect. A blocked target sets monitors.paused_reason='blocked_target' and writes no checks row. Reject unsafe URLs at monitor creation too.
Worker loop that claims due monitors with UPDATE ... WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED) setting locked_until, only for monitors with paused_reason IS NULL whose owner subscription status is trialing, active or past_due. Bounded concurrency [assumption: 50 parallel checks]. Writes a checks row, updates last_checked_at and next_check_at (now + interval_seconds, or +15s [assumption] while consecutive_failures is 1 or 2). Writes worker_heartbeats each loop and runs the canary; when canary_ok is false, discard that cycle's failures.
Implement the false-positive guard: set first_failure_at on the first failure, open when consecutive_failures reaches 3 [assumption]. Transactionally insert incidents (status 'open', open_monitor_id set, started_at = first_failure_at), the incident_updates row with kind 'auto_opened', notifications rows (event_kind 'opened', status 'pending', idempotency_key incident_id:channel_id:opened) for each enabled alert_channels row, and incident_feedback_links with token_hash only. Resolve after 2 consecutive successes [assumption]: status 'resolved', resolved_at set, open_monitor_id NULL, 'auto_resolved' update, 'resolved' notifications. Template all text; no free-form generation.
Process notifications with status 'pending'. Email to the owner's account address via the provider with templated subject and body; the open email carries the two feedback links. Slack: decrypt the webhook from alert_channel_secrets and post a templated message. Update status to 'sent' with sent_at, or increment attempts and finally set 'failed' with last_error. Slack connect UI stores the encrypted URL; the method (pasted webhook vs OAuth app) is an open question.
Route /s/{slug} server-rendered with service-role reads of an explicit column list. Show derived overall status, the open incident with its incident_updates, and incident history. Escape owner_note text. Add noindex and short caching options [assumption].
Route /badge/{slug}.svg returning an SVG for operational, down or unknown from monitors.current_state and open incidents. Cache-Control about 60 seconds [assumption]. Dashboard shows copyable Markdown and HTML snippets.
Guided flow: project name and slug, first monitor URL, auto-created 'email' alert_channels row, then show status page link and badge snippet; set profiles.onboarding_completed_at. Enforce plans.monitor_limit and min_interval_seconds on monitor creation. Support pausing (paused_reason 'user'). Track setup time and activation in product analytics [assumption].
Owner form to add an incident_updates row with kind 'owner_note' (length-limited plain text). Verdict endpoint hashes the presented token, matches incident_feedback_links.token_hash, checks expires_at and used_at, then sets incidents.owner_verdict and owner_verdict_at and marks the link used.
BillingProvider interface with a Stripe implementation [assumption]: create checkout, manage plan changes, handle webhooks. Webhook inserts into billing_events (unique provider, provider_event_id) and ignores duplicates, verifies signatures, then updates subscriptions. Cancelled status stops checks via the worker eligibility rule.
Scheduled job deleting checks older than 7 days [assumption]; incidents and incident_updates are kept as history.
Mock-server tests: a single failed request opens no incident; 3 consecutive failures open exactly one; a failure then success resets consecutive_failures; one success after downtime does not resolve, 2 do; each failure_reason (timeout, 5xx, 404, DNS failure, TLS error, redirect loop) classified correctly; 3xx redirect to a healthy page counts as up; opened and resolved alerts are not swapped, and the resolved alert never goes out while the incident is open; two workers claiming the same monitor produce one check; a retried dispatcher never sends a duplicate (idempotency_key); a Slack outage does not block email; canary failure suppresses incident open and resolve; a stale worker (old last_beat_at) does not resolve or open incidents; private IP, DNS-rebind and redirect-to-private targets are blocked; replayed billing webhook changes nothing; feedback link works once, rejects expired or wrong tokens; the badge and status page agree on state; canceled subscription stops checks.
Open engineering questions 11
- Where should the check scheduler run? Recommended: an always-on worker. Serverless cron frequency and function duration limits depend on the Vercel plan and must be confirmed in current docs before anyone moves checks onto Vercel functions or Supabase scheduled functions.
- Which plan tiers, limits and trial or free tier fall within $9-29/mo? The schema stores them per plan row (plans.monitor_limit, min_interval_seconds, price_cents) so this stays configurable; the trialing status and trial length are assumptions.
- Is a 1-minute check interval right for the first version? It sets worker load and gross margin, and the worker cost per monitor is unmeasured.
- Slack connection method: pasted incoming-webhook URL (assumed default, simplest) or a Slack OAuth app with channel picker (better UX, adds token storage and app review). Founder to choose.
- False positives from a single check location: the 3-failure rule plus canary reduce them, but a regional network fault between our worker and the customer's host can still open a public incident. Multi-region is out of scope. Should incidents show only to the owner for the first few minutes, or should the owner be able to switch off auto-publish per monitor?
- Flapping policy: if a site goes down again right after resolving, the current rule opens a new incident each time. Should it reopen the previous incident within a short window instead?
- A site that returns 200 with an error page, or an auth wall, counts as up, since only status codes are checked. Is keyword matching needed, or is this acceptable for the first version?
- Badge content: current up/down status only is assumed. Should it also show uptime percentage? That would need check history beyond the 7-day retention assumed.
- Worker redundancy: one worker is a single point of failure. Is running 2 worker instances (safe because of the claim lease) required at launch, and which external service watches the worker heartbeat?
- Email deliverability: which provider and sending domain, and is an owner-only alert address enough, or should owners add a second recipient?
- Billing provider: Stripe is assumed behind an interface. Confirm tax and invoicing needs for the founder's country.
06
Docs
The run's six docs, written from the artifacts above. This run didn't use Atlassian, so they stayed in Murmur and the run's zip; no Confluence space was created.
Docs
In Murmur and the run's zip · 6 docs
- 01 - Product
- 02 - Brand & Identity
- 03 - Engineering
- 04 - Competitor Analysis
- 05 - Roadmap
- 06 - Research Notes
07
Roadmap
Every story is placed in one of three phases.
- 3 phases
- 40 stories placed
Phase 1 Now
16stories
6 epics
- Engineering & Infrastructure
- Automatic HTTP/HTTPS uptime checks
- Automatic incident creation and resolution
- Email alerts on down and recovery
- Signup, project setup and paid plans
- False-positive guard before declaring downtime
Phase 2 Next
16stories
10 epics
- Automatic HTTP/HTTPS uptime checks
- Automatic incident creation and resolution
- Email alerts on down and recovery
- Slack alerts on down and recovery
- Hosted public status page
- Embeddable status badge
- Signup, project setup and paid plans
- False-positive guard before declaring downtime
- Edit or annotate auto-created incidents
- Setup in under two minutes
Phase 3 Later
8stories
4 epics
- Engineering & Infrastructure
- Hosted public status page
- Edit or annotate auto-created incidents
- Setup in under two minutes
08
Board
The board Murmur built from the PRD: 11 epics, 40 stories, each with its Murmur story id (S for a feature story, E for engineering). This run didn't use Atlassian, so no Jira project was created.
Murmur board11 epics · 40 stories
- P1 Now
- P2 Next
- P3 Later