Skip to content

fix: replace ALS-based db scoping with closure-based scoped client#17

Merged
mindsers merged 2 commits intomainfrom
fix/tenant-data-scoping-again
Apr 10, 2026
Merged

fix: replace ALS-based db scoping with closure-based scoped client#17
mindsers merged 2 commits intomainfrom
fix/tenant-data-scoping-again

Conversation

@mindsers
Copy link
Copy Markdown
Contributor

Summary

  • Root cause fix: the Prisma 7 pg adapter breaks AsyncLocalStorage after async queries, making ALS-based tenant scoping unreliable. A previous module-level fallback caused cross-tenant data leaks in production due to race conditions between concurrent requests.
  • New approach: createScopedDb(congregationId) creates a Prisma $extends client that captures congregationId in a JavaScript closure — completely immune to ALS propagation issues.
  • authenticateAndAuthorize() now returns a scoped db client alongside currentUser, congregation, session, and can().
  • All service functions (30+ files) receive db: ScopedDb as their first parameter instead of importing the global db.
  • LimitService constructor takes db as first parameter.
  • Background worker uses createScopedDb() directly for tenant isolation.
  • Docs updated to reflect the new architecture (OSS-focused, no SaaS-specific content).

Supersedes #13 and #12 which used ALS-based workarounds.

Test plan

  • Log into a congregation → see only that congregation's data
  • Create/edit/delete records → changes apply to the correct congregation
  • Navigate all pages — no "Congregation context is required" errors
  • pnpm test:unit — 322 tests pass
  • pnpm test:typecheck — clean
  • pnpm test:lint — clean

The Prisma 7 pg adapter breaks AsyncLocalStorage propagation after
async queries, making the ALS-based tenant scoping unreliable. A
module-level fallback was tried but caused cross-tenant data leaks
in production due to race conditions between concurrent requests.

Replace the entire approach with a closure-based scoped db client:

- Add createScopedDb(congregationId) factory that captures the ID in
  a closure instead of reading from AsyncLocalStorage
- authenticateAndAuthorize() now returns a scoped db client
- All service functions receive db: ScopedDb as first parameter
  instead of importing the global db
- LimitService constructor takes db as first parameter
- Background worker uses createScopedDb() directly
- Update all 29 test files to pass db to service function calls
- Update architecture docs to reflect the new pattern (OSS-focused)
@mindsers mindsers force-pushed the fix/tenant-data-scoping-again branch from df48454 to 3d3f931 Compare April 10, 2026 12:36
@mindsers mindsers added the bug Something isn't working label Apr 10, 2026
@mindsers mindsers merged commit 4df72fa into main Apr 10, 2026
4 checks passed
@mindsers mindsers deleted the fix/tenant-data-scoping-again branch April 10, 2026 12:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant