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.vn và factory-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:
- Host header — khi request từ subdomain hoặc custom domain (most reliable)
- X-Tenant-Slug header — explicit set bởi client app
- 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
| Column | Type | Purpose |
| slug | VARCHAR(50) UNIQUE | Always required · basis for subdomain {slug}.viecxanh.vn |
| custom_domain | VARCHAR(255) UNIQUE | Nullable · tenant's custom domain abc.com.vn |
| custom_domain_verified_at | TIMESTAMPTZ | NULL = not verified yet, NOT NULL = active |
| custom_domain_ssl_status | ENUM | Track Let's Encrypt cert provision |
| custom_domain_verification_token | VARCHAR(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)
- Tenant admin vào factory-abc.viecxanh.vn/admin/settings/domains
- Click "Thêm custom domain" → nhập abc.com.vn
- System generate random verification token (64 chars) → lưu vào custom_domain_verification_token
- 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)
- Tenant thêm TXT record vào DNS provider của họ (Cloudflare, Route53, etc.)
- Tenant click "Verify now" trên admin UI (hoặc system auto-check mỗi 5 phút)
- System thực hiện DNS lookup:
dig TXT _viecxanh-verify.abc.com.vn +short
# Compare with stored token
- Nếu match → set custom_domain_verified_at = NOW()
- Send email "Domain verified" to tenant admin
Phase 3: SSL Provision (Automatic)
- System trigger SSL job via Let's Encrypt ACME client (certbot hoặc acme.sh)
- HTTP-01 challenge: system serve /.well-known/acme-challenge/... trên abc.com.vn
- Tenant đã point A record abc.com.vn → viecxanh server IP
- Let's Encrypt validate → issue cert
- Cert stored trong tenant_ssl_certificates table, nginx reload
- Set custom_domain_ssl_status = 'active'
- Email "SSL active — your domain is live" to tenant admin
Phase 4: Auto-renewal
- Scheduled job daily: check cert expiry
- If expires < 30 days → trigger ACME renew
- On renew success → reload nginx, update DB
- 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.vn và abc.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
| Scenario | Impact |
| 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 domain | Old domain sunset 30 ngày, new domain provision |
| Tạm ngưng subscription | Tenant 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.