6.5 KiB
Multi-Tenant via URL Architecture Options
Implementing multi-tenancy over the URL is a common pattern for SaaS applications. Here is an exploration of the available options, their applicability, and safety implications, focusing specifically on the Hono server architecture and Supabase database isolation.
1. Subdomain-Based Routing (Recommended)
Format: https://tenant_slug.yourdomain.com
How it works: The tenant is identified by extracting the subdomain from the request's hostname.
Applicability: Highly applicable and the industry standard for B2B SaaS. It provides the cleanest URL structure and visually assures users they are in their dedicated workspace.
Server Implementation (Hono):
- Add a global middleware early in the
app.usechain that inspects theHostorX-Forwarded-Hostheader. - Extract the subdomain (e.g.,
tenant_slug) and inject it into the request context (c.set('tenant', tenant_slug)). - Infrastructure: Requires configuring a wildcard DNS record (
*.yourdomain.com) and wildcard SSL certificates.
Safety:
- High. Cross-tenant data leakage via the UI is hard because the context is structurally tied to the origin.
- Cookies can be scoped to the specific subdomain, preventing Session/Auth token leakage to other tenants.
2. Path-Based Routing (URL Prefix)
Format: https://yourdomain.com/t/tenant_slug/ or https://yourdomain.com/tenant_slug/
How it works: The tenant ID is the first prefix segment of the URL path.
Applicability: Very common when you cannot manage wildcard subdomains or want a simpler infrastructure setup. Useful for B2C SaaS or mixed platforms.
Server Implementation (Hono):
- Hono allows route grouping and middleware scoping based on paths.
- Mount the API under a tenant prefix, e.g.,
app.use('/api/t/:tenantSlug/*', tenantMiddleware). - The middleware extracts
:tenantSlugfromc.req.param('tenantSlug')and injects it intoc.set('tenant', tenantSlug). - This requires refactoring existing Hono route registrations (like
app.route) to be nested under the path parameter.
Safety:
- Medium-High. The backend middleware must strictly validate the
:tenantSlugparameter against the authenticated user's allowed tenants. - Cookies are shared across the root domain. A compromised session on one tenant path can theoretically affect another if the user relies on path-scoping, which browsers handle inconsistently.
3. Query Parameter-Based Routing (Not Recommended)
Format: https://yourdomain.com/dashboard?tenant=tenant_slug
How it works:
Every API call passes the tenant as a query parameter.
Safety:
- Low. Prone to state leakage.
Server & Database Security Architecture (Supabase)
Merely parsing the tenant from the URL does not make the application secure. The server must use the URL purely as context, and rely on the database for authorization.
-
Never Trust the URL for Auth: If a user navigates to
tenant-b.domain.combut their session limits them totenant-a, the server must reject the data access. -
Middleware Pipeline: Your Hono server should have a distinct sequence in
server/src/index.ts:tenantMiddleware: Extracts tenant from URL/Host ->c.set('tenantId', id).authMiddleware: Verifies the authentication JWT. Determines the user's ID.authorizationMiddleware(Optional but recommended): Verifies ifc.get('user')is a member ofc.get('tenantId')via a quick Supabase check or decoded JWT claims.
-
Database Isolation using Supabase (RLS): The most robust way to ensure safety is pushing the multi-tenant logic down to Postgres via Row Level Security (RLS).
Instead of modifying every query (
.eq('tenant_id', tenantId)), use Postgres local variables:- When Hono handles a requested endpoint, generate a Supabase client using Service Role or standard Auth.
- Before running queries, set the Postgres config context:
await supabase.rpc('set_tenant_context', { _tenant_id: c.get('tenantId') }); // Or raw query: set_config('app.current_tenant', '...', true); - Your RLS policies on tables simply do:
CREATE POLICY "Tenant isolation" ON public.documents USING (tenant_id = current_setting('app.current_tenant')::uuid);
This ensures that even if you forget to add a
.eq('tenant_id')to an API call, data leakage is impossible at the database engine level.
Deployment on Plesk (Apache Proxy to Node)
Since the application runs via an Apache proxy in a Plesk environment, the deployment implications vary significantly depending on the routing strategy:
For Subdomain-Based Routing
- Plesk Configuration: You must create a "Wildcard Subdomain" (
*.yourdomain.com) in Plesk attached to the same proxy configuration as the main application. - SSL Certificates: The Let's Encrypt extension in Plesk supports issuing Wildcard certificates, but this strictly requires DNS validation via API (e.g., Cloudflare) rather than standard HTTP validation.
- Apache Proxy Pass: Plesk's Apache to Node.js proxy preserves the original request host internally (
ProxyPreserveHost On), soHonowill natively receivetenant.yourdomain.comin theHostheader, alongsideX-Forwarded-Host. No advanced.htaccessediting is required; you just assign the wildcard domain to the Node app correctly.
For Path-Based Routing
- Plesk Configuration: Requires zero structural changes to your Plesk or Apache setup. Everything remains under
yourdomain.comwhich is already safely proxied to your application. - SSL Certificates: Your existing single-domain SSL certificate handles everything automatically. No DNS API integration is needed.
- Apache Proxy Pass: Passes through normally; the backend Node application sees the full URI path and routes accordingly.
Conclusion
Since the frontend is not driving the structural isolation via an OrganizationProvider, the true source of truth must live in the Hono Server and Supabase Database.
For a robust architecture:
- Choose either Subdomains (preferable) or Path Parameters for Hono routing.
- Implement a unified
tenantMiddlewareinserver/src/index.tsto extract and attach the context to the Honoc(Context). - Standardize on Supabase RLS with custom config variables to enforce hard data isolation, preventing accidental cross-tenant queries across all API routes.