Deploy to Render free tier with a preview per PR #233

Closed
opened 2026-10-04 19:26:07 +00:00 by root · 17 comments
root commented 2026-10-04 19:26:07 +00:00 (Migrated from gitlab.core-project.space)

Imported from GitHub amaurycoudr/chess-project#233, opened by @amaurycoudr on 2026-10-04 19:26 UTC.
Closed on GitHub on 2026-10-04 21:01 UTC as completed.


Problem Statement

The app has no deployment. Nobody can play it without cloning the repo, and a PR can't be tried in a browser before it merges.

Solution

Deploy to a single free Render web service, with automatic PR previews. The Node API serves the built front as well as /rpc, so both share one origin, as apps/front/src/room/api/client.ts assumes. Render is configured by a committed render.yaml blueprint. A one-off wizard script walks the owner through the dashboard setup.

Why Render free over the alternatives (benchmarked in the session that produced this spec):

  • Cost and setup: $0, no card, built-in single-service PR previews.
  • Accepted trade-off: the instance sleeps after 15 min idle and takes ~1 min to wake. Sleeping and redeploying both wipe in-memory Rooms, which fits the Rooms' existing 30-minute TTL.
  • Next step if that hurts: a Hetzner VM with Coolify, from the same artifact.

User Stories

  1. As a player, I open the production URL and the SPA loads; deep links such as /room/ABC123 load the SPA too.
  2. As a player, I create and play an online game on the production URL, and moves arrive over SSE.
  3. As the owner, every PR gets its own preview URL, with an API that accepts that preview's origin.
  4. As the owner, production deploys only after CI on main is green.
  5. As the owner, I run scripts/setup-render.sh once and it walks me through the Render dashboard, then smoke-tests the deploy.

Seams

  • Static front serving: a new API module, e.g. serveFront(app, { root }), tested by calling app.fetch against a temporary directory fixture. It is wired in apps/api/src/index.ts only, so createApp stays I/O-free (ADR 0009).
  • Config: loadConfig (apps/api/src/config.ts) gains an optional front directory (e.g. FRONT_DIST_DIR). Unset means no static serving, so dev and e2e are unchanged.

Implementation Decisions

  • Static serving uses serveStatic from @hono/node-server/serve-static for files under the front dir.
  • SPA fallback: any other GET that is not under /rpc returns index.html. /rpc/* behaviour is untouched, including the origin guard and the 404 for unmatched procedures.
  • render.yaml at the repo root, one web service:
    • name: chess-project, runtime: node, plan: free, region: frankfurt.
    • buildCommand: corepack enable && pnpm install --frozen-lockfile && pnpm build. Node comes from .nvmrc (24).
    • startCommand runs node apps/api/dist/index.js directly, not pnpm start. That way the apps/api/.env.local written by postinstall (env:sync) cannot apply.
    • ORIGIN_ALLOWLIST defaults to Render's per-service RENDER_EXTERNAL_URL, which previews also get, with the preview's own URL. For example: ORIGIN_ALLOWLIST=${ORIGIN_ALLOWLIST:-$RENDER_EXTERNAL_URL} node apps/api/dist/index.js.
    • FRONT_DIST_DIR=apps/front/dist.
    • PORT is injected by Render.
    • previews.generation: automatic, autoDeployTrigger: checksPass, healthCheckPath: /.
  • scripts/setup-render.sh is built from .claude/skills/wizard/template.sh, with the library above the STAGES marker untouched. Its 7 stages:
    1. Preflight: render.yaml is on origin/main.
    2. Render account sign-up with GitHub.
    3. Give Render's GitHub app access to this repo only.
    4. Dashboard → New → Blueprint → Apply.
    5. First deploy: capture the production URL, and optionally run gh repo edit --homepage (behind confirm).
    6. curl smoke test: /, a client route, and /rpc with an allowed origin vs a foreign one (403).
    7. Open a throwaway draft PR, check that Render posts a preview, then close it (behind confirm).
  • README gets a short Deploy section linking the wizard and the production URL.

Testing Decisions

  • Unit or integration tests through the serveFront seam:
    • an asset file is served with its content type;
    • an unknown non-/rpc path returns index.html;
    • /rpc/* is never shadowed (a foreign origin still gets 403, an unknown procedure still 404).
  • loadConfig tests cover the new optional key.
  • The wizard and render.yaml are checked with bash -n/shellcheck and a static trace. They are not run in CI.
  • The gate is the five commands in CONTRIBUTING.md.

Out of Scope

  • A custom domain.
  • Keeping the instance awake (cron pings).
  • Persisting Rooms.
  • SSE heartbeat tuning.
  • Moving off Render (Hetzner/Coolify, Cloudflare Durable Objects).

Further Notes

Unverified until the first deploy:

  • that corepack works in Render's native Node build;
  • that startCommand runs through a shell, which the ${...:-...} expansion needs.

The wizard's smoke test (stage 6) is what proves both. Render's docs say PR previews are "billed at the same rate as your base service", which is free for a free service. They count against the 750 free instance-hours per month.

> _Imported from GitHub [amaurycoudr/chess-project#233](https://github.com/amaurycoudr/chess-project/issues/233), opened by [@amaurycoudr](https://github.com/amaurycoudr) on 2026-10-04 19:26 UTC._ > _Closed on GitHub on 2026-10-04 21:01 UTC as completed._ <!-- github-issue:233 --> --- ## Problem Statement The app has no deployment. Nobody can play it without cloning the repo, and a PR can't be tried in a browser before it merges. ## Solution Deploy to a single **free Render web service**, with **automatic PR previews**. The Node API serves the built front as well as `/rpc`, so both share one origin, as `apps/front/src/room/api/client.ts` assumes. Render is configured by a committed `render.yaml` blueprint. A one-off wizard script walks the owner through the dashboard setup. Why Render free over the alternatives (benchmarked in the session that produced this spec): - **Cost and setup:** $0, no card, built-in single-service PR previews. - **Accepted trade-off:** the instance sleeps after 15 min idle and takes ~1 min to wake. Sleeping and redeploying both wipe in-memory Rooms, which fits the Rooms' existing 30-minute TTL. - **Next step if that hurts:** a Hetzner VM with Coolify, from the same artifact. ## User Stories 1. As a player, I open the production URL and the SPA loads; deep links such as `/room/ABC123` load the SPA too. 2. As a player, I create and play an online game on the production URL, and moves arrive over SSE. 3. As the owner, every PR gets its own preview URL, with an API that accepts that preview's origin. 4. As the owner, production deploys only after CI on `main` is green. 5. As the owner, I run `scripts/setup-render.sh` once and it walks me through the Render dashboard, then smoke-tests the deploy. ## Seams - **Static front serving:** a new API module, e.g. `serveFront(app, { root })`, tested by calling `app.fetch` against a temporary directory fixture. It is wired in `apps/api/src/index.ts` only, so `createApp` stays I/O-free (ADR 0009). - **Config:** `loadConfig` (`apps/api/src/config.ts`) gains an optional front directory (e.g. `FRONT_DIST_DIR`). Unset means no static serving, so dev and e2e are unchanged. ## Implementation Decisions - **Static serving** uses `serveStatic` from `@hono/node-server/serve-static` for files under the front dir. - **SPA fallback:** any other `GET` that is not under `/rpc` returns `index.html`. `/rpc/*` behaviour is untouched, including the origin guard and the 404 for unmatched procedures. - **`render.yaml`** at the repo root, one web service: - `name: chess-project`, `runtime: node`, `plan: free`, `region: frankfurt`. - `buildCommand: corepack enable && pnpm install --frozen-lockfile && pnpm build`. Node comes from `.nvmrc` (24). - `startCommand` runs `node apps/api/dist/index.js` directly, not `pnpm start`. That way the `apps/api/.env.local` written by `postinstall` (`env:sync`) cannot apply. - `ORIGIN_ALLOWLIST` defaults to Render's per-service `RENDER_EXTERNAL_URL`, which previews also get, with the preview's own URL. For example: `ORIGIN_ALLOWLIST=${ORIGIN_ALLOWLIST:-$RENDER_EXTERNAL_URL} node apps/api/dist/index.js`. - `FRONT_DIST_DIR=apps/front/dist`. - `PORT` is injected by Render. - `previews.generation: automatic`, `autoDeployTrigger: checksPass`, `healthCheckPath: /`. - **`scripts/setup-render.sh`** is built from `.claude/skills/wizard/template.sh`, with the library above the STAGES marker untouched. Its 7 stages: 1. Preflight: `render.yaml` is on `origin/main`. 2. Render account sign-up with GitHub. 3. Give Render's GitHub app access to this repo only. 4. Dashboard → New → Blueprint → Apply. 5. First deploy: capture the production URL, and optionally run `gh repo edit --homepage` (behind `confirm`). 6. curl smoke test: `/`, a client route, and `/rpc` with an allowed origin vs a foreign one (403). 7. Open a throwaway draft PR, check that Render posts a preview, then close it (behind `confirm`). - **README** gets a short Deploy section linking the wizard and the production URL. ## Testing Decisions - Unit or integration tests through the `serveFront` seam: - an asset file is served with its content type; - an unknown non-`/rpc` path returns `index.html`; - `/rpc/*` is never shadowed (a foreign origin still gets 403, an unknown procedure still 404). - `loadConfig` tests cover the new optional key. - The wizard and `render.yaml` are checked with `bash -n`/shellcheck and a static trace. They are not run in CI. - The gate is the five commands in `CONTRIBUTING.md`. ## Out of Scope - A custom domain. - Keeping the instance awake (cron pings). - Persisting Rooms. - SSE heartbeat tuning. - Moving off Render (Hetzner/Coolify, Cloudflare Durable Objects). ## Further Notes Unverified until the first deploy: - that corepack works in Render's native Node build; - that `startCommand` runs through a shell, which the `${...:-...}` expansion needs. The wizard's smoke test (stage 6) is what proves both. Render's docs say PR previews are "billed at the same rate as your base service", which is free for a free service. They count against the 750 free instance-hours per month.
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-04 21:01:59 +00:00
root commented 2026-10-13 08:39:13 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #240

mentioned in issue #240
root commented 2026-10-13 10:19:58 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 3bc0188c69

mentioned in commit 3bc0188c694151a34f4db9912b1e24f1a15641ef
root commented 2026-10-13 10:19:58 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 2bb8339615

mentioned in commit 2bb8339615e159a5a4427288cf108fa595ed480d
root commented 2026-10-13 10:19:59 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit fdb3090cfe

mentioned in commit fdb3090cfe16d99fb9347e7be7d532d28afab6b2
root commented 2026-10-13 10:19:59 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 35887424fe

mentioned in commit 35887424fe5553f52aeb6b25e3fd8afa0e068df0
root commented 2026-10-13 10:19:59 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 7d8c70ce67

mentioned in commit 7d8c70ce67aa26dd1081d8698368c1316b8f7285
root commented 2026-10-13 10:20:00 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit e4afbb77f3

mentioned in commit e4afbb77f3a4f28359ac8829163ee4ed09509689
root commented 2026-10-13 10:20:00 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit f54b6521c5

mentioned in commit f54b6521c52a5a1db47f0c1edec6f2fe53dcbb6c
root commented 2026-10-13 10:20:01 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit e8267d55d5

mentioned in commit e8267d55d5c4eb173bf1adbccbb72e807c62c4ef
root commented 2026-10-13 10:20:01 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 5a3f7d9a6d

mentioned in commit 5a3f7d9a6d8d67d196b314e1912ca8719ff4701f
root commented 2026-10-13 10:20:02 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit a7d5df0d86

mentioned in commit a7d5df0d86be3f85da39cca2a0e28741ca71b4b1
root commented 2026-10-13 10:20:03 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 57eeef21bc

mentioned in commit 57eeef21bcb6e467911c3e9ccadb10f3f9dc6442
root commented 2026-10-13 10:20:03 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 658d7acb8d

mentioned in commit 658d7acb8d9b288fcee18a4b03f57d3d4493cade
root commented 2026-10-13 10:20:05 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 32f3f5474a

mentioned in commit 32f3f5474acf6364c2c33ab02a27c03c54abad71
root commented 2026-10-13 10:20:06 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit d525aeca96

mentioned in commit d525aeca96e71f560799c401dab6c6621de5a5a9
root commented 2026-10-13 10:20:06 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 033490386d

mentioned in commit 033490386d07a0814eff632bbd42bcc6b3543d52
root commented 2026-10-13 10:20:07 +00:00 (Migrated from gitlab.core-project.space)

mentioned in commit 859853ff40

mentioned in commit 859853ff4015a5cc3f953d339c3879d0dba07be4
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amaurycoudr/chess-project#233
No description provided.