Multi-Tenancy
Multi-Tenancy
Global Watch uses subdomain-based routing for tenant isolation, providing a clean separation between personal accounts, team accounts, and marketing pages.
URL Structure
The platform uses different subdomains to route users to the appropriate context:
| Type | Subdomain | URL | Route Group |
|---|---|---|---|
| Marketing | (none) | global.watch | (marketing)/ |
| Personal | app | app.global.watch | home/ |
| Team | {slug} | acme.global.watch | account/ |
Examples
# Marketing site (public)
https://global.watch
https://global.watch/pricing
https://global.watch/blog
# Personal account (authenticated)
https://app.global.watch/home
https://app.global.watch/home/settings
https://app.global.watch/home/billing
# Team account (authenticated + team member)
https://acme.global.watch/account
https://acme.global.watch/account/projects
https://acme.global.watch/account/membersLocal Development
For local development, use localhost.direct which resolves to 127.0.0.1:
# Marketing
http://localhost.direct:3000
# Personal account
http://app.localhost.direct:3000
# Team account
http://acme.localhost.direct:3000localhost.direct is a special domain that resolves to 127.0.0.1, enabling subdomain testing without modifying your hosts file.
Tenant Utilities
Global Watch provides utility functions for working with tenants:
Extracting Subdomain
import { getSubdomain } from '~/lib/tenant/tenant-utils';
// Extract subdomain from host
getSubdomain('acme.localhost.direct:3000');
// → 'acme'
getSubdomain('app.localhost.direct:3000');
// → 'app'
getSubdomain('localhost.direct:3000');
// → nullDetecting Tenant Type
import { getCurrentTenant } from '~/lib/tenant/tenant-utils';
// Personal account
getCurrentTenant('app.localhost.direct:3000');
// → { type: 'personal' }
// Team account
getCurrentTenant('acme.localhost.direct:3000');
// → { type: 'team', slug: 'acme' }
// Marketing (no subdomain)
getCurrentTenant('localhost.direct:3000');
// → { type: 'marketing' }Building URLs
import { buildPersonalUrl, buildTeamUrl } from '~/lib/tenant/tenant-utils';
// Build personal account URL
buildPersonalUrl('/projects');
// → 'http://app.localhost.direct:3000/projects'
// Build team account URL
buildTeamUrl('acme', '/projects');
// → 'http://acme.localhost.direct:3000/projects'Validating Team Slugs
import { isValidTeamSlug } from '~/lib/tenant/tenant-utils';
isValidTeamSlug('acme'); // → true
isValidTeamSlug('my-team'); // → true
isValidTeamSlug('app'); // → false (reserved)
isValidTeamSlug('AB'); // → false (too short)
isValidTeamSlug('admin'); // → false (reserved)Reserved Slugs
Certain slugs are reserved and cannot be used for team accounts:
const reservedSlugs = [
'app', // Personal account subdomain
'www', // Common web prefix
'api', // API subdomain
'admin', // Admin panel
'auth', // Authentication
'cdn', // Content delivery
'assets', // Static assets
'static', // Static files
'docs', // Documentation
'blog', // Blog
'help', // Help center
'support', // Support
'status', // Status page
'mail', // Email
'workspace', // Generic workspace
'map', // Map application
'maps', // Maps
'report', // Reports
'reports', // Reports
];Workspace Contexts
Global Watch provides React contexts for accessing workspace data in components.
Personal Account Context
For pages in the personal account context (/home/*):
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
function PersonalPage() {
const { user, account } = useUserWorkspace();
return (
<div>
<h1>Welcome, {user.displayName}</h1>
<p>Account: {account.name}</p>
</div>
);
}Available properties:
| Property | Type | Description |
|---|---|---|
user | User | Current authenticated user |
account | Account | Personal account data |
Team Account Context
For pages in the team account context (/account/*):
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
function TeamPage() {
const { account, user, accounts } = useTeamAccountWorkspace();
return (
<div>
<h1>{account.name}</h1>
<p>Your role: {user.role}</p>
<p>Team slug: {account.slug}</p>
</div>
);
}Available properties:
| Property | Type | Description |
|---|---|---|
account | TeamAccount | Current team account |
user | TeamMember | Current user's membership |
accounts | TeamAccount[] | All teams user belongs to |
Environment Variables
Configure multi-tenancy with these environment variables:
# Base domain for subdomain routing
NEXT_PUBLIC_BASE_DOMAIN=localhost.direct:3000
# Personal account URL
NEXT_PUBLIC_APP_URL=http://app.localhost.direct:3000
# Marketing site URL
NEXT_PUBLIC_SITE_URL=http://localhost.direct:3000
# Enable subdomain routing
NEXT_PUBLIC_ENABLE_SUBDOMAIN_ROUTING=trueProduction Configuration
NEXT_PUBLIC_BASE_DOMAIN=global.watch
NEXT_PUBLIC_APP_URL=https://app.global.watch
NEXT_PUBLIC_SITE_URL=https://global.watch
NEXT_PUBLIC_ENABLE_SUBDOMAIN_ROUTING=trueRLS Isolation
Data isolation is enforced at the database level using Row Level Security (RLS):
-- Users can only view projects they have access to
CREATE POLICY "users_can_view_own_projects" ON projects
FOR SELECT USING (
account_id IN (
SELECT account_id FROM memberships
WHERE user_id = auth.uid()
)
);Key Principles
- Always use
account_id- All tenant-scoped data must have anaccount_idforeign key - RLS on all tables - Enable RLS on every table containing tenant data
- Membership checks - Use
has_role_on_account()for team access - Personal account checks - Use
account_id = auth.uid()for personal data
Example RLS Policies
-- Personal account data (only owner can access)
CREATE POLICY "personal_data_access" ON personal_settings
FOR ALL USING (
account_id = (SELECT auth.uid())
);
-- Team account data (any team member can access)
CREATE POLICY "team_data_access" ON team_projects
FOR SELECT USING (
public.has_role_on_account(account_id)
);
-- Team data with role requirement
CREATE POLICY "team_admin_access" ON team_settings
FOR ALL USING (
public.has_role_on_account(account_id, 'owner')
);Route Organization
The Next.js App Router structure reflects the multi-tenant architecture:
apps/web/app/
├── (marketing)/ # Public marketing pages
│ ├── page.tsx # Landing page
│ ├── pricing/
│ ├── blog/
│ └── contact/
│
├── home/ # Personal account routes
│ ├── (user)/ # User workspace
│ │ ├── page.tsx # Dashboard
│ │ ├── settings/
│ │ ├── billing/
│ │ └── teams/
│ └── layout.tsx
│
├── account/ # Team account routes
│ ├── page.tsx # Team dashboard
│ ├── projects/
│ ├── members/
│ ├── settings/
│ └── layout.tsx
│
└── admin/ # Super admin routes
└── ...Middleware Configuration
The middleware handles subdomain routing:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const host = request.headers.get('host') || '';
const subdomain = getSubdomain(host);
// Route based on subdomain
if (subdomain === 'app') {
// Personal account - route to /home
return NextResponse.rewrite(new URL('/home' + pathname, request.url));
}
if (subdomain && subdomain !== 'www') {
// Team account - route to /account
return NextResponse.rewrite(new URL('/account' + pathname, request.url));
}
// Marketing site - no rewrite needed
return NextResponse.next();
}Best Practices
1. Always Check Tenant Context
// ✅ CORRECT - Check context before operations
const { account } = useTeamAccountWorkspace();
await createProject({ accountId: account.id, ...data });
// ❌ WRONG - Hardcoded or missing account
await createProject({ accountId: 'some-id', ...data });2. Use Workspace Hooks
// ✅ CORRECT - Use provided hooks
const { account } = useTeamAccountWorkspace();
// ❌ WRONG - Parse URL manually
const slug = window.location.hostname.split('.')[0];3. Validate Slugs on Creation
// ✅ CORRECT - Validate before creating team
if (!isValidTeamSlug(slug)) {
return { error: 'Invalid team slug' };
}
await createTeam({ slug, name });4. Handle Cross-Tenant Navigation
// ✅ CORRECT - Use URL builders for navigation
const teamUrl = buildTeamUrl(team.slug, '/projects');
router.push(teamUrl);
// ❌ WRONG - Construct URLs manually
router.push(`https://${team.slug}.global.watch/projects`);Next Steps
- Database Design - Schema organization and RLS policies
- Hexagonal Architecture - Ports & Adapters pattern
- Access Control - Permissions and roles