# Static Client Asset Split Plan

[doc->REQ-DEP-01] [doc->REQ-DEP-04] [doc->REQ-CLI-08]

## Goal

Make client-only staging fixes deploy without rebuilding and pushing the full server image, while adding no new paid infrastructure.

## Current Cost

The staging workflow builds the Vite client into `apps/server/public/`, then bakes that directory into the Fly server image. Any client bundle change becomes a new Docker image layer and must be pushed through the Fly registry with the server runtime.

That is reliable, but too expensive for debug iteration because hashed client assets are static files.

## Zero-Extra-Cost Target

Use the existing Fly app and its existing persistent volume as the first split point:

- Keep the Node/Colyseus server image for server code and dependencies.
- Upload the built Vite output to `/data/client-assets/releases/<git-sha>/` on the same Fly machine.
- Atomically update `/data/client-assets/current` to point at that release.
- Serve static files from `STATIC_ASSETS_DIR=/data/client-assets/current` when it exists.
- Fall back to bundled `/app/public` if the volume asset directory is missing.

This adds no new host, bucket, CDN, or paid service. It only reuses the volume already attached for SQLite/Litestream state.

## Deployment Shape

1. Build client in CI:

   ```bash
   pnpm --filter @rebno/client build:staging
   ```

2. Package only the static output:

   ```bash
   tar -czf client-assets-${GITHUB_SHA}.tgz -C apps/server/public .
   ```

3. Upload the tarball to the Fly machine using `flyctl ssh sftp` or an equivalent `flyctl ssh console` extraction flow.

4. Extract to a content-addressed release directory:

   ```bash
   mkdir -p /data/client-assets/releases/${GITHUB_SHA}
   tar -xzf /tmp/client-assets-${GITHUB_SHA}.tgz -C /data/client-assets/releases/${GITHUB_SHA}
   ln -sfn /data/client-assets/releases/${GITHUB_SHA} /data/client-assets/current
   ```

5. Run a health check that confirms:

   - `/` returns the new `index.html`.
   - One hashed JS asset from the Vite manifest returns `200`.
   - `/healthz` still returns `200`.

6. Garbage collect old releases after a successful deploy:

   ```bash
   ls -1dt /data/client-assets/releases/* | tail -n +6 | xargs -r rm -rf
   ```

## Server Change

Change the static mount in `apps/server/src/index.ts` from a hard-coded `public` path to a fallback chain:

1. If `STATIC_ASSETS_DIR` is set and exists, serve that directory.
2. Else serve `join(__dirname, '..', 'public')`.

For staging/prod Fly config, set:

```toml
[env]
STATIC_ASSETS_DIR = "/data/client-assets/current"
```

The server image should keep a minimal bundled public fallback so a brand-new machine still serves a page before the first split asset upload.

## CI Workflow

Add a client-only fast path:

- If the diff only touches `apps/client/**` and packages used only by the client, skip Docker build and Fly image deploy.
- Build and upload the static tarball.
- Switch `/data/client-assets/current`.
- Run the three cheap checks above.

Keep the current full image path for:

- `apps/server/**`
- `packages/protocol/**`
- `packages/game-logic/**`
- `packages/db/**`
- Dockerfile, Fly config, lockfile, or workflow changes

## Rollback

Rollback is just a symlink switch:

```bash
ln -sfn /data/client-assets/releases/<previous-sha> /data/client-assets/current
```

No image rollback is required for client-only failures.

## Later Upgrade

If bandwidth or global latency becomes a real issue, move the same release directories to a static host or object store:

- Cloudflare Pages free tier for the Vite output.
- Tigris bucket if the project already has usable included capacity.
- A CDN in front of either option.

Do this only after the volume-backed split proves the workflow shape. The volume-backed path is the lowest-cost migration because it changes deployment mechanics without adding a new production dependency.
