indie-status-page/TECHNICAL_DESIGN.md
IndieStatusBot 902133edd3 feat: indie status page MVP -- FastAPI + SQLite
- 8 DB models (services, incidents, monitors, subscribers, etc.)
- Full CRUD API for services, incidents, monitors
- Public status page with live data
- Incident detail page with timeline
- API key authentication
- Uptime monitoring scheduler
- 13 tests passing
- TECHNICAL_DESIGN.md with full spec
2026-04-25 05:00:00 +00:00

21 KiB

Indie Status Page — Technical Design Document

1. Architecture Overview

Indie Status Page is a self-hosted, lightweight status page tool built for indie SaaS developers. It follows a minimal monolithic architecture with three concerns:

  1. REST API — FastAPI endpoints for programmatic incident & service management
  2. Public Status Page — Jinja2-rendered HTML pages (SSR, no JS framework)
  3. Background Workers — Uptime monitor (HTTP checks) and notification dispatcher
┌──────────────────────────────────────────────┐
│                  FastAPI App                  │
│                                              │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
│  │ REST API │  │  Status   │  │  Background│  │
│  │Endpoints │  │  Pages    │  │  Scheduler │  │
│  │ /api/v1  │  │ /status/  │  │ (APScheduler)│
│  └────┬─────┘  └────┬─────┘  └─────┬──────┘  │
│       │              │              │         │
│  ┌────┴──────────────┴──────────────┴──────┐  │
│  │          SQLAlchemy + SQLite            │  │
│  └────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘
        │              │              │
   HTTP clients    Visitors       SMTP / Webhooks
   (API/CLI)      (public)      (notifications)

Design Principles

  • Single binary, single process — FastAPI + APScheduler in one uvicorn process
  • SQLite as the only store — zero-config, file-based, easy backups
  • Server-rendered pages — Jinja2 templates with minimal CSS, no build step
  • No paid dependencies — all OSS libraries, SMTP for email, SQLite for data
  • Docker-first deployment — single container, volume-mount the DB file

2. Database Schema (SQLite via SQLAlchemy)

2.1 services

Column Type Constraints Description
id UUID PK, default=uuid4 Unique service identifier
name VARCHAR(100) NOT NULL Display name (e.g. "API")
slug VARCHAR(50) NOT NULL, UNIQUE URL slug (e.g. "api")
description TEXT Optional one-liner
group_name VARCHAR(50) Grouping label (e.g. "Core")
position INTEGER DEFAULT 0 Sort order on status page
is_visible BOOLEAN DEFAULT TRUE Show on public page?
created_at DATETIME DEFAULT NOW
updated_at DATETIME DEFAULT NOW, on update

2.2 incidents

Column Type Constraints Description
id UUID PK, default=uuid4
service_id UUID FK → services.id Affected service
title VARCHAR(200) NOT NULL Incident title
status VARCHAR(20) NOT NULL investigating, identified, monitoring, resolved
severity VARCHAR(20) NOT NULL minor, major, outage
started_at DATETIME NOT NULL When incident began
resolved_at DATETIME NULLABLE When resolved
created_at DATETIME DEFAULT NOW
updated_at DATETIME DEFAULT NOW, on update

2.3 incident_updates

Column Type Constraints Description
id UUID PK, default=uuid4
incident_id UUID FK → incidents.id Parent incident
status VARCHAR(20) NOT NULL Status at time of update
body TEXT NOT NULL Update content (markdown)
created_at DATETIME DEFAULT NOW

2.4 monitors

Column Type Constraints Description
id UUID PK, default=uuid4
service_id UUID FK → services.id Monitored service
url VARCHAR(500) NOT NULL URL to check
method VARCHAR(10) DEFAULT "GET" HTTP method
expected_status INTEGER DEFAULT 200 Expected HTTP status
timeout_seconds INTEGER DEFAULT 10 Request timeout
interval_seconds INTEGER DEFAULT 60 Check interval
is_active BOOLEAN DEFAULT TRUE Enabled?
created_at DATETIME DEFAULT NOW
updated_at DATETIME DEFAULT NOW, on update

2.5 monitor_results

Column Type Constraints Description
id UUID PK, default=uuid4
monitor_id UUID FK → monitors.id
status VARCHAR(20) NOT NULL up, down, degraded
response_time_ms INTEGER Latency in ms
status_code INTEGER HTTP response code
error_message TEXT Error if failed
checked_at DATETIME NOT NULL When check ran

2.6 subscribers

Column Type Constraints Description
id UUID PK, default=uuid4
email VARCHAR(255) NOT NULL, UNIQUE Subscriber email
is_confirmed BOOLEAN DEFAULT FALSE Double-opt-in?
confirm_token VARCHAR(100) Email confirmation token
created_at DATETIME DEFAULT NOW

2.7 notification_logs

Column Type Constraints Description
id UUID PK, default=uuid4
incident_id UUID FK → incidents.id
subscriber_id UUID FK → subscribers.id
channel VARCHAR(20) NOT NULL email, webhook
status VARCHAR(20) NOT NULL sent, failed
created_at DATETIME DEFAULT NOW

2.8 site_settings

Column Type Constraints Description
id UUID PK, default=uuid4
key VARCHAR(50) NOT NULL, UNIQUE Setting name
value TEXT Setting value (JSON-serializable)
updated_at DATETIME DEFAULT NOW, on update

Pre-seeded settings: site_name, site_url, logo_url, accent_color, smtp_host, smtp_port, smtp_user, smtp_pass, notify_from, webhook_url.


3. API Endpoints

All API endpoints are versioned under /api/v1. Authentication uses a simple API key via X-API-Key header (stored in site_settings as admin_api_key).

3.1 Services

Method Endpoint Description
GET /api/v1/services List all services
POST /api/v1/services Create a service
GET /api/v1/services/{id} Get a service
PATCH /api/v1/services/{id} Update a service
DELETE /api/v1/services/{id} Delete a service

3.2 Incidents

Method Endpoint Description
GET /api/v1/incidents List incidents (filterable)
POST /api/v1/incidents Create an incident
GET /api/v1/incidents/{id} Get incident + updates
PATCH /api/v1/incidents/{id} Update incident status
DELETE /api/v1/incidents/{id} Delete incident
POST /api/v1/incidents/{id}/updates Add an update

3.3 Monitors

Method Endpoint Description
GET /api/v1/monitors List all monitors
POST /api/v1/monitors Create a monitor
GET /api/v1/monitors/{id} Get monitor + recent results
PATCH /api/v1/monitors/{id} Update monitor
DELETE /api/v1/monitors/{id} Delete monitor
POST /api/v1/monitors/{id}/check Trigger manual check

3.4 Subscribers

Method Endpoint Description
GET /api/v1/subscribers List subscribers
POST /api/v1/subscribers Add subscriber
DELETE /api/v1/subscribers/{id} Remove subscriber
POST /api/v1/subscribers/{id}/confirm Confirm subscription

3.5 Site Settings

Method Endpoint Description
GET /api/v1/settings List all settings
PATCH /api/v1/settings Update settings

3.6 Public Status Page (HTML)

Method Endpoint Description
GET / Status page (HTML)
GET /incident/{id} Incident detail (HTML)
GET /subscribe Subscribe form (HTML)
POST /subscribe Handle subscription
GET /confirm/{token} Confirm email (HTML)

3.7 Health

Method Endpoint Description
GET /health Health check

4. File/Folder Structure

indie-status-page/
├── pyproject.toml              # Project config, deps, scripts
├── README.md                   # Quick-start guide
├── TECHNICAL_DESIGN.md         # This document
├── Dockerfile                  # Production container
├── docker-compose.yml          # Local dev setup
├── .env.example                # Environment variable template
├── alembic.ini                 # DB migration config
├── migrations/
│   └── versions/               # Alembic migration scripts
├── app/
│   ├── __init__.py
│   ├── main.py                 # FastAPI app factory + lifespan
│   ├── config.py               # Pydantic settings from env
│   ├── database.py             # SQLAlchemy engine, session, Base
│   ├── dependencies.py         # FastAPI deps (DB session, auth)
│   ├── models/
│   │   ├── __init__.py         # Re-exports all models
│   │   ├── service.py          # Service model
│   │   ├── incident.py         # Incident + IncidentUpdate models
│   │   ├── monitor.py          # Monitor + MonitorResult models
│   │   ├── subscriber.py       # Subscriber model
│   │   ├── notification.py     # NotificationLog model
│   │   └── settings.py         # SiteSettings model
│   ├── api/
│   │   ├── __init__.py
│   │   ├── router.py           # v1 router aggregation
│   │   ├── services.py         # Service CRUD endpoints
│   │   ├── incidents.py        # Incident CRUD + updates
│   │   ├── monitors.py         # Monitor CRUD + manual check
│   │   ├── subscribers.py      # Subscriber management
│   │   └── settings.py         # Settings endpoints
│   ├── services/
│   │   ├── __init__.py
│   │   ├── uptime.py           # HTTP check logic
│   │   ├── notifier.py         # Email (SMTP) + webhook dispatch
│   │   └── scheduler.py        # APScheduler job registration
│   ├── templates/
│   │   ├── base.html            # Layout template
│   │   ├── status.html          # Public status page
│   │   ├── incident.html        # Incident detail page
│   │   ├── subscribe.html       # Subscribe form
│   │   └── confirm.html         # Confirmation page
│   └── static/
│       ├── css/
│       │   └── style.css        # Minimal responsive styles
│       └── js/
│           └── status.js        # Auto-refresh (optional)
├── tests/
│   ├── __init__.py
│   ├── conftest.py             # Fixtures, test DB
│   ├── test_api_services.py
│   ├── test_api_incidents.py
│   ├── test_api_monitors.py
│   ├── test_api_subscribers.py
│   ├── test_health.py
│   └── test_services_uptime.py
└── scripts/
    └── cli.py                  # CLI tool for managing incidents

5. Core Models (Pydantic Schemas)

Service

class ServiceCreate(BaseModel):
    name: str = Field(..., max_length=100)
    slug: str = Field(..., max_length=50, pattern=r"^[a-z0-9-]+$")
    description: str | None = None
    group_name: str | None = Field(None, max_length=50)
    position: int = 0
    is_visible: bool = True

class ServiceRead(ServiceCreate):
    id: UUID
    created_at: datetime
    updated_at: datetime
    model_config = ConfigDict(from_attributes=True)

Incident

class IncidentCreate(BaseModel):
    service_id: UUID
    title: str = Field(..., max_length=200)
    status: Literal["investigating", "identified", "monitoring", "resolved"]
    severity: Literal["minor", "major", "outage"]
    started_at: datetime | None = None  # defaults to now

class IncidentUpdate(BaseModel):
    status: Literal["investigating", "identified", "monitoring", "resolved"]
    body: str

Monitor

class MonitorCreate(BaseModel):
    service_id: UUID
    url: HttpUrl
    method: Literal["GET", "POST", "HEAD"] = "GET"
    expected_status: int = 200
    timeout_seconds: int = Field(10, ge=1, le=60)
    interval_seconds: int = Field(60, ge=30, le=3600)

Subscriber

class SubscriberCreate(BaseModel):
    email: EmailStr

6. MVP Feature List (Prioritized)

P0 — Ship Day 1 (Must Have)

# Feature Description
1 Service CRUD Add/edit/delete services to track
2 Incident CRUD + Updates Create incidents, post updates, resolve
3 Public Status Page SSR status page with current service status
4 Uptime Monitoring (HTTP) Periodic HTTP GET checks, store results
5 Status Derivation Auto-derive service status from monitors
6 Health Check Endpoint /health for container orchestration
7 API Key Auth Simple X-API-Key header for protected routes
8 Docker Setup Dockerfile + docker-compose

P1 — Ship Day 3-4 (Should Have)

# Feature Description
9 Subscriber Signup + Confirmation Email opt-in with double confirmation
10 Email Notifications (SMTP) Send incident updates to subscribers
11 Webhook Notifications POST to external URL on incidents
12 CLI Tool Terminal commands for incident management
13 90-Day Uptime Calculator Uptime % from monitor_results for each service

P2 — Post-MVP (Nice to Have)

# Feature Description
14 RSS Feed /feed.xml for incident history
15 Custom Domain Support CNAME-friendly setup guide
16 Status Badge SVG badge embed for READMEs
17 Scheduled Maintenance Windows Planned downtime with auto-resolve
17 Multi-language UI i18n via Jinja2 templates
18 Slack/Discord Integration Bot-based notifications

Technical Decisions & Rationale

Decision Choice Rationale
Web Framework FastAPI Async support, auto-docs, type validation
ORM SQLAlchemy 2.0 Mature, async-capable, SQLite-friendly
Database SQLite Zero-config, single file, perfect for MVP
Migrations Alembic Standard for SQLAlchemy, easy schema evolution
Task Scheduler APScheduler In-process, no Redis/Celery needed
Templates Jinja2 Built into FastAPI, no build step
Email stdlib smtplib No paid service, any SMTP relay works
Settings pydantic-settings Env-based config, type validation, .env support
Testing pytest + httpx Standard Python testing with async ASGI client
CLI Typer Intuitive CLI with auto-help, same dep as FastAPI