|
| 1 | +# Deployment |
| 2 | + |
| 3 | +## Architecture |
| 4 | + |
| 5 | +- **Frontend**: Static SPA on Vercel (SvelteKit with `adapter-static`) |
| 6 | +- **Backend**: Rust binary on Fly.io (Docker, SQLite with persistent volume) |
| 7 | +- Frontend and backend are on different domains, so HTTP API calls use cross-origin requests with CORS. |
| 8 | + |
| 9 | +## Staging |
| 10 | + |
| 11 | +- **Frontend**: https://platform-staging-five-gamma.vercel.app (Vercel project `platform-staging` under `trading-bootcamp` team) |
| 12 | +- **Backend**: https://trading-bootcamp-staging.fly.dev (Fly app `trading-bootcamp-staging`) |
| 13 | + |
| 14 | +### Deploy staging backend |
| 15 | + |
| 16 | +```bash |
| 17 | +fly deploy --config backend/fly.staging.toml |
| 18 | +``` |
| 19 | + |
| 20 | +### Deploy staging frontend |
| 21 | + |
| 22 | +```bash |
| 23 | +vercel --prod --scope trading-bootcamp |
| 24 | +``` |
| 25 | + |
| 26 | +### Vercel env vars (Production environment) |
| 27 | + |
| 28 | +| Variable | Value | |
| 29 | +|----------|-------| |
| 30 | +| `PUBLIC_KINDE_CLIENT_ID` | `a9869bb1225848b9ad5bad2a04b72b5f` | |
| 31 | +| `PUBLIC_KINDE_DOMAIN` | `https://account.trading.camp` | |
| 32 | +| `PUBLIC_KINDE_REDIRECT_URI` | `https://platform-staging-five-gamma.vercel.app` | |
| 33 | +| `PUBLIC_SERVER_URL` | `wss://trading-bootcamp-staging.fly.dev/api` | |
| 34 | +| `PUBLIC_TEST_AUTH` | `false` | |
| 35 | + |
| 36 | +**Important**: When setting env vars via `vercel env add`, pipe with `printf` (not `echo`) to avoid embedding trailing newlines: |
| 37 | +```bash |
| 38 | +printf 'value' | vercel env add VAR_NAME production --scope trading-bootcamp |
| 39 | +``` |
| 40 | + |
| 41 | +### Kinde setup |
| 42 | + |
| 43 | +Add the frontend URL to "Allowed callback URLs" in the Kinde application settings for client ID `a9869bb1225848b9ad5bad2a04b72b5f`. |
| 44 | + |
| 45 | +## Code changes required for deployment |
| 46 | + |
| 47 | +### 1. `.dockerignore` |
| 48 | + |
| 49 | +Excludes `backend/target`, `backend/data`, SQLite files, `node_modules`, `.git`, `.claude` etc. Without this, Docker context transfer is ~733MB instead of ~700KB. |
| 50 | + |
| 51 | +### 2. `backend/Dockerfile` modifications |
| 52 | + |
| 53 | +- **`ENV SQLX_OFFLINE=true`**: SQLx does compile-time query checking against a live database by default. In Docker there's no database, so offline mode uses the pre-generated `.sqlx/` cache instead. |
| 54 | +- **`COPY ./backend/global_migrations /app/global_migrations`**: The multi-cohort feature added a `global_migrations/` directory that needs to be in the runtime image. |
| 55 | + |
| 56 | +### 3. `.vercelignore` |
| 57 | + |
| 58 | +Excludes the same heavy directories from Vercel uploads. |
| 59 | + |
| 60 | +### 4. CORS: `backend/Cargo.toml` + `backend/src/main.rs` |
| 61 | + |
| 62 | +Since frontend (Vercel) and backend (Fly.io) are on different domains, the browser blocks cross-origin API requests. The fix: |
| 63 | + |
| 64 | +- Added `"cors"` feature to `tower-http` in `Cargo.toml` |
| 65 | +- Replaced the manual `SetResponseHeaderLayer` for `Access-Control-Allow-Origin: *` with `CorsLayer::permissive()`, which properly handles preflight OPTIONS requests and allows the `Authorization` header |
| 66 | + |
| 67 | +### 5. Cross-origin HTTP API calls: `frontend/src/lib/apiBase.ts` |
| 68 | + |
| 69 | +In development, the Vite dev server proxies `/api/*` to the backend (see `frontend/vite.config.ts`). In production, there's no proxy — the frontend and backend are on different domains. |
| 70 | + |
| 71 | +`apiBase.ts` derives the HTTP base URL from `PUBLIC_SERVER_URL`: |
| 72 | +- `wss://host.fly.dev/api` → `https://host.fly.dev` |
| 73 | +- `ws://localhost:8080` → `http://localhost:8080` |
| 74 | + |
| 75 | +`cohortApi.ts` and `adminApi.ts` use `API_BASE` to make absolute URL requests instead of relative `/api/...` paths. This works in both dev (Vite proxy still intercepts) and production (direct cross-origin requests). |
| 76 | + |
| 77 | +### 6. `frontend/src/routes/[cohort_name]/docs/[slug]/+page.svelte` |
| 78 | + |
| 79 | +Fixed markdown import paths — the file moved one level deeper into `[cohort_name]/` so the relative imports needed an extra `../` (e.g. `../../../../../docs/` → `../../../../../../docs/`). |
| 80 | + |
| 81 | +### 7. `backend/fly.staging.toml` |
| 82 | + |
| 83 | +Fly.io config for the staging app. Key differences from production (`fly.toml`): |
| 84 | +- `app = 'trading-bootcamp-staging'` |
| 85 | +- Separate persistent volume mount (`trading_bootcamp_staging`) |
| 86 | +- Staging database path |
| 87 | + |
| 88 | +## Gotchas |
| 89 | + |
| 90 | +### Stale `.sqlx/` cache breaks Fly builds |
| 91 | + |
| 92 | +Because the Dockerfile sets `SQLX_OFFLINE=true`, the build relies entirely on the committed `.sqlx/` cache. If queries in `db.rs` change (e.g., new column like `account.color`) without regenerating the cache, Fly builds fail with cryptic errors like: |
| 93 | + |
| 94 | +``` |
| 95 | +error: key must be a string at line 3 column 1 |
| 96 | + --> src/db.rs:262:9 |
| 97 | +``` |
| 98 | + |
| 99 | +This is *not* a syntax error in `db.rs` — it's SQLx failing to parse/find a matching cache file. Fix: |
| 100 | + |
| 101 | +```bash |
| 102 | +cd backend |
| 103 | +sqlx migrate run # ensure local DB matches migrations |
| 104 | +cargo sqlx prepare -- --features dev-mode --tests # regenerate .sqlx/ |
| 105 | +git add backend/.sqlx && git commit # commit the regenerated cache |
| 106 | +``` |
| 107 | + |
| 108 | +Always include `--tests` so queries used only in tests are cached too. CLAUDE.md's "Required Checks" section mentions this but it's easy to miss — treat any SQL change in `db.rs` as requiring a cache regen before pushing. |
| 109 | + |
| 110 | +### Vercel deployment URL vs. production alias |
| 111 | + |
| 112 | +`vercel --prod` prints a line like: |
| 113 | + |
| 114 | +``` |
| 115 | +Production: https://platform-staging-fgbm5rn8e-trading-bootcamp.vercel.app |
| 116 | +``` |
| 117 | + |
| 118 | +That is the **immutable deployment URL** for this specific build, not a different project. The stable production alias (`https://platform-staging-five-gamma.vercel.app`) is updated to point to this deployment. Verify with `vercel project ls --scope trading-bootcamp` — the `Latest Production URL` column shows the alias. |
| 119 | + |
| 120 | +### Two `.vercel/` project links in the repo |
| 121 | + |
| 122 | +Both `/.vercel/` (repo root) and `/frontend/.vercel/` exist and point to projects named `platform-staging`, but with **different org IDs**. Running `vercel` from the repo root uses the root link, which is the correct one (the `trading-bootcamp` team's `platform-staging` project that aliases to `platform-staging-five-gamma.vercel.app`). Do not `cd frontend` before deploying. |
0 commit comments