Skip to content

feat(gastown): Database Schema & Provisioning API#294

Open
jrf0110 wants to merge 1 commit into271-gt-prop-afrom
282-db-schema-provisioning
Open

feat(gastown): Database Schema & Provisioning API#294
jrf0110 wants to merge 1 commit into271-gt-prop-afrom
282-db-schema-provisioning

Conversation

@jrf0110
Copy link
Contributor

@jrf0110 jrf0110 commented Feb 17, 2026

Summary

Closes #282

  • Add gastown_towns and gastown_rigs Postgres tables with Drizzle schema, migration, partial unique indices, and enum check constraints
  • Create deterministic sandbox ID generation (gastownSandboxId) from userId + townName, following KiloClaw's base64url pattern
  • Create Fly.io Machines API client (fly-client.ts) for volume/machine CRUD operations
  • Implement tRPC router with createTown, destroyTown, getTownStatus, listTowns, stopTown, startTown, addRig, removeRig — all using baseProcedure (authenticated)
  • Provisioning flow: insert DB row → create Fly volume → create Fly machine → update DB with Fly IDs; rolls back (soft-delete + volume cleanup) on failure
  • Destroy flow: soft-delete DB row first, then best-effort Fly resource cleanup (ignores 404s)

Files

File Description
src/db/migrations/0017_gastown.sql Migration for both tables
src/db/schema.ts Drizzle schema + type exports
src/lib/gastown/sandbox-id.ts Deterministic sandbox ID
src/lib/gastown/fly-client.ts Fly.io API client
src/lib/config.server.ts Gastown env var exports
src/routers/gastown-router.ts tRPC router
src/routers/root-router.ts Register gastown router

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Store the generated key in the gastown_towns row (e.g. in the config jsonb column), or
  2. Require GASTOWN_INTERNAL_API_SECRET to be set (throw in getFlyConfig if missing), or
  3. 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'`),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
.where(sql`status = 'active'`),
.where(sql`"gastown_rigs"."status" = 'active'`),

@kiloconnect
Copy link
Contributor

kiloconnect bot commented Feb 17, 2026

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
src/lib/gastown/sandbox-id.ts 18 Base64 encoding (not hashing) will exceed 63-char limit for many valid town names, causing runtime errors
src/routers/gastown-router.ts 141 Error cleanup in createTown only deletes volume but not the Fly machine — machine leak if DB update fails
src/routers/gastown-router.ts 97 GASTOWN_INTERNAL_API_SECRET defaults to empty string; random UUID fallback is never persisted, making sandbox auth unrecoverable

SUGGESTION

File Line Issue
src/db/schema.ts 3041 Unqualified status = 'active' in unique index where clause doesn't match table-qualified form in migration SQL — may cause Drizzle Kit drift
Files Reviewed (7 files)
  • src/db/migrations/0017_gastown.sql - 0 issues
  • src/db/schema.ts - 1 issue
  • src/lib/config.server.ts - 0 issues
  • src/lib/gastown/fly-client.ts - 0 issues
  • src/lib/gastown/sandbox-id.ts - 1 issue
  • src/routers/gastown-router.ts - 2 issues
  • src/routers/root-router.ts - 0 issues

Fix these issues in Kilo Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant