How I fixed 6 critical bugs to seed a 26-model AI routing database from a Termux terminal on Android — using proot Ubuntu, raw SQL, and a lot of detective work.
I run QuantumFlow AI — an AI routing platform billed as "the Stripe for AI Routing." It routes requests across 26 AI models (13 sovereign local + 13 cloud fallback), orchestrated by a bio-inspired octopus architecture with 12 quantum brains and 18 quantum arms. The live site runs on Vercel, the database on Neon PostgreSQL, and the source on a private GitHub repo.
And for the last 8 hours, I did all the critical infrastructure work from my Android phone, in a Termux terminal, while sitting in a moving vehicle.
This is the story of the 6 bugs that tried to stop the launch — and the forensic debugging that turned a broken Prisma connection into a fully seeded production database, ready for our Hacker News debut on Tuesday.
The Setup: Termux + Next.js + Prisma + Neon
Here's the stack that was supposed to work:
· Device: Android phone running Termux
· App: Next.js 16 monorepo (quantumflow-ai-ecosystem)
· ORM: Prisma 6.19.3
· DB: Neon PostgreSQL (serverless Postgres with a pooler endpoint)
· Deploy: Vercel auto-deploy on push to main
· Engine: Node.js 22 LTS
The plan was simple: open Termux, run npx prisma db push, run the seed script, push to GitHub, let Vercel deploy. Done in 5 minutes.
It did not go that way.
Bug #1: Prisma Refused to Connect on Termux
The first command failed before it even reached the database:
$ npx prisma db push
prisma:warn Prisma detected unknown OS "android" and may not work as expected. Defaulting to "linux".
Error: Schema engine error:
That's it. No further output. Just Schema engine error: followed by silence.
Root Cause
Prisma's schema engine ships pre-compiled binaries for linux-arm64 (glibc), darwin-arm64, and win32-x64. Termux runs Android, which uses the bionic libc instead of glibc. The linux-arm64 binary refuses to execute against bionic, so Prisma's schema engine silently crashes at startup.
The prisma:warn line is the giveaway — Prisma sees process.platform === 'android', doesn't recognize it, defaults to the Linux binary, and that binary fails to load.
The Fix: proot-distro Ubuntu
I installed a real Linux environment inside Termux using proot-distro:
pkg install proot-distro
proot-distro install ubuntu
proot-distro login ubuntu --bind /data/data/com.termux/files/home:/home
The --bind flag mounts my Termux home directory at /home inside the Ubuntu proot, so my existing project at ~/projects/quantumflow-ai-ecosystem becomes /home/projects/quantumflow-ai-ecosystem. Inside Ubuntu, I installed Node 22 via NodeSource, ran npm install --legacy-peer-deps, and finally:
$ npx prisma db push
🚀 Your database is now in sync with your Prisma schema. Done in 2.65s
✔ Generated Prisma Client (v6.19.3) to ./node_modules/@prisma/client in 2.13s
Schema pushed. 164 tables created. On to the seed.
Lesson: Termux is great for development, but anything that ships pre-compiled native binaries (Prisma, sharp, better-sqlite3, node-gyp modules) needs proot-distro to provide a glibc environment. Keep a Ubuntu proot handy — it's a 300MB one-time install that saves hours of debugging.
Bug #2: prisma.aiModel Was undefined
The seed script crashed on the very first line:
$ npx tsx db/seed-intelligent.ts
🌱 QuantumFlow AI — Intelligent Database Seed v595.0.1
📦 Seeding AI Models (13 local + 15 cloud = 28)...
❌ Seed failed: TypeError: Cannot read properties of undefined (reading 'upsert')
at main (/home/.../db/seed-intelligent.ts:179:26)
prisma.aiModel.upsert(...) was failing because prisma.aiModel was undefined. But the schema had model AiModel { ... }, so Prisma should have generated prisma.aiModel — right?
The Investigation
I ran a diagnostic script to list every model accessor the generated Prisma Client actually exposed:
$ npx tsx test-prisma.ts
aiModel type: undefined
Total models: 161
Models containing "odel":
aiModelPreference
globalFederatedModel
mLModel
modelTraining
modelUpdate
Wait — aiModel was undefined, but aiModelPreference existed. The client knew about other models but not the one I needed. Something was very wrong.
Root Cause: A SQLite Adapter Was Hijacking the Client
The smoking gun was in lib/db.ts:
// lib/db.ts — the broken version
import { PrismaClient } from '@prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
const connectionString = process.env.DATABASE_URL || 'file:./db/custom.db';
async function createPrismaClient() {
const adapterFactory = new PrismaBetterSqlite3({
url: connectionString,
});
const adapter = await adapterFactory.connect();
const prisma = new PrismaClient({ adapter });
return prisma;
}
export const db = await createPrismaClient();
This file was 93 versions out of date (v502.2.0-TRANSCENDENT-UNITED vs the current v595.0.1). It was written when the project used SQLite. It was still instantiating a SQLite adapter, pointing at a non-existent custom.db file, and passing that adapter to PrismaClient. The adapter override caused Prisma to ignore the entire PostgreSQL schema — so prisma.aiModel was undefined because, from the SQLite adapter's perspective, no models existed.
The Fix: Strip the Adapter, Use Plain PrismaClient
// lib/db.ts — the clean version
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export const db = prisma;
After this rewrite, prisma.aiModel came alive:
$ npx tsx test-prisma.ts
aiModel type: object
Total models: 161
161 models accessible. The seed could finally run.
Lesson: When a Prisma model accessor is undefined after a successful prisma generate, the problem is almost never the schema — it's how the client is being instantiated. Check lib/db.ts for adapter overrides, custom output paths, or anything that bypasses the default @prisma/client import.
Bug #3: The Schema Casing Trap — AIModel vs AiModel
While debugging Bug #2, I noticed something in prisma/schema.prisma:
model AIModel {
id String @id @default(cuid())
name String @unique
...
}
model AIModelPreference {
...
}
Prisma generates client accessors by camelCasing the model name. For model AIModel, it generates prisma.aIModel (capital A, lowercase i). For model AiModel, it generates prisma.aiModel. My seed script was calling prisma.aiModel — which only works if the schema declares model AiModel.
The Fix
I renamed both model declarations:
model AiModel {
...
}
model AiModelPreference {
...
}
And updated the relation reference in model User:
model User {
...
aiModelPreferences AiModelPreference[] // was: AIModelPreference[]
}
Lesson: Prisma's model-name-to-accessor mapping is PascalCase → camelCase, but it's literal about the casing. AIModel becomes aIModel, not aiModel. If your codebase expects prisma.aiModel, your schema MUST say model AiModel. This is documented but easy to miss — and the error message (Unknown argument) doesn't immediately point to casing as the cause.
Bug #4: PostgreSQL's Strict jsonb Casts
The seed ran, but a chunk of it failed silently:
📦 Seeding Unified Storage entries...
⚠️ Unified storage seed skipped: PrismaClientKnownRequestError:
Raw query failed. Code: `42804`. Message: `ERROR: column "value" is of type
jsonb but expression is of type text
HINT: You will need to rewrite or cast the expression.`
Root Cause
PostgreSQL is strict about implicit casts. The seed was using $executeRawUnsafe with parameterized SQL:
await prisma.$executeRawUnsafe(
`INSERT INTO unified_storage_entries (key, value, tier, timestamp, ttl, metadata)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`,
organismMetadata.key,
JSON.stringify(organismMetadata.value), // ← text, but column is jsonb
'database',
organismMetadata.timestamp,
null,
JSON.stringify({ seeded: true, version: 'v595.0.1' }) // ← text, but column is jsonb
);
Prisma passes JSON.stringify(obj) as a text parameter. The columns value and metadata are jsonb. Postgres refuses to implicitly cast text → jsonb, so it errors with code 42804.
The Fix: Add Explicit ::jsonb Casts
await prisma.$executeRawUnsafe(
`INSERT INTO unified_storage_entries (key, value, tier, timestamp, ttl, metadata)
VALUES ($1, $2::jsonb, $3, $4, $5, $6::jsonb)
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()`,
...
);
But here's the twist: a different raw SQL statement in the same seed had the opposite bug. The supreme_systems table had all-TEXT columns, but the SQL was casting parameters to ::jsonb:
// ❌ WRONG — these columns are TEXT, not jsonb
VALUES ($1, $2::jsonb, $3, $4, $5, $6::jsonb)
So the fix was asymmetric:
· unified_storage_entries: ADD ::jsonb casts (columns ARE jsonb)
· supreme_systems: REMOVE ::jsonb casts (columns are TEXT)
Lesson: When using prisma.$executeRawUnsafe with PostgreSQL jsonb columns, you must add explicit ::jsonb casts on the parameter placeholders. But always verify the column types first — casting a text parameter to jsonb on a TEXT column fails just as hard.
Bug #5: The Pre-Push Hook's Unbound Variable
After committing the fixes, every git push failed with:
.githooks/pre-push: line 67: remote_sha: unbound variable
.githooks/pre-push: line 79: remote_sha: unbound variable
[pre-push] All hard gates passed. Push allowed.
The push succeeded (the hook was non-blocking for this path), but the errors were noisy and confusing.
Root Cause
The hook script started with set -euo pipefail (which makes unbound variables fatal), then had:
while read -r local_ref local_sha remote_ref _remote_sha; do
...
changed_files=$(git diff --name-only --diff-filter=d "$remote_sha" "$local_sha" 2>/dev/null)
^^^^^^^^^^^
❌ BUG: variable is _remote_sha, not remote_sha
Someone (probably an IDE auto-refactor) added a leading underscore to _remote_sha to suppress an "unused variable" warning — without realizing the variable WAS used later, just under the wrong name. With set -u, accessing the nonexistent $remote_sha triggered the unbound variable error.
The Fix
# Rename _remote_sha back to remote_sha
sed -i 's/while read -r local_ref local_sha remote_ref _remote_sha; do/while read -r local_ref local_sha remote_ref remote_sha; do/' .githooks/pre-push
After the fix, two clean test pushes confirmed the bug was gone:
[pre-push] All hard gates passed. Push allowed.
b65b9d535..70572df00 main -> main
Lesson: set -u (treat unset variables as errors) is a great safety net, but it amplifies the cost of variable-name typos. If your shell script uses set -u and a refactor renames a variable, every reference must be updated — not just the declaration.
Bug #6: The QSO CLI's Phantom Commands
The final bug was cosmetic but educational. My QSO CLI (qso.cjs, a 3,711-line Node script with 230+ commands) listed status and health in its help text:
🚀 Service Management (8 commands):
start - Start frontend server
tunnel - Start Cloudflare tunnel
full - Start all services
stop - Stop all services
status - Check service status ← listed here
health - Check service health ← listed here
logs - Show service logs
metrics - Show metrics status
But running them returned:
$ node qso.cjs status
❌ Unknown command: status
ℹ️ Use "help" to see available commands
Root Cause
The CLI's dispatch table used an if (command === '...') chain starting at line 3232. I grepped for case statements (zero results — wrong pattern), then for === 'start' (zero results — wrong quotes), then finally for args[0] and found the actual dispatch at line 3232:
const command = args[0]?.toLowerCase();
if (command === 'g-commit' || command === 'gc') { ... }
if (command === 'version' || command === '--version' || command === '-v') { ... }
if (command === 'help' || command === '--help' || command === '-h' || !command) { ... }
// ... 200+ more if-blocks ...
if (command === 'dev:status') return this.ecosystemEngine.devStatus();
// ...
// Unknown command fallback
console.log(`Unknown command: ${command}`);
The status and health cases were simply missing from the chain. The help text was generated separately (probably copy-pasted from an older version) and listed commands that the dispatch table never registered.
The Fix
A one-line sed insertion right before the "Unknown command" fallback:
sed -i '/^ \/\/ Unknown command$/i\ if (command === "status" || command === "health") { return this.ecosystemEngine.devStatus(); }' qso.cjs
After the fix:
$ node qso.cjs status
📊 DEV ECOSYSTEM STATUS
⚡ PM2 STATUS
┌────┬───────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
└────┴───────────┴──────────┴──────┴───────────┴──────────┴──────────┘
🌐 CADDY STATUS
✅ Caddy: RUNNING on :81
Lesson: Help text and dispatch tables drift apart over time. If your CLI has more than 50 commands, write a test that runs every command listed in the help text and asserts none return "Unknown command". I'll be adding that test next week.
The Final State
After fixing all 6 bugs, the database seeded cleanly:
Component Count
AI models 26 (13 local PRIMARY + 13 cloud fallback)
Quantum Brains 9
Quantum Arms 18
Supreme Systems 13
Emergent Capabilities 13
Total tables 164
Production health check from my cloud sandbox:
$ curl -sS https://quantumflow-ai-ecosystem.vercel.app/api/health
{
"status": "healthy",
"version": "v595.0.1",
"uptime": 4,
"checks": [
{"name": "edge-runtime", "status": "pass"},
{"name": "quantum-coherence", "status": "pass"},
{"name": "api-gateway", "status": "pass"}
],
"rateLimiting": {
"enabled": true,
"upstashEnabled": true,
"categories": {
"auth": {"limit": 5, "window": 60, "algorithm": "sliding"},
"ai-operations": {"limit": 10, "window": 60, "algorithm": "tokenBucket"},
...
}
}
}
Security headers — all 10 critical headers present:
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' ...
cross-origin-embedder-policy: credentialless
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-site
permissions-policy: camera=(), microphone=(), geolocation=(), payment=()
referrer-policy: origin-when-cross-origin
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
4 commits shipped in one session:
a3d1b7749 fix(qso+hooks): add status/health commands + fix pre-push unbound var
70572df00 test: pre-push hook fix verification
b65b9d535 test: pre-push hook fix verification
271304c66 chore: cleanup working tree + preserve audit artifacts
2dc3fb77a fix(db): PostgreSQL Prisma client + jsonb casts + AiModel casing
789c4f003 feat(db): intelligent seed — 28 AI models + organism metadata
The Meta-Lesson: Debugging Is Forensics
What made this session work wasn't any single fix — it was the discipline of treating each bug as a forensic investigation:
- Read the actual error message. Not the first line — the whole trace. The prisma:warn Prisma detected unknown OS "android" line was the key to Bug #1.
- Verify your assumptions. I assumed prisma.aiModel existed because the schema had model AiModel. The diagnostic script (Object.keys(prisma).filter(...)) proved it didn't — which redirected the investigation from "why isn't the model seeded?" to "why is the client broken?"
- Read the source file, not just the schema. The lib/db.ts SQLite adapter was the real culprit for Bug #2, but it lived 3 layers above where the error manifested.
- Asymmetric fixes are real. Bug #4 required ADDING ::jsonb casts in one place and REMOVING them in another. The "obvious" fix (add casts everywhere) would have made it worse.
- Cosmetic bugs still matter. Bug #6 (QSO CLI status command) didn't block anything functionally, but it eroded trust in the tooling. Fixing it took 30 seconds and made the whole CLI feel professional.
What's Next
The database layer is production-certified. The HN launch is Tuesday 8–10am ET. After we land our first 5 customers, the next milestone is adding DeepSeek V3.1 via API (~9× cheaper than GPT-4o) as a cloud fallback model — customer-funded development is the right discipline.
If you want to follow along, the live site is at https://quantumflow-ai-ecosystem.vercel.app, and I'll be posting the HN link to @AetheriusFlow when it goes live.
Building something wild from your phone? I'd love to hear about it. Drop a comment below or find me on X — I read everything.
Top comments (0)