> ## Documentation Index
> Fetch the complete documentation index at: https://sportzdocs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# DevOps & CI/CD

> How code moves from a laptop to production: Docker, GitHub Actions, and Render.

## Environments

| Environment        | Database                       | How it runs                                     |
| ------------------ | ------------------------------ | ----------------------------------------------- |
| **Local (direct)** | plain Postgres or Neon Local   | `npm run dev` (tsx watch)                       |
| **Local (Docker)** | Neon Local (ephemeral branch)  | `npm run dev:docker` → `docker-compose.dev.yml` |
| **Test / CI**      | `postgres:16-alpine` container | `npm test`                                      |
| **Production**     | Neon Cloud (pooled)            | Docker image on Render                          |

## The Docker image

A **multi-stage** build (full detail in [Backend](/architecture/backend)):

* **`builder`**: installs all deps, compiles TypeScript → `dist/`.
* **`runner`**: fresh image, production deps only, copies just `dist/`, runs as a non-root user.

The final image ships no compiler, no devDeps, no source, so it's smaller and has a lower attack surface. Both compose files target the `runner` stage (a mismatch here was a real bug, see [Issues](/issues)).

## Local dev with Neon Local

`docker-compose.dev.yml` runs the app plus a **Neon Local** proxy that forks a fresh ephemeral Neon branch on `up` and deletes it on `down`. `scripts/dev.sh` (via `npm run dev:docker`) validates env, waits for the proxy to be healthy, runs migrations, then tails logs.

```bash theme={null}
npm run dev:docker                              # start the dev stack
docker compose -f docker-compose.dev.yml down   # stop + delete ephemeral branch
```

<Tip>
  This project has **two** compose files and no default `docker-compose.yml`, so the `-f` flag is always required. A handy alias: `alias dc-dev='docker compose -f docker-compose.dev.yml'`.
</Tip>

## CI/CD: three GitHub Actions workflows

```mermaid theme={null}
flowchart LR
  push["push / PR\nto main or staging"] --> lint["lint-and-format.yml\nESLint + Prettier"]
  push --> tests["tests.yml\nVitest vs postgres service"]
  merge["push to main"] --> docker["docker-build-and-push.yml\nmulti-arch image → Docker Hub"]
```

1. **`lint-and-format.yml`**: runs ESLint and Prettier; reports *both* failures together (not one-at-a-time) with fix commands as annotations.
2. **`tests.yml`**: spins up a `postgres:16-alpine` service container, runs migrations, runs `npm test`, uploads coverage (30-day retention), writes a summary.
3. **`docker-build-and-push.yml`**: on merge to main (or manual), builds `linux/amd64` + `linux/arm64`, tags (branch, SHA, `latest`, `prod-YYYYMMDD-HHmmss`), pushes to Docker Hub. Requires `DOCKER_USERNAME` / `DOCKER_PASSWORD` repo secrets.

## Deploy to Render

Production deploys via a **Render Blueprint** (`render.yaml`) using Render's **Docker runtime**: Render builds the same Dockerfile you tested locally.

```yaml theme={null}
services:
  - type: web
    name: sportz-api
    runtime: docker
    dockerfilePath: ./Dockerfile
    healthCheckPath: /        # GET / is registered BEFORE Arcjet — never rate-limited
    envVars:
      - { key: DATABASE_URL, sync: false }   # Neon Cloud pooled URL, set in dashboard
      - { key: ARCJET_KEY,   sync: false }
      - { key: CORS_ORIGIN,  sync: false }
      - { key: NODE_ENV, value: production }
      - { key: PORT,     value: "8000" }
      - { key: HOST,     value: "0.0.0.0" }  # bind all interfaces so Render can route in
```

**Migrations are not automatic**: Render runs the app, not migrations. Run once against the prod DB before/after first deploy:

```bash theme={null}
DATABASE_URL="<neon-cloud-pooled-url>" npm run db:migrate
```

### The flow, end to end

```
laptop → git push → PR → CI (lint + tests) → review → merge to main
   → docker-build-and-push (image) + Render auto-deploy from main → production
```

## Frontend CI/CD (sportz-ui)

The frontend has its own pipeline, mirroring the backend's conventions.

**CI: two GitHub Actions workflows:**

1. **`lint-and-format.yml`**: ESLint + Prettier, both reported together with a gate (same pattern as the backend). Node 22 (matches the frontend's Dockerfile and Next 16).
2. **`tests.yml`**: two parallel jobs: **Typecheck** (`tsc --noEmit`) and **E2E (Playwright)** (builds a production server on :3100 and runs the mocked suite + axe a11y scan). No database/service container: the e2e suite mocks REST/WS at the network boundary.

**CD: Vercel (Git integration):** Vercel builds and deploys on every push to `main`, with a **preview deployment per PR**. This *is* the CD; there's no separate deploy workflow. `NEXT_PUBLIC_*` env vars are set in Vercel's project settings (they're baked at build time, so changing one requires a redeploy). `CORS_ORIGIN` on the Render backend must include the Vercel origin or the live site is blocked ([ADR-009](/decisions)).

**Branch protection connects CI to CD.** `main` requires the three checks (`Typecheck`, `E2E (Playwright)`, `Lint and Format Check`) to pass **and** a pull request before merge, with admin bypass disabled. Since Vercel's *production* deploy comes from `main`, gating `main` effectively gates production, even though Vercel itself doesn't read CI. PR **preview** deploys still happen regardless (you want to preview WIP).

```
feature branch → push → PR → CI checks + Vercel PREVIEW deploy
   → (merge blocked until checks green) → merge to main
   → Vercel PRODUCTION deploy
```

**Dockerized too (optional).** The frontend has a `standalone` Dockerfile (gated behind `DOCKER_BUILD=1` so `next start` and Vercel are unaffected) for own-infra/learning use, but Vercel is the live deploy. Vercel fits a client-rendered app better (managed CDN, image optimization, preview URLs) than self-hosting a container would ([ADR-009](/decisions)).

## Not yet built

* **Rollback automation**: Render keeps prior deploys (manual rollback in dashboard); no scripted rollback.
* **Graceful shutdown** on deploy: SIGTERM currently drops WS connections uncleanly (`close()` exists as the building block; see [Backend](/architecture/backend)).
* **Automated post-deploy smoke**: the deployed smoke test ([Testing](/operations/testing)) is run manually (`npm run test:e2e:deployed`); a workflow that runs it automatically *after* a Vercel deploy is a logical next step.
* **Pre-commit enforcement**: lint/format are enforced today by the npm scripts (local, manual) and by CI as the merge gate. A **husky + lint-staged** pre-commit hook would auto-run `eslint --fix` + `prettier --write` on staged files so unformatted code can't even be committed, and **commitlint** would enforce a commit-message convention (Conventional Commits). Planned tooling, not yet wired.
