# ADR-015: Dev and Prod Are Two Full Stacks on the Same VPS, Not Branches or Feature Flags

## Status

Accepted, 2026-06-25.

## Status History

```yaml
status_history:
  - date: 2026-06-25
    status: Proposed
    changed_by: hkl
    reason: Formalising two-stack separation implemented 2026-06-23
    changed_via: adr-kit (360lm)
  - date: 2026-06-25
    status: Accepted
    changed_by: hkl
    reason: Two-stack live and stable since 2026-06-23
    changed_via: adr-kit (360lm)
```

## Context

Before 2026-06-23, all development happened on the same URL and DB as production. A broken migration or half-built feature could break live users. The system needed an isolated environment for building and testing new PWAs before promoting them to production. The constraint: single VPS, limited budget — no separate cloud instances. Implemented 2026-06-23 (commit `1bb5a31`).

## Decision

Run two complete stacks on the same VPS:

| | Dev | Prod |
|---|---|---|
| URL | `dev.srv1111289.hstgr.cloud` | `srv1111289.hstgr.cloud` |
| DB | `lm360` | `lm360_prod` |
| Files | `/var/www/360lm/` | `/var/www/360lm-prod/` |
| Git | `dev` branch | `master` branch |

Promotion: merge `dev` → `master` → run `deploy-prod.sh`. One-way flow only. Never hotfix directly in prod files — always fix in `dev`, promote via script.

**Decision Maker:** hkl

## Alternatives Considered

- **Single environment (dev = prod).** Rejected: any broken migration or in-progress feature breaks live users; no safe space to test; was the original setup and caused incidents.
- **Separate VPS for dev.** Rejected: doubles infrastructure cost; data sync between two VPS instances adds complexity; single VPS is sufficient for current traffic (< 20 users).
- **Feature flags to hide in-progress work.** Rejected: feature flags require discipline to clean up; adds conditional branching to every new feature; unsuitable for schema migrations which cannot be flag-gated.
- **Docker container per environment (same machine, port-based routing).** Effectively what is implemented — Traefik routes by domain to two httpd containers on the same Docker network.
- **GitHub Actions / CI-CD pipeline.** Rejected: over-engineered for the current team size and deployment frequency; `deploy-prod.sh` provides the same outcome with less infrastructure. // ponytail: upgrade trigger=team size > 3 developers or deployment frequency > 2/day

## Consequences

**Positive:**
- Dev can have broken migrations, in-progress PWAs, and experimental features without affecting prod.
- `deploy-prod.sh --dry-run` shows exactly what will change before promotion.
- Bugfix protocol: clone prod DB as `lm360_bugfix` → reproduce bug exactly → fix → promote.
- Schema migrations tracked in `public.schema_migrations` in both DBs — no guessing what prod has.

**Negative / Trade-offs:**
- New PWA built in dev does not exist in prod until explicitly promoted (schema + files + hub registry).
- Two DBs to keep in sync when promoting — `deploy-prod.sh` handles SQL migrations but developer must apply them correctly.
- Playwright tests must specify which domain to use — some tests must run against dev, some against prod.

**Risks and mitigations:**
- Dev DB drifts from prod: mitigated by `schema_migrations` table and `--migrate-only` flag to catch up prod DB without redeploying files.
- Promoting broken code to prod: mitigated by Playwright test suite on dev before any merge to master.

## Related Decisions

- ADR-009 (schema-per-PWA) — schema promotion is per-PWA; `deploy-prod.sh` applies pending SQL.
- ADR-005 (SW CACHE_VER) — prod and dev have independent cache versions; bumping dev CACHE_VER does not affect prod until promoted.

## References

- `memory/infra_env_separation.md` — full two-stack details
- `/usr/local/bin/deploy-prod.sh` — promotion script
- `/var/www/360lm/docs/prod-hotfix-protocol.md` — hotfix procedure
