feat(gastown): Database Schema & Provisioning API#294
feat(gastown): Database Schema & Provisioning API#294jrf0110 wants to merge 1 commit into271-gt-prop-afrom
Conversation
Add gastown_towns and gastown_rigs tables with Drizzle schema, Fly.io client, deterministic sandbox ID generation, and tRPC router with createTown, destroyTown, stopTown, startTown, listTowns, getTownStatus, addRig, and removeRig endpoints. Provisioning flow creates a Fly volume and machine with rollback on failure.
| export function gastownSandboxId(userId: string, townName: string): string { | ||
| const input = `gastown:${userId}:${townName}`; | ||
| const bytes = new TextEncoder().encode(input); | ||
| const encoded = bytesToBase64url(bytes); |
There was a problem hiding this comment.
WARNING: bytesToBase64url is a plain base64 encoding, not a hash — it expands input by ~33%. The router allows townName up to 100 chars, and userId can be long too. With the gastown: prefix the raw input can easily exceed 47 bytes, producing a base64 string longer than MAX_SANDBOX_ID_LENGTH (63). This means createTown will throw at runtime for many valid town names.
Consider using a hash (e.g. SHA-256 truncated to a fixed length) instead of raw base64 encoding, or tighten the townName max length to something that guarantees the encoded output stays under 63 chars.
| return { id: updated.id, status: updated.status }; | ||
| } catch (error) { | ||
| // Best-effort cleanup of Fly resources created before failure | ||
| if (volumeId) { |
There was a problem hiding this comment.
WARNING: Cleanup only deletes the volume but not the machine. If createMachine succeeds but the subsequent DB update (line 120-129) throws, the Fly machine is leaked — it will keep running with no corresponding DB record.
Track machineId similarly to volumeId and destroy it in the catch block:
let machineId: string | undefined;
// ... after createMachine:
machineId = machine.id;
// ... in catch:
if (machineId) {
try { await destroyMachine(flyConfig, machineId); } catch { /* best-effort */ }
}| volumeId = volume.id; | ||
|
|
||
| // Create Fly machine | ||
| const internalApiKey = GASTOWN_INTERNAL_API_SECRET || crypto.randomUUID(); |
There was a problem hiding this comment.
WARNING: GASTOWN_INTERNAL_API_SECRET defaults to '' in config.server.ts, which is falsy. This means in any environment where the env var isn't set, every createTown call generates a random UUID as the internal API key. That key is passed to the Fly machine's env but never persisted in the DB, so the backend can never authenticate to the sandbox later.
Either:
- Store the generated key in the
gastown_townsrow (e.g. in theconfigjsonb column), or - Require
GASTOWN_INTERNAL_API_SECRETto be set (throw ingetFlyConfigif missing), or - Derive the key deterministically from something recoverable.
| table => [ | ||
| uniqueIndex('UQ_gastown_rigs_town_name') | ||
| .on(table.town_id, table.rig_name) | ||
| .where(sql`status = 'active'`), |
There was a problem hiding this comment.
SUGGESTION: The where clause here uses unqualified sql`status = 'active'` but the generated migration SQL (line 36 of 0017_gastown.sql) uses the table-qualified form "gastown_rigs"."status" = 'active'. This mismatch may cause Drizzle Kit to detect schema drift and generate a spurious migration on the next drizzle-kit generate.
Consider table-qualifying it to match:
| .where(sql`status = 'active'`), | |
| .where(sql`"gastown_rigs"."status" = 'active'`), |
Code Review SummaryStatus: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (7 files)
|
Summary
Closes #282
gastown_townsandgastown_rigsPostgres tables with Drizzle schema, migration, partial unique indices, and enum check constraintsgastownSandboxId) from userId + townName, following KiloClaw's base64url patternfly-client.ts) for volume/machine CRUD operationscreateTown,destroyTown,getTownStatus,listTowns,stopTown,startTown,addRig,removeRig— all usingbaseProcedure(authenticated)Files
src/db/migrations/0017_gastown.sqlsrc/db/schema.tssrc/lib/gastown/sandbox-id.tssrc/lib/gastown/fly-client.tssrc/lib/config.server.tssrc/routers/gastown-router.tssrc/routers/root-router.ts