also looking at this
Adopt the hq-apps app conventions
#76
31 files
+1656
-395
| @@ -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 | ||
| 8 | Production domain: **benchreview.org** (declared in [`site.config.ts`](site.config.ts)). | |
| 9 | ||
| 10 | ## What ships | |
| 11 | ||
| 12 | Two 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 | ||
| 19 | stx is a **build-time** templating engine here: every page in `dist/` is | |
| 20 | already 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 | |
| 22 | self-contained. | |
| 23 | ||
| 24 | ## The deploy contract | |
| 25 | ||
| 26 | The host must provide two things that are **not** part of the built artifact. | |
| 27 | A host that skips them serves a site that looks fine and is broken in two | |
| 28 | specific ways. | |
| 29 | ||
| 30 | ### 1. Rewrite `/verify-email/:id/:token` → `/verify-email.html` (status 200) | |
| 31 | ||
| 32 | Verification tokens are minted per request, so unlike every other dynamic | |
| 33 | route there is no finite set of pages to pre-render. Without this rewrite the | |
| 34 | link in **every signup confirmation email 404s**. | |
| 35 | ||
| 36 | It must be a *rewrite*, not a redirect: `Bench/VerifyEmail` reads the id and | |
| 37 | token off `location.pathname`, so the URL has to survive. | |
| 38 | ||
| 39 | ### 2. Proxy `/api/*` → the API origin (same-origin, status 200) | |
| 40 | ||
| 41 | Every 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. | |
| 43 | Without the proxy every API call resolves against the static origin and 404s, | |
| 44 | so the site renders its shells and then stays empty forever. | |
| 45 | ||
| 46 | `preview.ts` fakes both locally so the built artifact can be exercised | |
| 47 | end-to-end before shipping — that is a local convenience, not a substitute for | |
| 48 | configuring them at the host. | |
| 49 | ||
| 50 | On CloudFront these are both cache behaviours: an `/api/*` behaviour with the | |
| 51 | API as origin, and a CloudFront Function (or a `/verify-email/*` behaviour) | |
| 52 | rewriting 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, | |
| 58 | Open Graph tags, the JSON-LD graph, `sitemap.xml` and `robots.txt`, so it must | |
| 59 | be 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 | ||
| 73 | Local ports: views `4000`, API `4008`. | |
| 74 | ||
| 75 | ## Build | |
| 76 | ||
| 77 | ```bash | |
| 78 | APP_URL=https://benchreview.org bun run build | |
| 79 | ``` | |
| 80 | ||
| 81 | The build runs `buildApp()` (stx SSG) and then splices in, in order: per-page | |
| 82 | SEO head tags, `noindex` markers, per-entity SEO for dynamic pages, canonicals, | |
| 83 | the 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 | |
| 95 | bun run gate # build + 17 build/SEO integrity checks | |
| 96 | bun run ui:validate # accessibility lint over resources/ | |
| 97 | bun run visual:validate # headless-Chrome contrast / overflow / focus audit | |
| 98 | bun 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 | |
| 103 | build made with a production `APP_URL`**. That is deliberate: it is the check | |
| 104 | that catches a dev artifact being shipped. | |
| 105 | ||
| 106 | `visual:validate` needs a Chrome or Chromium binary; set `CHROME_PATH` if it | |
| 107 | isn't in a standard location. Screenshots land in | |
| 108 | `storage/logs/visual-contract/`. | |
| 109 | ||
| 110 | ## Preview the real artifact | |
| 111 | ||
| 112 | ```bash | |
| 113 | bun run preview # serves dist/ on :3001, proxies /api to :4008 | |
| 114 | ``` | |
| 115 | ||
| 116 | This is the only way to exercise what actually ships: the dev server has the | |
| 117 | API 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 | |
| 123 | bun run deployment:smoke | |
| 124 | ``` | |
| 125 | ||
| 126 | Hits 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 | |
| 128 | unresolved stx expressions in attributes. Override the target with | |
| 129 | `SMOKE_BASE_URL`; tune retries with `SMOKE_ATTEMPTS` / `SMOKE_RETRY_MS`. | |
| 130 | ||
| 131 | Health is served at **`GET /api/health`** and answers `503` when the database | |
| 132 | or cache probe fails, so a plain HTTP check gets the right answer without | |
| 133 | parsing the body. See [`app/Support/health.ts`](app/Support/health.ts). | |
| 134 | ||
| 135 | ## Database | |
| 136 | ||
| 137 | Migrations are plain SQL under `database/migrations/`, applied with | |
| 138 | `./buddy migrate`. Seeders under `database/seeders/` are idempotent and keyed | |
| 139 | on 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 | ||
| 150 | 1. **Pick a host** and implement the two deploy-contract rules above. | |
| 151 | 2. **`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. | |
| 155 | 3. **Set a production `APP_URL`** — without it the artifact is not deployable. | |
| 156 | 4. **Configure real SMTP** — verification and password reset are dead without it. | |
| 157 | 5. **Seed or restore the database** so dynamic pages are pre-rendered. | |
| 158 | 6. **`MAIL_FROM_ADDRESS`** still reads `no-reply@benchreview.com`; the | |
| 159 | canonical domain is `benchreview.org`. | |