fix(webapp): route OrganizationDataStoresRegistry writes through the writer prisma#3722
Conversation
…writer prisma
The singleton was constructed with $replica, so addDataStore /
updateDataStore / deleteDataStore (and their backing SecretStore upserts)
ran against the read-only replica and Postgres rejected the INSERT with
code 25006:
cannot execute INSERT in a read-only transaction
Symptom: admin /admin/data-stores form returns a 400 with the
"prisma.secretStore.upsert" Prisma error wrapping the 25006 message.
Read path (loadFromDatabase + its SecretStore.getSecret) is unaffected
because findMany + the secret read are read-only.
Fix: change the registry constructor to take a writer alongside the
replica. Read path keeps using the replica (it's cache-fill, not on a
user-latency-sensitive path); write methods use the writer. Singleton
in organizationDataStoresRegistryInstance.server.ts now passes both
(prisma, $replica). Test sites pass (prisma, prisma) — the testcontainer
exposes a single client so the two roles collapse.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
🧰 Additional context used📓 Path-based instructions (10)**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
{packages/core,apps/webapp}/**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.{test,spec}.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
**/*.ts📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)
Files:
apps/webapp/**/*.{ts,tsx}📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Files:
apps/webapp/**/*.test.{ts,tsx}📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)
Files:
**/*.{js,jsx,ts,tsx,json,md,yml,yaml}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.test.{ts,tsx,js,jsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
apps/webapp/**/*.server.ts📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)
Files:
🧠 Learnings (9)📚 Learning: 2026-03-22T13:26:12.060ZApplied to files:
📚 Learning: 2026-03-22T19:24:14.403ZApplied to files:
📚 Learning: 2026-05-18T08:21:27.694ZApplied to files:
📚 Learning: 2026-05-18T08:21:27.694ZApplied to files:
📚 Learning: 2026-05-07T12:25:18.271ZApplied to files:
📚 Learning: 2026-05-12T21:04:05.815ZApplied to files:
📚 Learning: 2026-05-18T14:40:02.173ZApplied to files:
📚 Learning: 2026-03-26T09:02:07.973ZApplied to files:
📚 Learning: 2026-05-05T09:38:02.512ZApplied to files:
🔇 Additional comments (4)
WalkthroughThis PR refactors Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bug
The
OrganizationDataStoresRegistrysingleton inapps/webapp/app/services/dataStores/organizationDataStoresRegistryInstance.server.tswas constructed with$replica. That client was then used by both the polling read path and byaddDataStore/updateDataStore/deleteDataStore(and their backingSecretStore.setSecretupserts). The write methods route through the read replica, which Postgres rejects with error code 25006:User-visible symptom: the admin
/admin/data-stores"Add data store" form returns a 400 with this error wrapped, so noOrganizationDataStorerow can ever be created via the UI.The read path (
loadFromDatabasepolling +SecretStore.getSecret) is unaffected becausefindMany+ secret read are read-only.Fix
Change the registry constructor to take both a writer and a replica:
loadFromDatabase()keeps using_replica(and itsSecretStore.getSecretcalls) — these are background cache-fillers, not on user-latency-sensitive paths.addDataStore/updateDataStore/deleteDataStore(and theirSecretStore.setSecret/deleteSecretcalls) now use_writer.organizationDataStoresRegistryInstance.server.tspasses(prisma, $replica)from~/db.server. Test sites that constructed with(prisma)now pass(prisma, prisma)— the testcontainer exposes a single client, so the writer/replica split collapses to one connection.Files
apps/webapp/app/services/dataStores/organizationDataStoresRegistry.server.ts— constructor + read/write splitapps/webapp/app/services/dataStores/organizationDataStoresRegistryInstance.server.ts— passprismaalongside$replicaapps/webapp/test/organizationDataStoresRegistry.test.ts— 14 call sites bumpedapps/webapp/test/clickhouseFactory.test.ts— 5 call sites bumpedTest plan
organizationDataStoresRegistry.test.ts+clickhouseFactory.test.tsstill pass (constructor sites updated; behavior unchanged for tests)./admin/data-stores"Add data store" form for the HIPAA org — should now succeed and the row should appear.ORGANIZATION_DATA_STORES_RELOAD_INTERVAL_MS(60s default) and the factory starts routing to the org-scoped instance.