How to set up subdomain-based multi-tenancy with Next.js, Supabase (DB/Auth), and next-intl? #84461
Replies: 2 comments
-
|
Hello! Your setup is quite comprehensive, but I can suggest some improvements for production readiness. Here's an enhanced approach: 1. Enhanced Middleware Structure // middleware.ts
import createMiddleware from "next-intl/middleware";
import { type NextRequest, NextResponse } from "next/server";
import { routing } from "@/i18n/routing";
import { createServerClient } from "@supabase/ssr";
import { getPathname } from "@/i18n/navigation";
const handleI18nRouting = createMiddleware(routing);
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const subdomain = extractSubdomain(request);
if (!subdomain) {
return handleI18nRouting(request);
}
const response = handleI18nRouting(request);
return await handleTenantRouting(request, response, subdomain);
}
async function handleTenantRouting(
request: NextRequest,
response: NextResponse,
subdomain: string
) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options);
});
},
},
}
);
const { data: tenant, error } = await supabase
.from("tenants")
.select("id, status")
.eq("subdomain", subdomain)
.eq("status", "active")
.single();
if (!tenant || error) {
return new NextResponse("Tenant not found", { status: 404 });
}
const { data: { user } } = await supabase.auth.getUser();
const isAuthPath = pathname.includes('/auth/');
const isPublicPath = ['/login', '/signup', '/pricing'].some(path =>
pathname.includes(path)
);
if (!user && !isPublicPath && !isAuthPath) {
const loginUrl = new URL(`/login`, request.url);
return NextResponse.redirect(loginUrl);
}
response.headers.set('x-tenant-id', tenant.id);
response.headers.set('x-tenant-subdomain', subdomain);
return response;
}2. Tenant-Aware Database Client // lib/supabase/tenant-client.ts
import { createServerClient } from "@supabase/ssr";
import { headers } from "next/headers";
export async function createTenantClient() {
const headersList = await headers();
const tenantId = headersList.get('x-tenant-id');
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
global: {
headers: {
'X-Tenant-ID': tenantId || '',
},
},
}
);
return supabase;
}3. Row Level Security (RLS) for Multi-tenancy ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON projects
FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);4. Production Security Considerations
5. Vercel Configuration // vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"functions": {
"app/[locale]/(app)/[tenant]/**/*": {
"maxDuration": 30
}
}
}This approach ensures proper tenant isolation, security, and scalability for production use. The key is combining middleware-based routing with Supabase RLS for robust multi-tenancy. |
Beta Was this translation helpful? Give feedback.
-
|
Hey @KaloudasDev ! First off, thanks a lot for your detailed response, it was super insightful! I have a quick follow-up question regarding this part of my middleware: I tried your suggestion, but without this section, the internal rewrite no longer happens. Do you have any thoughts on how to achieve this cleanly? Thanks again for your help |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
I’m trying to build a multi-tenant platform similar to how Sentry works, where the root domain (e.g. https://sentry.io/en) serves the marketing site, and each tenant has its own localised subdomain (e.g. https://acme.sentry.io/en).
My stack is:
I’ve experimented locally and can get subdomains to resolve, but I’m not confident that the approach I took is secure or production-ready.
Has anyone successfully implemented this setup? Are there recommended patterns, examples, or best practices for combining custom subdomains, Supabase Auth/DB, and next-intl in a Next.js app?
My current folder structure looks like this:
For my middleware i have:
and
Thanks guys!
Additional information
No response
Example
No response
Beta Was this translation helpful? Give feedback.
All reactions