mono/packages/ui/docs/multi-tenant.md
2026-04-02 22:59:54 +02:00

108 lines
6.5 KiB
Markdown

# 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.use` chain that inspects the `Host` or `X-Forwarded-Host` header.
* 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 `:tenantSlug` from `c.req.param('tenantSlug')` and injects it into `c.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 `:tenantSlug` parameter 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**.
1. **Never Trust the URL for Auth:**
If a user navigates to `tenant-b.domain.com` but their session limits them to `tenant-a`, the server must reject the data access.
2. **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 if `c.get('user')` is a member of `c.get('tenantId')` via a quick Supabase check or decoded JWT claims.
3. **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:
```typescript
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:
```sql
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`), so `Hono` will natively receive `tenant.yourdomain.com` in the `Host` header, alongside `X-Forwarded-Host`. No advanced `.htaccess` editing 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.com` which 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:
1. Choose either **Subdomains** (preferable) or **Path Parameters** for Hono routing.
2. Implement a unified `tenantMiddleware` in `server/src/index.ts` to extract and attach the context to the Hono `c` (Context).
3. Standardize on **Supabase RLS with custom config variables** to enforce hard data isolation, preventing accidental cross-tenant queries across all API routes.