diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..192c56f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +node_modules +.nuxt +.output +.data +*.log +.env +.env.* +!.env.example +docker-compose*.yml +Dockerfile* +tests +docs +tasks +.DS_Store +.fleet +.idea diff --git a/.env.example b/.env.example index 54f8cdb..50e99cd 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,11 @@ NUXT_SESSION_SECRET=change-me # Database (Postgres) — Coolify-managed in prod, docker-compose in dev DATABASE_URL=postgres://andon:andon@localhost:5432/andon +# Credentials for the bundled Postgres service in docker-compose.yml +POSTGRES_USER=andon +POSTGRES_PASSWORD=andon +POSTGRES_DB=andon + # Google OAuth (Task 9) — restricted to the theodo.com hosted domain GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/Dockerfile b/Dockerfile index b4182b9..182e785 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,8 @@ FROM node:24-alpine AS build RUN corepack enable WORKDIR /app -COPY package.json pnpm-lock.yaml ./ -RUN pnpm install --frozen-lockfile COPY . . +RUN pnpm install --frozen-lockfile RUN pnpm build # --- Runtime stage: minimal image running the built server --- diff --git a/Dockerfile.dev b/Dockerfile.dev index dfd561b..528438c 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -3,7 +3,7 @@ RUN corepack enable WORKDIR /app # Install dependencies first for better layer caching. -COPY package.json pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ RUN pnpm install --frozen-lockfile COPY . . diff --git a/README.md b/README.md index 965c6eb..2fb1107 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Two apps, one codebase, behind Google SSO (`@theodo.com`): - **`andon.apoena.dev`** — the reporting view: click an ASCII board section, pick your project, describe the problem. -- **`dashboard.andon.apoena.dev`** — the same board as a red-dot defect map, a +- **`dashboard-andon.apoena.dev`** — the same board as a red-dot defect map, a reverse-chronological feed, and verbatims in a modal. ## Status @@ -33,14 +33,35 @@ House of Quality: **F1 board definition → F2/F3 filing → F4/F5 weak-point ma ## Stack -Nuxt 3 (full-stack Vue) · PostgreSQL + Drizzle (Coolify-managed, auto-backups) · +Nuxt 4 (full-stack Vue) · PostgreSQL + Drizzle (Coolify-managed, auto-backups) · Google OAuth (`hd=theodo.com` + server-side domain recheck) · Web Push (PWA) · -Docker Compose on Coolify. +Docker on Coolify. -## Deployment notes +## Running with Docker -- **Use Coolify's managed Postgres** (with scheduled backups); the app connects - via `DATABASE_URL`. See [ADR 0003](./docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md). -- **Don't auto-deploy from `main`** — every merge would deploy. Use a tagged - release or manual trigger. -- **Don't trust the OAuth `hd` claim alone** — verify the email domain server-side. +```bash +# Production-like full stack (build the image + Postgres) +docker compose up --build # app on http://localhost:3000 + +# Local development (hot-reload, source-mounted) +docker compose -f docker-compose.dev.yml up + +# Or the dev server directly against a containerized Postgres +docker compose -f docker-compose.dev.yml up -d db +DATABASE_URL=postgres://andon:andon@localhost:5432/andon pnpm dev +``` + +Migrations are applied automatically on server boot (a Nitro plugin) — no manual +migrate step is needed. + +## Deploying to Coolify + +1. Point Coolify at this repo; it builds the production `Dockerfile`. +2. Create a **managed Postgres** in Coolify (automatic backups — [ADR 0003](./docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md)) + and set `DATABASE_URL` to it. _(Alternatively deploy `docker-compose.yml`, which bundles a Postgres service.)_ +3. Set the env vars from [`.env.example`](./.env.example) (`NUXT_SESSION_SECRET`, + Google OAuth, VAPID) and route `andon.apoena.dev` + `dashboard-andon.apoena.dev` to the service. +4. **Enable Coolify's autodeploy** so it builds and deploys on every push to + `main`. Protect `main` with required PR review + CI so only vetted commits + reach production. +5. The OAuth `hd` claim is spoofable — the app re-checks the email domain server-side (T9). diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..5a29bf9 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,37 @@ +# Local development stack: app with hot-reload (source bind-mounted) + Postgres. +# docker compose -f docker-compose.dev.yml up +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + ports: + - "3000:3000" + environment: + DATABASE_URL: postgres://andon:andon@db:5432/andon + HOST: 0.0.0.0 + depends_on: + db: + condition: service_healthy + volumes: + - .:/app + - /app/node_modules + + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: andon + POSTGRES_PASSWORD: andon + POSTGRES_DB: andon + ports: + - "5432:5432" + volumes: + - andon_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U andon"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + andon_pgdata: diff --git a/docker-compose.yml b/docker-compose.yml index ec5e9c5..eaf2ab8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,32 +1,40 @@ +# Production / self-host stack (Coolify-deployable as a Docker Compose resource). +# For production, prefer a Coolify-managed Postgres (automatic backups — see +# docs/adr/0003) by setting DATABASE_URL to it; the bundled `db` service is for +# self-hosting and local production testing. +# docker compose up --build services: app: build: context: . - dockerfile: Dockerfile.dev + dockerfile: Dockerfile + restart: unless-stopped ports: - "3000:3000" environment: - DATABASE_URL: postgres://andon:andon@db:5432/andon - HOST: 0.0.0.0 + DATABASE_URL: ${DATABASE_URL:-postgres://andon:andon@db:5432/andon} + NUXT_SESSION_SECRET: ${NUXT_SESSION_SECRET:-change-me} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} + GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-} + VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} + VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} + VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:owner@theodo.com} depends_on: db: condition: service_healthy - volumes: - - .:/app - - /app/node_modules db: image: postgres:16-alpine + restart: unless-stopped environment: - POSTGRES_USER: andon - POSTGRES_PASSWORD: andon - POSTGRES_DB: andon - ports: - - "5432:5432" + POSTGRES_USER: ${POSTGRES_USER:-andon} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-andon} + POSTGRES_DB: ${POSTGRES_DB:-andon} volumes: - andon_pgdata:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U andon"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-andon}"] interval: 5s timeout: 5s retries: 5 diff --git a/docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md b/docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md index 7bcdb10..15af098 100644 --- a/docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md +++ b/docs/adr/0003-use-coolify-managed-postgres-over-sqlite.md @@ -18,5 +18,6 @@ switch to **Coolify-managed PostgreSQL** (via Drizzle's `pg` driver). prod — one more moving part than a single file. - **Supersedes** the SQLite choice in DESIGN.md (T5, F10) and the original `/deep-design` decision. -- Unchanged: deploys must **not** auto-run from `main` (every merge would deploy); - use a tagged release or manual trigger. +- Autodeploy from `main` is enabled (Coolify watches the repo and deploys on + push); `main` must be protected with required PR review + CI so only vetted + commits reach production. diff --git a/package.json b/package.json index 48aac6c..b275129 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "andon", "type": "module", "private": true, + "packageManager": "pnpm@11.0.9", "scripts": { "build": "nuxt build", "dev": "nuxt dev", @@ -14,7 +15,10 @@ "typecheck": "nuxt typecheck", "db:generate": "drizzle-kit generate", "db:migrate": "tsx server/db/migrate.ts", - "db:verify": "tsx scripts/verify-db.ts" + "db:verify": "tsx scripts/verify-db.ts", + "docker:dev": "docker compose -f docker-compose.dev.yml up", + "docker:dev:build": "docker compose -f docker-compose.dev.yml up --build", + "docker:dev:down": "docker compose -f docker-compose.dev.yml down" }, "dependencies": { "drizzle-orm": "^0.45.2", diff --git a/server/plugins/migrate.ts b/server/plugins/migrate.ts new file mode 100644 index 0000000..0b8f277 --- /dev/null +++ b/server/plugins/migrate.ts @@ -0,0 +1,13 @@ +import { migrate } from 'drizzle-orm/node-postgres/migrator' +import { getDb } from '../db/client' + +// Apply pending migrations once when the server boots (dev and prod). Skipped +// when DATABASE_URL is unset (e.g. during build) so it never blocks the bundle. +export default defineNitroPlugin(async () => { + if (!process.env.DATABASE_URL) { + console.warn('[migrate] DATABASE_URL not set — skipping migrations') + return + } + await migrate(getDb(), { migrationsFolder: 'server/db/migrations' }) + console.log('[migrate] migrations applied') +}) diff --git a/tasks/plan.md b/tasks/plan.md index 0fe3289..d085daf 100644 --- a/tasks/plan.md +++ b/tasks/plan.md @@ -313,7 +313,8 @@ section, project, and a verbatim snippet; log failures (no retry queue in v1). **Description:** Production Dockerfile; provision Coolify-managed Postgres with scheduled backups; configure the two domains and TLS on Coolify; wire env/secrets (`DATABASE_URL`, Google client, VAPID, session secret); run the -migration on boot. Deploy from a tagged release/manual trigger, not auto-`main`. +migration on boot. Coolify autodeploys on push to `main`, with `main` +protected by PR review + CI. **Acceptance criteria:** - [ ] After a redeploy, previously filed defects are still present (0 data loss). @@ -339,7 +340,7 @@ migration on boot. Deploy from a tagged release/manual trigger, not auto-`main`. | Risk | Impact | Mitigation | |------|--------|------------| | Data lost on redeploy / disk failure | High | Coolify-managed Postgres with scheduled backups; redeploy durability test (T12, ADR 0003) | -| Auto-deploy from `main` ships every merge | Med | Deploy from tagged release / manual trigger only (T12) | +| Autodeploy from `main` ships every merge | Med | Protect `main` with required PR review + CI; migrations-on-boot + healthcheck catch failures; Coolify keeps rollback (T12) | | Google `hd` claim spoofed by a custom client | High | Server-side verified-email-domain recheck (T9) | | Google OAuth credentials/redirect URIs unavailable | Med | `getReporter()` seam lets Phases 1–3 proceed with dev auth (T5, T9) | | Dot map unreadable on hot Sections | Med | Visible-dot cap + "+N more" (T7) |