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