Prisma Schema Migration: How to Survive Local Hell in a Health Monorepo
Field lessons on the pitfalls of schema migrations in a medical monorepo with multiple databases and development environments.
Mario Inostroza
A couple of weeks ago, I faced a situation that every developer dreads: “everything worked yesterday, and today it doesn’t.” In Examya’s case, this translated into 7 test flows of the medical bot failing locally after applying fixes that worked perfectly in production.
What I discovered was a Prisma schema migration hell in a monorepo with multiple services, multiple databases, and multiple development environments. This is the story of how I solved the problems and the lessons I learned in the process.
The Problem: Why Were Local Tests Failing?
It all started with PR #323, which fixed critical bugs in the ExamIA medical bot. Production tests passed without issues, but running them locally resulted in strange errors:
NotFoundError: No exam_catalog table in public schemarelation "whatsapp_context" does not existInvalid prismaMainin the agents
The root cause: out-of-sync database schemas between environments.
The Pain: Databases in a Complex Monorepo
Examya is a monorepo with several services using different databases:
API Service (apps/api/):
- Uses
examya_devfor the FONASA exam catalog, users, etc. - 80+ tables including
exam_catalog,user,user_phone.
Agents Service (apps/agents/):
- Uses
examya_agentsfor WhatsApp context, order processing. - Agent-specific tables like
whatsapp_context,exam_results.
The problem: Incorrect local configurations pointing to databases with outdated or erroneous schemas.
Lesson #1: Never Trust db push in Production
The first big lesson came from practice: never use prisma db push in production. This command is only for rapid prototyping because it can cause data loss.
In my local case, the problem was even more subtle:
// apps/agents/.env INCORRECT
DATABASE_URL=examya_agents // ❌ This pointed to the wrong schema
DATABASE_URL_AGENTS=examya_agents_test // ❌ This wasn't the right schema either
The correct configuration should be:
// apps/agents/.env CORRECT
DATABASE_URL=examya_dev // ✅ For prismaMain (exam_catalog, users)
DATABASE_URL_AGENTS=examya_agents // ✅ For prismaAgents (whatsapp_context)
Lesson #2: node --watch Is a Silent Enemy
Another dangerous trap: using node --watch with services that depend on environment variables.
# Problematic command
dotenv -e .env.local -- node -r ts-node/register -r tsconfig-paths/register src/main.ts --watch
When prisma generate regenerated Prisma clients, node --watch restarted the process but without the environment variables. The new process used default credentials from the .env file instead of local ones.
The solution: always restart manually with the correct environment.
# Correct command (without --watch)
dotenv -e .env.local -- node -r ts-node/register -r tsconfig-paths/register src/main.ts
Lesson #3: The Trap of Duplicate Schemas
Prisma generates different clients for different schemas:
prismaMainfor the main schema inprisma/schema.prismaprismaAgentsfor the agents schema in its own directory.
The problem: if you only generate one, the other becomes obsolete. This caused “schema not applied” errors because the client files didn’t match the actual schema in the database.
The Recovery Process
Step 1: Identify the Actual State of the Databases
# Verify schema in examya_dev
psql $DATABASE_URL -c "\dt exam_catalog"
# Verify schema in examya_agents
psql $DATABASE_URL_AGENTS -c "\dt"
# Verify pgvector extension
psql $DATABASE_URL_AGENTS -c "SELECT extversion FROM pg_extension WHERE extname = 'pgvector';"
Step 2: Apply the Full Schema
The main schema of 80+ tables had never been correctly applied to the local examya_dev:
# Apply full schema accepting data loss
cd apps/api
DATABASE_URL=examya_dev npx prisma db push --accept-data-loss
# Seed FONASA data
npx tsx scripts/seed-fonasa-exams.ts
Step 3: Correct URL Configuration
Update .env in agents with the correct URLs:
# apps/agents/.env
DATABASE_URL=examya_dev
DATABASE_URL_AGENTS=examya_agents
DIRECT_URL_AGENTS=postgresql://user:pass@localhost:5432/examya_agents
Code That Broke (And How It Was Fixed)
Bug #1: Multi-exam Search with Commas
The original code joined exam names with ”, ” and searched as a single string:
// ❌ INCORRECT
const examNames = exams.join(", "); // "HEMOGRAMA, PERFIL LIPÍDICO"
const results = await prisma.exam_catalog.findMany({
where: { name: examNames }
});
// 0 results because "HEMOGRAMA, PERFIL LIPÍDICO" doesn't exist as a single record
The solution: search for each exam individually:
// ✅ CORRECT
const results = await Promise.all(
exams.map(exam => prisma.exam_catalog.findMany({
where: { name: exam.replace(/, /g, " ") }
}))
);
const flatResults = results.flat();
Bug #2: Regex with Accented Characters
JavaScript’s \b regex doesn’t work well with non-ASCII characters like “í”:
// ❌ INCORRECT
const isConfirmation = /\bsí\b/.test("sí, confirmo");
// false because \b doesn't work with "í" in many JS environments
Solution: use an alternative boundary:
// ✅ CORRECT
const isConfirmation = /\bsí(?:[^a-z]|$)/.test("sí, confirmo");
Lessons from the Field
- Document the Local Environment: Don’t assume you’ll remember how to configure the DBs in two months.
- Validate Schemas Before Commit: A pre-commit hook verifying critical tables’ existence saves you hours of debugging.
- Use Separate Environments per Service: Each microservice or agent should have its own clear environment definition.
Conclusion: The Cost of Speed
Learning these lessons cost me two frustrating days of work, but it saved me from production disasters. The temptation to use shortcuts like db push is strong when under pressure.
But in software development, especially in critical systems like healthcare, quality and reliability are worth more than speed. As we say in the south: take it slow to make it steady.
📱 WhatsApp: +56962170366
🐦 X.com: @mariohealthbits
🌐 mariohealthbits.dev
Related reading
In this series
MCP / Tool Use: The Future of Real Tool Integration
How Model Context Protocols are revolutionizing the way AI agents interact with external tools to execute complex tasks.
In this series
Multi-Agent Orchestration vs Single Agent: Lessons from the Trenches
My journey building Cotocha: why multi-agent orchestration beats single agents in real-world projects.
In this series
When your sub-agent lies: 3 failing tests that gemini-flash swore were passing
gemini-flash reported 'all tests passing': 3 tests were failing, 353 lines of stray package-lock.json included. The 4-command protocol I built to audit sub-agents in Examya.