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:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
384
TECHNICAL_DESIGN.md
Normal file
384
TECHNICAL_DESIGN.md
Normal 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 |
|
||||
Loading…
Add table
Add a link
Reference in a new issue