Skip to content

Commit 7cd390c

Browse files
crthplbriansmileyclaude
authored
Multi-cohort support with per-member initial balance (#358)
Co-authored-by: Brian <briantsmiley42@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b200854 commit 7cd390c

82 files changed

Lines changed: 4569 additions & 1413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
backend/target
2+
backend/data
3+
backend/*.sqlite*
4+
backend/remote-dbs
5+
backend/uploads
6+
frontend/node_modules
7+
frontend/.svelte-kit
8+
frontend/build
9+
node_modules
10+
.git
11+
.claude

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ node_modules
55
backend/uploads
66
.dev-ports
77
.playwright-cli
8+
.claude/worktrees/
9+
.vercel
810
.claude

.vercelignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
backend/target
2+
backend/data
3+
backend/*.sqlite*
4+
backend/remote-dbs
5+
.git
6+
.claude
7+
node_modules

AGENTS.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ Copy the appropriate template to `frontend/.env` for your use case:
256256

257257
#### `backend/src/main.rs`
258258
- **Entry point** for the backend server
259-
- Initializes Axum router with WebSocket handler at `/api`, image upload/serving routes, and Airtable sync endpoint
259+
- Initializes Axum router with per-cohort WebSocket handler at `/api/ws/:cohort_name`, admin/user REST endpoints, and image upload/serving routes
260260
- Implements port binding with fallback logic (tries sequential ports if in use)
261261
- Manages uploads directory and request body size limits
262262
- Depends on `lib.rs` for `AppState`, `handle_socket.rs` for WebSocket handling
@@ -266,7 +266,7 @@ Copy the appropriate template to `frontend/.env` for your use case:
266266
- Defines `AppState` struct containing DB connection pool, pub/sub subscriptions, and rate limiters
267267
- Configures separate admin/user rate limit quotas for expensive queries and mutations
268268
- Includes protobuf module generation via `build.rs`
269-
- Declares modules: `websocket_api`, `auth`, `db`, `handle_socket`, `subscriptions`, `airtable_users`, `convert`, `seed`, `test_utils`
269+
- Declares modules: `websocket_api`, `auth`, `db`, `global_db`, `handle_socket`, `subscriptions`, `convert`, `seed`, `test_utils`
270270

271271
#### `backend/src/handle_socket.rs`
272272
- Core WebSocket request/response handler (~1150 lines)
@@ -301,11 +301,6 @@ Copy the appropriate template to `frontend/.env` for your use case:
301301
- Implements `From` trait for all domain types (Portfolio, Market, Order, Trade, Transfer, Account, Auction)
302302
- Converts Rust Decimal to protobuf floats, timestamps to protobuf Timestamp format
303303

304-
#### `backend/src/airtable_users.rs`
305-
- Syncs Airtable student records to Kinde and database
306-
- Creates Kinde accounts and DB entries, assigns initial balances based on product ID
307-
- Caches Kinde API tokens, logs errors back to Airtable
308-
309304
#### `backend/src/seed.rs`
310305
- Development seed data (feature-gated behind `dev-mode`)
311306
- Seeds fresh databases with test accounts (Alice, Bob, Charlie, Admin), markets, orders, and trades

DEPLOY.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.

backend/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ target/
33
db.sqlite
44
db.sqlite-shm
55
db.sqlite-wal
6+
*.sqlite
7+
*.sqlite-shm
8+
*.sqlite-wal

backend/.sqlx/query-aae2c0449b7f47212ceac7d9fc1231968e23b191d4c9f7e9d414e9b37f86872d.json renamed to backend/.sqlx/query-411e2737e334e6e70fc1b288e146ae70c4360d7c7f087942f34fc880da0960c7.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/.sqlx/query-433242ec70fe924887b6fde177112404d773129357a3845da91d0ec752fd143a.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/.sqlx/query-943427a7ba64285bf8dc6f35a61f4e025b5c2ec12c2448768425245ffe49290d.json renamed to backend/.sqlx/query-6026fe9a76ae8dd30ead15b3c809a78a546129d2e9bf4e99993b4c92e07f6ad9.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/.sqlx/query-0c7c90c8753c66208ee11c0cc2826e7044881937d5d1aadccb5dc412dec9f55a.json renamed to backend/.sqlx/query-62de5a5e7713956efa9ee7f382a36c1c1646d5f224b97ba88985192518773f92.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)