Multi-tenant applications let many users or groups share the same app while keeping their data separate and secure.
Multi-tenant applications in Remix
import { json } from '@remix-run/node'; export async function loader({ request, params }) { const tenantId = getTenantIdFromRequest(request); const data = await fetchDataForTenant(tenantId); return json(data); } function getTenantIdFromRequest(request) { // Example: extract tenant from subdomain or headers const url = new URL(request.url); return url.hostname.split('.')[0]; } async function fetchDataForTenant(tenantId) { // Placeholder function to fetch tenant-specific data return {}; }
Use the loader function to fetch data specific to each tenant.
Extract tenant info from the request, like subdomain or headers, to separate data.
import { json } from '@remix-run/node'; export async function loader({ request }) { const tenantId = request.headers.get('x-tenant-id'); const data = await getTenantData(tenantId); return json(data); } async function getTenantData(tenantId) { // Fetch tenant data based on tenantId return {}; }
import { json } from '@remix-run/node'; export async function loader({ request }) { const url = new URL(request.url); const tenantId = url.pathname.split('/')[1]; const data = await getTenantData(tenantId); return json(data); } async function getTenantData(tenantId) { // Fetch tenant data based on tenantId return {}; }
import { json } from '@remix-run/node'; export async function loader({ request }) { const tenantId = getTenantFromSession(request); const data = await getTenantData(tenantId); return json(data); } function getTenantFromSession(request) { // Example: read tenant from user session cookie return 'tenant123'; } async function getTenantData(tenantId) { // Fetch tenant data based on tenantId return {}; }
This Remix loader extracts the tenant ID from the subdomain of the request URL. It then fetches data specific to that tenant from a simple in-memory object. The response includes the tenant ID and a welcome message. This shows how to separate data per tenant in a multi-tenant app.
import { json } from '@remix-run/node'; export async function loader({ request }) { const tenantId = getTenantIdFromRequest(request); const data = await fetchDataForTenant(tenantId); return json({ tenantId, data }); } function getTenantIdFromRequest(request) { const url = new URL(request.url); return url.hostname.split('.')[0]; } async function fetchDataForTenant(tenantId) { const fakeDatabase = { tenantA: { message: 'Welcome Tenant A!' }, tenantB: { message: 'Hello Tenant B!' }, }; return fakeDatabase[tenantId] || { message: 'Unknown tenant' }; }
Always validate tenant IDs to avoid security issues.
Use environment variables or config to manage tenant-specific settings.
Consider performance impacts when scaling many tenants.
Multi-tenant apps share one codebase but keep data separate per user group.
Extract tenant info from requests to load correct data.
Remix loaders are great places to handle tenant-specific data fetching.
Practice
Solution
Step 1: Understand multi-tenant concept
Multi-tenant apps use one codebase but keep data separate for each user group or tenant.Step 2: Identify Remix multi-tenant goal
Remix apps handle requests to serve tenant-specific data without duplicating code.Final Answer:
To serve multiple user groups with separate data using one codebase -> Option DQuick Check:
Multi-tenant = shared code, separate data [OK]
- Thinking each tenant needs a separate app
- Assuming tenants share the same data
- Confusing multi-tenant with multi-instance
Solution
Step 1: Recall Remix loader signature
Remix loaders receive an object with a request property: loader({ request }).Step 2: Identify correct usage for tenant extraction
Tenant info is extracted from the request object, so the loader must accept { request }.Final Answer:
export async function loader({ request }) { /* use request to get tenant */ } -> Option AQuick Check:
Loader({ request }) is correct signature [OK]
- Using request as a direct parameter instead of inside an object
- Expecting tenant info in a 'context' parameter
- Defining loader with no parameters
export async function loader({ request }) {
const url = new URL(request.url);
const tenant = url.pathname.split('/')[1];
return { tenant };
}Solution
Step 1: Parse the URL pathname
The pathname is '/tenantA/dashboard'. Splitting by '/' gives ['', 'tenantA', 'dashboard'].Step 2: Extract tenant from split array
The tenant is at index 1, which is 'tenantA'.Final Answer:
{ tenant: 'tenantA' } -> Option AQuick Check:
Split pathname[1] = tenant name [OK]
- Picking index 2 instead of 1 for tenant
- Not accounting for leading slash causing empty string at index 0
- Assuming tenant is query parameter
export async function loader({ request }) {
const url = new URL(request.url);
const tenant = url.pathname.split('/')[2];
if (!tenant) throw new Error('Tenant missing');
return { tenant };
}Solution
Step 1: Analyze pathname splitting
Pathname split by '/' results in ['', tenant, ...]. Tenant is at index 1, not 2.Step 2: Understand error condition
If tenant is at index 2, it will often be undefined, causing error even when tenant exists.Final Answer:
Tenant index should be 1, not 2 -> Option BQuick Check:
Tenant = pathname.split('/')[1] [OK]
- Using wrong index for tenant extraction
- Confusing async usage with URL constructor
- Throwing error instead of handling gracefully
Solution
Step 1: Understand tenant data isolation
Each tenant must only access their own data to keep data private and secure.Step 2: Apply tenant filtering in loader
Extract tenant ID from request and query database filtering by that tenant only.Final Answer:
Use the tenant identifier from the request to query only that tenant's data -> Option CQuick Check:
Filter data by tenant in loader for isolation [OK]
- Fetching all data and filtering later (inefficient and unsafe)
- Using global variables for tenant data
- Ignoring tenant filtering in queries
Ready to prove it?
Pass the test and this pattern moves unknown
