Skip to content

feat: prisma DB multi-tenancy (cal.eu) #21364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 93 commits into
base: main
Choose a base branch
from
Draft

Conversation

zomars
Copy link
Member

@zomars zomars commented May 16, 2025

What does this PR do?

This pull request introduces multi-tenancy support for Prisma by implementing tenant-aware database connections and refactoring existing routes and utilities to utilize the new multi-tenant architecture. The main changes include the creation of a TenantAwarePrismaService, updates to API routes to use tenant-aware Prisma wrappers, and modifications to the Prisma initialization logic.

TODOs

  • Provide a way to keep same prisma imports, but tenant-aware
  • Develop the prisma store to move away from globals
  • Create a fix for cron helpers
  • Wrap tRPC root
  • Wrap app dir pages with runWithTenants, possibly at layout.tsx level so we do it only once.
  • Wrap missing API handlers for both App Dir and Pages
  • Determine a way to opt-in for multi-tenancy (env var)
  • Determine a way to check for .eu domains via env var.
  • Determine how are we going to lookup which orgs should be rewritten to which domain at Cloudflare level

Multi-tenancy implementation:

  • apps/api/v2/src/modules/prisma/tenant-aware-prisma.service.ts: Added a new TenantAwarePrismaService to dynamically configure Prisma connections based on the tenant derived from the request's host header. This service ensures tenant-specific database URLs are used.

  • packages/prisma/index.ts: Refactored Prisma initialization to use a proxy that dynamically selects the appropriate tenant-aware Prisma instance or throws an error if accessed outside of a tenant-aware context. Introduced getPrisma and getTenantAwarePrisma utilities for tenant-specific database connections. [1] [2]

Updates to API routes:

  • API route refactoring: Updated multiple API routes (e.g., calendar-cache, credentials, selected-calendars, tasks) to wrap handlers with withMultiTenantPrisma, ensuring tenant-aware database operations. [1] [2] [3] [4] [5] [6]

  • New tenant-aware routes: Added new tenant-aware API routes for testing and demonstration purposes, such as prisma-test and tenant-aware-example. These routes showcase fetching tenant-specific data using the new multi-tenant setup. [1] [2]

Utility and library updates:

  • packages/features/auth/lib/getServerSession.ts: Updated the getServerSession function to run within a tenant-aware context using the runWithTenants utility, ensuring tenant-specific session handling. [1] [2]

  • packages/features/calendar-cache/api/cron.ts: Refactored the calendar-cache cron handler to use NextRequest and NextResponse for better compatibility with the tenant-aware architecture. [1] [2]

Cleanup and deprecations:

  • Removed old Prisma extensions: Deprecated custom Prisma extensions and middleware in favor of the new tenant-aware architecture. Simplified the Prisma client initialization logic. [1] [2]

  • Removed unused exports: Removed legacy exports like default from certain files (e.g., calendar-cache/cron.ts).

This refactor significantly improves the application's scalability by enabling multi-tenancy while maintaining backward compatibility for existing routes.

  • Fixes #XXXX (GitHub issue number)
  • Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).

Video Demo (if applicable):

  • Show screen recordings of the issue or feature.
  • Demonstrate how to reproduce the issue, the behavior before and after the change.

Image Demo (if applicable):

  • Add side-by-side screenshots of the original and updated change.
  • Highlight any significant change(s).

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Are there environment variables that should be set?
  • What are the minimal test data to have?
  • What is expected (happy path) to have (input and output)?
  • Any other important info that could help to test that PR

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings

Summary by mrge

Added an initial Prisma client store to support multi-tenant database connections based on request host.

  • New Features
    • Created helpers to select the correct database for each tenant.
    • Added middleware and wrappers for API routes and SSR to inject the right Prisma client.
    • Updated environment variable handling for multiple database URLs.

@keithwillcode keithwillcode added core area: core, team members only foundation labels May 16, 2025
Copy link

vercel bot commented May 16, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
cal ⬜️ Ignored (Inspect) Visit Preview May 29, 2025 11:05pm

Copy link
Member Author

Choose a reason for hiding this comment

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

I've tried to cover the main entrypoints with re-usable wrappers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Same for pages

Copy link
Member Author

Choose a reason for hiding this comment

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

Not a real middleware, but an example on how this would work in middleware. We could attach the tenant to headers

Copy link
Member Author

Choose a reason for hiding this comment

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

This would be the main client storage. This plays nice with serverless and we move away to global hacks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Wrapper for API routes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Wrapper for SSR.

Copy link
Member Author

Choose a reason for hiding this comment

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

We centralize tenants pairing here

@zomars zomars changed the title feat: Initial implentation of prisma clients store feat: prisma DB multi-tenancy (cal.eu) May 20, 2025
Copy link
Contributor

@joeauyeung joeauyeung left a comment

Choose a reason for hiding this comment

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

Awesome reading through this PR @zomars.

I was wondering for the routes I know we need to have the withMultiTenantPrisma wrapper but I'm curious why cannot we use Nextjs middleware here for those routes? Would invoking that function in the middleware set the AsyncLocalStorage for the endpoint handler?

export function getPrisma(tenant: Tenant, options?: Prisma.PrismaClientOptions) {
if (process.env.NODE_ENV === "test") {
const url = tenantToDatabaseUrl[tenant];
if (!url) throw new Error(`Missing DB URL for tenant: ${tenant}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that we have two database URLs, should we create an alert if this error hits or the one on line 78? Just in case something happens with our config?

@joeauyeung
Copy link
Contributor

Wrap app dir pages with runWithTenants, possibly at layout.tsx level so we do it only once.

I'm wondering if we can also utilize middleware here so we don't need to add more boiler plate code

@zomars
Copy link
Member Author

zomars commented May 23, 2025

I'm wondering if we can also utilize middleware here so we don't need to add more boiler plate code

AsyncLocalStorage is part of the Node.js async_hooks module, which is not available in the Edge Runtime. So no middleware. @joeauyeung

Copy link
Contributor

github-actions bot commented May 23, 2025

E2E results are ready!

@github-actions github-actions bot added the ❗️ .env changes contains changes to env variables label May 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core area: core, team members only ❗️ .env changes contains changes to env variables foundation ready-for-e2e
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants