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
This commit is contained in:
IndieStatusBot 2026-04-25 05:00:00 +00:00
commit 902133edd3
4655 changed files with 1342691 additions and 0 deletions

384
TECHNICAL_DESIGN.md Normal file
View file

@ -0,0 +1,384 @@
# 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 |