# dd0c/alert — Dual-Mode Deployment Addendum

**Template:** Based on dd0c/route dual-mode pattern

---

## Cloud → Self-Hosted Service Mapping

| Cloud Service | Self-Hosted Replacement | Notes |
|--------------|----------------------|-------|
| API Gateway + Lambda | Fastify/Express container | Webhook ingestion endpoint |
| SQS | PostgreSQL pgmq | Between ingestion and correlation |
| ECS Fargate (Correlation) | Docker container | Same Node.js code |
| DynamoDB | PostgreSQL (JSONB) | Incidents stored as JSONB with GIN indexes |
| TimescaleDB on RDS | TimescaleDB container | Analytics unchanged |
| Cognito | Local JWT (HS256) | AuthProvider pattern |
| S3 (raw payload archive) | Local FS or MinIO | ObjectStore trait |
| SES | SMTP relay | Email notifications |
| EventBridge | Cron container | Scheduled tasks |

## Self-Hosted Compose Services

```yaml
services:
  ingestion:        # Webhook endpoint (replaces API Gateway + Lambda)
    image: ghcr.io/dd0c/alert-ingestion:latest
  correlation:      # Correlation engine (replaces ECS Fargate)
    image: ghcr.io/dd0c/alert-correlation:latest
  api:              # Dashboard API
    image: ghcr.io/dd0c/alert-api:latest
  dashboard:        # React SPA
    image: ghcr.io/dd0c/alert-dashboard:latest
  postgres:         # Incidents (JSONB) + config
    image: postgres:16-alpine
  timescaledb:      # Analytics
    image: timescale/timescaledb:latest-pg16
  redis:            # Sliding windows for correlation, circuit breakers
    image: redis:7-alpine
  caddy:            # Reverse proxy + auto-TLS
    image: caddy:2-alpine
```

## Key Difference: DynamoDB → PostgreSQL JSONB

DynamoDB Single-Table design maps to PostgreSQL JSONB with partition-like indexes:

```sql
CREATE TABLE incidents (
  id UUID PRIMARY KEY,
  tenant_id TEXT NOT NULL,
  data JSONB NOT NULL,
  severity TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'open',
  created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_incidents_tenant ON incidents(tenant_id);
CREATE INDEX idx_incidents_data ON incidents USING GIN(data);
-- RLS policy
ALTER TABLE incidents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON incidents USING (tenant_id = current_setting('app.tenant_id'));
```

## Epic Impact

| Epic | Change | Effort |
|------|--------|--------|
| 1 (Webhook Ingestion) | Lambda → Fastify container | 3 pts |
| 2 (Normalization) | No change — pure logic | 0 |
| 3 (Correlation) | pgmq instead of SQS, same Redis | 2 pts |
| 4 (Notifications) | SMTP fallback | 1 pt |
| 5 (Slack Bot) | No change | 0 |
| 6 (Dashboard API) | LocalAuthProvider, DynamoDB→PG | 3 pts |
| 7 (Dashboard UI) | Local login form | 2 pts |
| 8 (Infrastructure) | docker-compose.yml + install.sh | 5 pts |
| 9 (Onboarding) | Local signup, remove Stripe req | 3 pts |
| 10 (TF Tenets) | No change | 0 |
| **Total** | | **19 pts** |
