Tenant Domain Architecture

V4 hỗ trợ 2 cách access song song: subdomain mặc định cho tất cả tenants + custom domain tùy chọn cho gói premium. Mỗi tenant có thể có cả 2.

Option A · Basic tier · All tenants factory-abc.viecxanh.vn Subdomain auto-provision, SSL wildcard
Option C · Premium tier · Optional abc.com.vn Custom domain + Let's Encrypt SSL

Mục lục

  1. Overview — 5 domain zones + 2 access patterns
  2. Domain Zones — Chi tiết từng zone
  3. Resolution Logic — Request → Tenant mapping
  4. Schema Changes — tenants table thêm 3 columns
  5. Custom Domain Onboarding — DNS verify + SSL flow
  6. UX Flows — Per user type
  7. Tiers — Basic vs Premium access
  8. Infrastructure — DNS, SSL, monitoring

1. Overview — 5 zones, 2 access patterns

V4 chia domain thành 5 zones. Tenant operations có 2 cách access song song.

Domain zones
5
Public, Tenant, Worker, API, System
Tenant access modes
2
Subdomain + Custom domain
Tenant onboarding
5 phút
Subdomain auto (wildcard SSL)
Custom domain setup
~1 ngày
DNS verify + SSL provision
╔══════════════════════════════════════════════════════════════════════════════╗ ║ V4 DOMAIN ARCHITECTURE — 5 Zones ║ ╚══════════════════════════════════════════════════════════════════════════════╝ ┌──────────────────────────────────────────────────────────────────────────┐ │ Zone 1: PUBLIC MARKETPLACE │ │ viecxanh.vn Single domain, SEO focused │ │ viecxanh.vn/worker worker portal (multi-tenant: worker làm nhiều NM) │ │ viecxanh.vn/employer employer portal (tenant switcher for multi-tenant)│ └──────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────────────┐ │ Zone 2: TENANT OPERATIONS (Admin SPA) │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ A) Subdomain (Basic tier - ALL TENANTS) │ │ │ │ factory-abc.viecxanh.vn/admin │ │ │ │ supplier-xyz.viecxanh.vn/admin │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ C) Custom Domain (Premium tier - OPTIONAL) │ │ │ │ abc.com.vn/admin ← points to same admin-spa │ │ │ │ vieclam.xyz.vn │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ Both modes resolve to same tenant context + same API │ └──────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────────────┐ │ Zone 3: API (unified backend Laravel) │ │ api.viecxanh.vn All apps consume here │ │ Tenant identified via: │ │ - Host header (if subdomain/custom domain) │ │ - X-Tenant-Slug header (fallback) │ │ - JWT claim tenant_id (most secure) │ └──────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────────────┐ │ Zone 4: AI / CHAT SERVICE │ │ ai.viecxanh.vn NestJS + Socket.io + LLMs │ └──────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────────────────┐ │ Zone 5: SYSTEM ADMIN (ViệcXanh ops team only) │ │ system.viecxanh.vn Super admin, cross-tenant, 2FA required │ └──────────────────────────────────────────────────────────────────────────┘

2. Domain Zones — Chi tiết

5 zones độc lập, mỗi zone có purpose rõ.

🟢 Zone 1 — Public Marketplace
SEO · Cross-tenant

Single domain cho public jobs marketplace + worker/employer portal. SEO tối ưu cho search engines crawl cross-tenant job listings.

  • viecxanh.vn — Homepage, job feed, SEO pages
  • viecxanh.vn/viec-lam/:slug — Job detail pages (cross-tenant)
  • viecxanh.vn/cong-ty/:slug — Company profile pages
  • viecxanh.vn/worker — Worker portal (login cross-tenant identity)
  • viecxanh.vn/employer — Employer portal (tenant switcher if multi-tenant user)
🔵 Zone 2A — Tenant Subdomain (Basic tier)
Auto-provision · Wildcard SSL

Mỗi tenant khi tạo tự động có subdomain theo slug. SSL cert wildcard *.viecxanh.vn cover tất cả. Tenant không cần làm gì.

  • factory-abc.viecxanh.vn/admin — Admin workspace cho "Nhà máy ABC"
  • supplier-xyz.viecxanh.vn/admin — Admin workspace cho "NCC XYZ"
  • holding-mnp.viecxanh.vn/admin — Admin workspace cho tenant hybrid
Always available: Subdomain luôn hoạt động cho mọi tenant, kể cả khi đã có custom domain (serve as fallback).
🟡 Zone 2B — Custom Domain (Premium tier)
Opt-in · Per-tenant SSL

Tenant gói Premium có thể thêm custom domain của họ. SSL cert provision tự động qua Let's Encrypt ACME. DNS verify qua TXT record.

  • abc.com.vn/admin — Custom domain của "Nhà máy ABC"
  • vieclam.xyz.vn/admin — Custom domain của "NCC XYZ"
  • tuyendung.mnp.com.vn — Subdomain của tenant ở domain của họ
Same tenant context: abc.com.vnfactory-abc.viecxanh.vn cùng route tới tenant ID duy nhất. User login 1 nơi, context giống nhau ở cả 2.
🟣 Zone 3 — API Backend
Unified Laravel · Multi-auth

Unified API endpoint cho tất cả apps consume. Backend Laravel identify tenant qua 3 cách fallback:

  1. Host header — khi request từ subdomain hoặc custom domain (most reliable)
  2. X-Tenant-Slug header — explicit set bởi client app
  3. JWT claim tenant_id — most secure (server-signed, cannot spoof)
  • api.viecxanh.vn/v4/... — All app consume
🔵 Zone 4 — AI / Chat Service
NestJS · Socket.io · LLMs

AI orchestrator service (renamed từ chat-service). WebSocket + REST API.

  • ai.viecxanh.vn — WebSocket + REST
  • ai.viecxanh.vn/chat — Chat endpoints
  • ai.viecxanh.vn/agents/:agentType — AI agent calls
🔴 Zone 5 — System Admin
Super admin · 2FA · Cross-tenant

ViệcXanh ops team access. Cross-tenant operations: tenant creation, billing, platform settings. 2FA mandatory.

  • system.viecxanh.vn — Platform operations
  • system.viecxanh.vn/tenants — Tenant management
  • system.viecxanh.vn/billing — Billing dashboard
  • system.viecxanh.vn/audit — Cross-tenant audit log

3. Resolution Logic — Request → Tenant

Request đến, backend Laravel middleware resolve tenant qua Host header.

┌─────────────────────────────────────────────────────────────────────┐ │ Request arrives at nginx/load balancer │ │ Host: ??? │ └───────────────────────────┬─────────────────────────────────────────┘┌────────────▼────────────┐ │ Parse Host header │ └────────────┬────────────┘┌──────────────────────────┼──────────────────────────────┐ │ │ │ ┌─▼──────────────────┐ ┌──────▼──────────────────┐ ┌─▼──────────────────────┐ │ viecxanh.vn │ │ {slug}.viecxanh.vn │ │ abc.com.vn (or other) │ │ api.viecxanh.vn │ │ │ │ │ │ system.viecxanh.vn │ │ Strip first segment: │ │ Check in DB: │ │ ai.viecxanh.vn │ │ slug = "factory-abc" │ │ SELECT * FROM tenants │ │ │ │ │ │ WHERE custom_domain = │ │ NO tenant context │ │ SELECT * FROM tenants │ │ 'abc.com.vn' │ │ (public routes) │ │ WHERE slug = '...' │ │ AND custom_domain_ │ │ │ │ │ │ verified_at IS NOT │ │ Use JWT/header for │ │ Found? Set tenant ctx │ │ NULL │ │ tenant if needed │ │ Not found? → 404 │ │ │ └────────────────────┘ └─────────────────────────┘ │ Found? Set tenant ctx │ │ Not found? → 404 │ └────────────────────────┘┌────────────▼────────────┐ │ Tenant resolved │ │ app('current_tenant') │ │ SET app.current_tenant_id │ │ (for PostgreSQL RLS) │ └─────────────────────────┘┌────────────▼────────────┐ │ Continue request │ │ All queries scoped │ └─────────────────────────┘

Laravel middleware logic (pseudocode)

// app/Http/Middleware/IdentifyTenant.php
class IdentifyTenant {
  public function handle(Request $request, Closure $next) {
    $host = $request->getHost();                       // e.g., "factory-abc.viecxanh.vn"
    $tenant = null;

    // 1. Reserved domains — no tenant
    if (in_array($host, ['viecxanh.vn', 'api.viecxanh.vn', 'system.viecxanh.vn', 'ai.viecxanh.vn'])) {
      // Try extract tenant from JWT claim or X-Tenant-Slug header
      $tenantId = auth()->user()?->current_tenant_id
               ?? Tenant::where('slug', $request->header('X-Tenant-Slug'))->first()?->id;
      if ($tenantId) $tenant = Tenant::find($tenantId);
      return $this->setContext($tenant, $next, $request);
    }

    // 2. Subdomain pattern: {slug}.viecxanh.vn
    if (str_ends_with($host, '.viecxanh.vn')) {
      $slug = explode('.', $host)[0];
      $tenant = Tenant::where('slug', $slug)->first();
      if (!$tenant) abort(404, 'Tenant không tồn tại');
      return $this->setContext($tenant, $next, $request);
    }

    // 3. Custom domain lookup
    $tenant = Tenant::where('custom_domain', $host)
      ->whereNotNull('custom_domain_verified_at')
      ->first();
    if (!$tenant) abort(404, 'Domain chưa được cấu hình');
    return $this->setContext($tenant, $next, $request);
  }

  private function setContext($tenant, $next, $request) {
    if ($tenant) {
      app()->instance('current_tenant', $tenant);
      // Set PG session var for RLS policies
      DB::statement("SET app.current_tenant_id = ?", [$tenant->id]);
    }
    return $next($request);
  }
}

4. Schema Changes — tenants table

Thêm 3 columns để hỗ trợ custom domain.

ALTER TABLE tenants
  ADD COLUMN custom_domain VARCHAR(255) UNIQUE,                 -- e.g., 'abc.com.vn'
  ADD COLUMN custom_domain_verified_at TIMESTAMPTZ,             -- DNS TXT verified
  ADD COLUMN custom_domain_ssl_status VARCHAR(20)                -- pending|active|expired|failed
    CHECK (custom_domain_ssl_status IN ('pending','active','expired','failed')),
  ADD COLUMN custom_domain_verification_token VARCHAR(64);        -- DNS TXT value

-- Index for fast custom domain lookup
CREATE UNIQUE INDEX idx_tenants_custom_domain
  ON tenants(custom_domain)
  WHERE custom_domain_verified_at IS NOT NULL;

-- Optional: SSL cert storage
CREATE TABLE tenant_ssl_certificates (
  id BIGSERIAL PRIMARY KEY,
  tenant_id BIGINT NOT NULL REFERENCES tenants(id),
  domain VARCHAR(255) NOT NULL,
  certificate TEXT,                                    -- PEM encoded
  private_key TEXT,                                    -- encrypted
  issued_at TIMESTAMPTZ,
  expires_at TIMESTAMPTZ,
  auto_renew BOOLEAN DEFAULT true,
  status VARCHAR(20)
);

Model relationships

ColumnTypePurpose
slugVARCHAR(50) UNIQUEAlways required · basis for subdomain {slug}.viecxanh.vn
custom_domainVARCHAR(255) UNIQUENullable · tenant's custom domain abc.com.vn
custom_domain_verified_atTIMESTAMPTZNULL = not verified yet, NOT NULL = active
custom_domain_ssl_statusENUMTrack Let's Encrypt cert provision
custom_domain_verification_tokenVARCHAR(64)TXT record value for DNS verify

5. Custom Domain Onboarding Flow

Tenant từ subdomain-only → thêm custom domain trong 3 phase: add → verify → SSL → activate.

Phase 1: Add Domain (Tenant admin action)

  1. Tenant admin vào factory-abc.viecxanh.vn/admin/settings/domains
  2. Click "Thêm custom domain" → nhập abc.com.vn
  3. System generate random verification token (64 chars) → lưu vào custom_domain_verification_token
  4. System hiển thị cho tenant admin:
    DNS TXT record to add at your DNS provider:
    
      Name:  _viecxanh-verify.abc.com.vn
      Type:  TXT
      Value: vx4-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
      TTL:   300

Phase 2: DNS Verification (Async)

  1. Tenant thêm TXT record vào DNS provider của họ (Cloudflare, Route53, etc.)
  2. Tenant click "Verify now" trên admin UI (hoặc system auto-check mỗi 5 phút)
  3. System thực hiện DNS lookup:
    dig TXT _viecxanh-verify.abc.com.vn +short
    # Compare with stored token
  4. Nếu match → set custom_domain_verified_at = NOW()
  5. Send email "Domain verified" to tenant admin

Phase 3: SSL Provision (Automatic)

  1. System trigger SSL job via Let's Encrypt ACME client (certbot hoặc acme.sh)
  2. HTTP-01 challenge: system serve /.well-known/acme-challenge/... trên abc.com.vn
  3. Tenant đã point A record abc.com.vn → viecxanh server IP
  4. Let's Encrypt validate → issue cert
  5. Cert stored trong tenant_ssl_certificates table, nginx reload
  6. Set custom_domain_ssl_status = 'active'
  7. Email "SSL active — your domain is live" to tenant admin

Phase 4: Auto-renewal

  1. Scheduled job daily: check cert expiry
  2. If expires < 30 days → trigger ACME renew
  3. On renew success → reload nginx, update DB
  4. On renew fail → alert tenant admin + ops team
Pre-requisites: Tenant phải A record abc.com.vn → [VIECXANH_LB_IP] hoặc CNAME abc.com.vn → cname.viecxanh.vn trước khi SSL provision work.
Fallback: Trong khi chờ SSL provision, user truy cập abc.com.vn sẽ redirect tạm sang factory-abc.viecxanh.vn. Subdomain LUÔN hoạt động, không bao giờ off.

6. UX Flows — Per User Type

Trải nghiệm thực tế cho từng loại user.

Staff Admin (của tenant)

# Scenario 1: Basic tier (subdomain only) 1. Nhập URL: factory-abc.viecxanh.vn/admin 2. Redirect to login: factory-abc.viecxanh.vn/admin/login 3. Email + password → JWT issued with tenant_id = 42 (Nhà máy ABC) 4. Admin SPA load, tenant context locked 5. URL bar hiển thị: factory-abc.viecxanh.vn/admin/dashboard 6. Logout → back to login same subdomain # Scenario 2: Premium tier (custom domain active) 1. Nhập URL: abc.com.vn/admin ← tenant's own domain 2. Backend resolve: custom_domain='abc.com.vn' → tenant_id = 42 3. Redirect to login: abc.com.vn/admin/login 4. Same JWT flow, tenant context = 42 5. URL bar: abc.com.vn/admin/dashboard ← branded feel 6. Cookies stored under abc.com.vn (isolated from other tenants) # Scenario 3: Both modes coexist - Tenant có cả factory-abc.viecxanh.vnabc.com.vn - Staff có thể truy cập qua bất kỳ URL nào - Session independent (cookies per domain) - Backend resolve vẫn ra tenant_id = 42 - Data giống nhau

Worker (cross-tenant identity)

1. Nhập URL: viecxanh.vn hoặc mobile app 2. Login with phone + OTP → JWT với worker_account_id (NO tenant_id) 3. Dashboard hiển thị employments của mình across ALL tenants: - Employment #1: Nhà máy ABC (factory-abc) - Đang làm - Employment #2: NCC XYZ (supplier-xyz) - Đã nghỉ - Employment #3: Công ty MNP (holding-mnp) - Đang làm 4. Click vào 1 employment → view detail của tenant đó 5. Profile số PERSIST cross-tenant (skills, certs, history) # Worker KHÔNG truy cập subdomain/custom domain tenant # Lý do: worker có thể làm nhiều tenant, không thể gán 1 subdomain

Employer User (multi-tenant capable)

1. Nhập URL: viecxanh.vn/employer hoặc business mobile app 2. Login → JWT với user_id + danh sách tenants accessible 3. Dashboard hiển thị tenant switcher dropdown nếu có nhiều tenants: ┌───────────────────────────┐ │ ▼ Current: Nhà máy ABC │ ← tenant_id 42 │ Switch to: │ │ - NCC XYZ (tenant 87) │ │ - Holding MNP (t. 123) │ └───────────────────────────┘ 4. Switch tenant → set X-Tenant-Slug header, refetch dashboard 5. URL stays viecxanh.vn/employer (single domain for employers) # Có thể jump sang admin SPA nếu có role # Click "Admin Portal" → redirect tới factory-abc.viecxanh.vn/admin

Super Admin (ViệcXanh ops)

1. Nhập URL: system.viecxanh.vn 2. Login với email + password + 2FA (TOTP) 3. JWT với super_admin=true flag + no tenant_id lock 4. Access all tenants data + platform operations: - Create new tenant - Billing dashboard - Cross-tenant audit log - Platform-wide analytics 5. Khi cần switch vào 1 tenant specific để debug: - Click "Impersonate factory-abc" - Redirect: factory-abc.viecxanh.vn/admin?impersonation_token=... - Tenant admin SPA shows "Impersonating (super admin)" banner - All actions logged với impersonator user_id

7. Tiers — Basic vs Premium

Dual-domain support cho phép tier pricing rõ ràng.

📘 Basic Tier

Mặc định cho mọi tenant · Included
  • Subdomain {slug}.viecxanh.vn auto-provision
  • SSL wildcard (zero setup)
  • Full admin features
  • Email notifications from noreply@viecxanh.vn
  • Standard support
Onboarding: ~5 phút · chỉ cần tenant creation form

⭐ Premium Tier (+ Custom Domain)

Extra fee · ~500k-1M VND/tháng
  • Tất cả Basic features
  • Custom domain abc.com.vn hoặc subdomain riêng
  • SSL auto-provision via Let's Encrypt
  • Email notifications from noreply@abc.com.vn (SMTP config riêng)
  • Custom branding: logo, colors trong admin UI
  • Priority support + dedicated account manager
  • White-label mobile option (Phase 5+)
Onboarding: ~1 ngày · DNS setup + verify + SSL provision

Tenant có thể đổi tier bất kỳ lúc nào

ScenarioImpact
Basic → Premium (thêm custom domain)Subdomain vẫn live, custom domain provision async
Premium → Basic (bỏ custom domain)Custom domain dừng serve (redirect về subdomain 6 tháng)
Đổi custom domainOld domain sunset 30 ngày, new domain provision
Tạm ngưng subscriptionTenant suspended, URL hiển thị "Tài khoản tạm ngưng"

8. Infrastructure Requirements

DNS, SSL, monitoring setup (không chi tiết config — đó là việc sau).

DNS

  • Wildcard A record *.viecxanh.vn
  • Main A record viecxanh.vn
  • Subdomain A: api, system, ai
  • Custom domain: tenant point via A/CNAME
  • Provider: Cloudflare hoặc Route53

SSL Certificates

  • Wildcard cert *.viecxanh.vn từ Let's Encrypt
  • Auto-renew 60 ngày trước expiry
  • Per-tenant cert cho custom domain
  • ACME client: acme.sh hoặc certbot
  • Cert storage: encrypted at rest trong DB hoặc AWS Secrets Manager

Load Balancer

  • Nginx hoặc Cloudflare Tunnel
  • SNI-based routing (TLS extension)
  • HTTP/2 + HTTP/3 enabled
  • Rate limiting per IP + per tenant
  • DDoS protection (Cloudflare)

Laravel Backend

  • IdentifyTenant middleware (parse Host)
  • ScopeTenant middleware (inject query scope)
  • RLS enforcement (PG session var)
  • Tenant cache (Redis, TTL 5 min)
  • Impersonation audit log

Monitoring

  • Domain health check every 5 min
  • SSL expiry alerts (30/14/7 days)
  • DNS propagation check on custom domain setup
  • Per-tenant metrics (requests, errors)
  • Sentry error tracking per tenant

Tenant Onboarding Automation

  • API: tenant create → insert DB
  • Background job: update DNS (if needed)
  • Subdomain instant (wildcard covers)
  • Custom domain: async verify + SSL
  • Email tenant admin với credentials

Summary

V4 hỗ trợ dual-domain: subdomain auto-provision cho mọi tenant + custom domain tùy chọn cho premium tier.

Tenant admin SPA serve qua cả 2 modes, cùng tenant context, cùng API. Workers/employers/public giữ single domain cho SEO + cross-tenant UX. Onboarding subdomain 5 phút, custom domain 1 ngày.