ReviewOS

also looking at this

stacks/bench-review

Adopt the hq-apps app conventions

#76
Merged glennmichael123 wants to merge chore/hq-apps-conventions into main
31 files +1656 -395
DEPLOY.mdadded+159-0
Changes to DEPLOY.md
@@ -0,0 +1,159 @@
1# Deploying Bench Review
2
3> **Host not yet chosen.** This document describes the artifact bench-review
4> produces and the contract any host must satisfy to serve it. It does not
5> describe a provisioned environment, because there isn't one yet. See
6> [Outstanding before first deploy](#outstanding-before-first-deploy).
7
8Production domain: **benchreview.org** (declared in [`site.config.ts`](site.config.ts)).
9
10## What ships
11
12Two artifacts, deployed independently:
13
14| Artifact | Produced by | What it is |
15|---|---|---|
16| `dist/` | `bun run build` | Pre-rendered static HTML, one file per route, plus hashed assets, `sitemap.xml` and `robots.txt` |
17| the API | `bun dev/api.ts` (or your process manager) | bun-router serving `/api/*` — auth, reviews, moderation, uploads |
18
19stx is a **build-time** templating engine here: every page in `dist/` is
20already rendered HTML. There is no Node render server for the frontend and
21**no package publish step** is needed to deploy — the built artifact is
22self-contained.
23
24## The deploy contract
25
26The host must provide two things that are **not** part of the built artifact.
27A host that skips them serves a site that looks fine and is broken in two
28specific ways.
29
30### 1. Rewrite `/verify-email/:id/:token` → `/verify-email.html` (status 200)
31
32Verification tokens are minted per request, so unlike every other dynamic
33route there is no finite set of pages to pre-render. Without this rewrite the
34link in **every signup confirmation email 404s**.
35
36It must be a *rewrite*, not a redirect: `Bench/VerifyEmail` reads the id and
37token off `location.pathname`, so the URL has to survive.
38
39### 2. Proxy `/api/*` → the API origin (same-origin, status 200)
40
41Every store calls the API with a **relative** path (`/api/auth`, `/api/me`,
42`/api/judges`, …). The dev server proxies this; a static host does not.
43Without the proxy every API call resolves against the static origin and 404s,
44so the site renders its shells and then stays empty forever.
45
46`preview.ts` fakes both locally so the built artifact can be exercised
47end-to-end before shipping — that is a local convenience, not a substitute for
48configuring them at the host.
49
50On CloudFront these are both cache behaviours: an `/api/*` behaviour with the
51API as origin, and a CloudFront Function (or a `/verify-email/*` behaviour)
52rewriting the URI. On nginx/Caddy they are a `try_files`/`rewrite` and a
53`proxy_pass`. Whatever the host, **these two are the deploy contract.**
54
55## Environment
56
57`APP_URL` is the one variable the *build* reads. It is baked into canonicals,
58Open Graph tags, the JSON-LD graph, `sitemap.xml` and `robots.txt`, so it must
59be the production origin at build time, not at serve time.
60
61| Var | Purpose | Notes |
62|---|---|---|
63| `APP_URL` | Canonical origin, baked into the artifact | **Required for a deployable build.** Unset ⇒ the gate fails with `APP_URL unset — artifact carries N loopback URLs` |
64| `APP_ENV` | `production` | |
65| `APP_KEY` | Encryption/signing key | `./buddy key:generate` |
66| `DB_CONNECTION` / `DB_*` | Database | dev is `sqlite`; production should be `postgres` |
67| `MAIL_*` | SMTP | **On the critical path** — email verification and password reset do not work without it. Dev points at a local catcher (`127.0.0.1:2525`) |
68| `FILESYSTEM_DISK` | `local` or `s3` | Review photos + avatars. See `app/Storage/disk.ts` |
69| `AWS_*` | S3 credentials | Only when `FILESYSTEM_DISK=s3` |
70| `API_ORIGIN` | Where `preview.ts` forwards `/api/*` | Local preview only; defaults to `http://localhost:4008` |
71| `TS_ANALYTICS_*` | Analytics endpoint override | Optional; the App ID lives in `config/ts-analytics.ts` |
72
73Local ports: views `4000`, API `4008`.
74
75## Build
76
77```bash
78APP_URL=https://benchreview.org bun run build
79```
80
81The build runs `buildApp()` (stx SSG) and then splices in, in order: per-page
82SEO head tags, `noindex` markers, per-entity SEO for dynamic pages, canonicals,
83the analytics tag, webfont tags, the CSP meta, and finally a DB-derived
84`sitemap.xml` + `robots.txt`. See [`build.ts`](build.ts).
85
86> Dynamic routes (`/judges/:id`, `/court-houses/:id`, `/article/:id`) are
87> enumerated from the **database** at build time via `getStaticPaths`. Building
88> against an empty database produces zero entity pages and the gate fails with
89> `no pages for: judges, court-houses, article`. Seed or restore the database
90> before building for release.
91
92## Validate before shipping
93
94```bash
95bun run gate # build + 17 build/SEO integrity checks
96bun run ui:validate # accessibility lint over resources/
97bun run visual:validate # headless-Chrome contrast / overflow / focus audit
98bun run release:validate # route text contracts, assets, metadata, migrations
99```
100
101`release:validate` asserts that `sitemap.xml` and `robots.txt` name
102`benchreview.org` and contain no `localhost` URLs, so it **only passes on a
103build made with a production `APP_URL`**. That is deliberate: it is the check
104that catches a dev artifact being shipped.
105
106`visual:validate` needs a Chrome or Chromium binary; set `CHROME_PATH` if it
107isn't in a standard location. Screenshots land in
108`storage/logs/visual-contract/`.
109
110## Preview the real artifact
111
112```bash
113bun run preview # serves dist/ on :3001, proxies /api to :4008
114```
115
116This is the only way to exercise what actually ships: the dev server has the
117API but serves from source (so it shows none of the build-time output), and
118`dist/` alone has no API.
119
120## After deploying
121
122```bash
123bun run deployment:smoke
124```
125
126Hits the live domain and asserts the homepage marker, a login form, `/api/health`
127(including `checks.database.ok`), `robots.txt`, and that no page ships
128unresolved stx expressions in attributes. Override the target with
129`SMOKE_BASE_URL`; tune retries with `SMOKE_ATTEMPTS` / `SMOKE_RETRY_MS`.
130
131Health is served at **`GET /api/health`** and answers `503` when the database
132or cache probe fails, so a plain HTTP check gets the right answer without
133parsing the body. See [`app/Support/health.ts`](app/Support/health.ts).
134
135## Database
136
137Migrations are plain SQL under `database/migrations/`, applied with
138`./buddy migrate`. Seeders under `database/seeders/` are idempotent and keyed
139on natural keys, so re-running is safe.
140
141> The migrations directory currently mixes two timestamp epochs — 10-digit
142> (seconds) and 13-digit (milliseconds) filenames — because a scaffold sync
143> renumbered some tables. Filename sort therefore interleaves the two families.
144> Ordering happens to still be correct (every `create` precedes its `alter`),
145> but add new migrations with the 13-digit form and treat the directory as
146> needing a deliberate cleanup pass.
147
148## Outstanding before first deploy
149
1501. **Pick a host** and implement the two deploy-contract rules above.
1512. **`config/cloud.ts` is still stock Stacks** — it declares sites for
152 `stacksjs.org`, `docs.stacksjs.org`, `tlsx.stacksjs.org` and an AWS
153 serverless target. Nothing in it describes bench-review. Either rewrite it
154 for the chosen host or delete it; do not assume `./buddy deploy` works.
1553. **Set a production `APP_URL`** — without it the artifact is not deployable.
1564. **Configure real SMTP** — verification and password reset are dead without it.
1575. **Seed or restore the database** so dynamic pages are pre-rendered.
1586. **`MAIL_FROM_ADDRESS`** still reads `no-reply@benchreview.com`; the
159 canonical domain is `benchreview.org`.