# 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 ```python 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 ```python 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 ```python 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 ```python 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 |