Skip to content

English {#english}

Tenant Subdomain and DNS Design — tenant.pregoi.com

Purpose: Compare and recommend how to register {subdomain}.pregoi.com from user input at signup and integrate it with stable internal routing and operations.


Adopted approach: C (internal canonical + user subdomain CNAME)

Prego uses approach C.

TypeRuleExample
Internal (always)tenant-{short_id}.pregoi.com. short_id = first 8 chars of tenant_id (UUID). Only this address is 1:1 with server/Tunnel and Frappe site.tenant-a1b2c3d4.pregoi.com
User (signup input){subdomain_slug}.pregoi.com. DNS CNAME points to canonical.acme.pregoi.com → CNAME → tenant-a1b2c3d4.pregoi.com
RoutingUser reaches acme.pregoi.com; actual routing uses canonical only (same origin after DNS).
  • Canonical: Server, Tunnel, Frappe site are all keyed by tenant-{short_id}.pregoi.com. One place to change on migration.
  • User URL: Branding (e.g. acme.pregoi.com). Change by updating CNAME only.

1. Requirements

  • At signup (or package request), user enters a subdomain → provide URL {subdomain}.pregoi.com.
  • DNS must resolve that subdomain to the real service (Frappe/app).
  • A stable internal identifier simplifies operations, routing, and provisioning.
  • Reserved names, duplicates, and abusive input must be blocked.

2. Terms

  • User-requested subdomain: Name entered at signup (e.g. acme, company-sg). Used for customer-facing URL.
  • Internal canonical subdomain: System-assigned stable name (e.g. tenant-a1b2c3d4). Used for DNS, Frappe site, routing.
  • Base domain: pregoi.com (Cloudflare zone).

3. Approach comparison (summary)

  • A — User subdomain only: One DNS record per tenant (acme.pregoi.com). Simple; validation and subdomain changes are heavier.
  • B — Internal canonical only: Only tenant-{short_id}.pregoi.com in DNS; user subdomain in D1 for display/redirect. Stable; poor branding unless redirect adds CNAME (then like C).
  • C — Internal + user CNAME (recommended): Canonical A/CNAME + user acme.pregoi.com CNAME → canonical. Stable; two records; user slug change = CNAME only. Best for B2B branding and subdomain changes.
  • D — User as primary: Same as A with optional canonical column for later redirect.

Comparison: C gives 2 records/tenant, user URL like acme.pregoi.com, subdomain change via CNAME only, high internal stability, medium validation (user slug). Prego recommends C.


4–5. Recommendation: Approach C

(1) Frappe multi-tenancy: Fix canonical hostname to site 1:1 for simple routing. (2) Signup: user enters acme → validate → create tenant_id, canonical → in provisioning create DNS (canonical → origin, acme CNAME → canonical). (3) Change: update only user CNAME when company name changes. (4) Security: allow subdomain (CNAME) change only after tenant admin auth.


6. Internal subdomain naming (canonical)

  • Format: tenant-{short_id}.pregoi.com
  • short_id: First 8 characters of tenant_id (UUID). Example: tenant_id a1b2c3d4-e5f6-... → short_id a1b2c3d4.
  • Use hyphen only (tenant- + short_id).

7. User subdomain validation

  • Chars: Lowercase, digits, hyphen. Regex e.g. ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$ (length 3–63, ends alphanumeric).
  • Reserved: www, app, api, admin, mail, staging, dev, prego, pregoi, support, help, status, login, signup, billing, dashboard, node-01, etc.
  • Duplicate: D1 tenants_master.subdomain_slug UNIQUE.
  • Storage: Signup/checkout → metadata.subdomain_slug; Control Plane stores in tenants_master.

8. Schema

  • tenants_master: subdomain_slug TEXT UNIQUE, canonical_hostname TEXT NOT NULL.
  • tenant_runtime: host aligned to canonical (or Frappe site host).
  • At provision complete: (1) create canonical A/CNAME, (2) if subdomain_slug present, create subdomain_slug.pregoi.com CNAME → canonical_hostname.

9. When and who creates DNS

  • Recommended: A “DNS registration” step inside the provisioning pipeline (script/Worker) calling Cloudflare API. Pulumi handles nodes/common infra only; tenant DNS is dynamic, so create/delete via API with tenant_id, canonical_hostname, subdomain_slug from D1.
  • Alternatives: Control Plane Worker before/after workflow_dispatch; or Pulumi reading tenant list from D1 (state grows in Pulumi).

10. Summary

  • Adopted: Internal canonical (tenant-{short_id}.pregoi.com) + user CNAME (acme.pregoi.com → canonical).
  • Internal: tenant-{short_id}.pregoi.com (short_id = first 8 of tenant_id UUID). 1:1 with server/Tunnel and Frappe site.
  • User: Signup subdomain → acme.pregoi.com; DNS CNAME to canonical; routing uses canonical only.
  • DNS creation: In provisioning flow, Cloudflare API for canonical + user CNAME.
  • Schema: tenants_master: subdomain_slug (unique), canonical_hostname (not null).

Full comparison tables (A/B/C/D pros and cons) and detailed wording are in the Korean section below.


한국어 {#korean}

테넌트 서브도메인·DNS 설계 — tenant.pregoi.com 형식 등록

목적: 가입 시 사용자가 입력한 서브도메인으로 {subdomain}.pregoi.com 형식의 DNS를 등록하고, 내부 라우팅·운영과 안정적으로 연동하는 방식을 비교·권장.


채택 방식: 방식 C (내부 canonical + 사용자 서브도메인 CNAME)

Prego 서비스는 방식 C를 채택한다.

구분규칙예시
내부(항상 사용)tenant-{short_id}.pregoi.com. short_id = tenant_id(UUID) 앞 8자. 이 주소만 서버/Tunnel·Frappe site와 1:1로 고정.tenant-a1b2c3d4.pregoi.com
사용자(가입 시 입력){subdomain_slug}.pregoi.com. DNS는 CNAME으로 canonical을 가리킴.acme.pregoi.com → CNAME → tenant-a1b2c3d4.pregoi.com
라우팅사용자는 acme.pregoi.com으로 접속하고, 실제 라우팅은 canonical로만 처리. (DNS 해석 후 동일 오리진 도달)
  • canonical: 서버·Tunnel·Frappe site 식별은 모두 tenant-{short_id}.pregoi.com 기준. 변경·이전 시 한 곳만 수정.
  • 사용자 URL: acme.pregoi.com 등 브랜딩용. 변경 시 CNAME만 수정하면 됨.

1. 요구사항 정리

  • 가입(또는 패키지 신청) 시 사용자 입력 서브도메인으로 {subdomain}.pregoi.com 형태의 URL 제공.
  • DNS에 해당 서브도메인이 등록되어 실제 서비스( Frappe / 앱)로 라우팅되어야 함.
  • 내부적으로는 고정·안정적인 식별자가 있으면 운영·라우팅·프로비저닝이 단순해짐.
  • 예약어·중복·악의적 입력 방지가 필요함.

2. 용어 정의

용어의미
사용자 신청 서브도메인가입/신청 시 사용자가 입력하는 이름 (예: acme, company-sg). 노출용 URL에 사용.
내부 canonical 서브도메인시스템이 부여하는 고정 이름 (예: tenant-1, tenant-abc12def). DNS·Frappe site·라우팅의 안정 식별자.
base domainpregoi.com. Zone은 Cloudflare에서 관리.

3. 방식 비교

방식 A: 사용자 서브도메인만 사용 (1:1 DNS)

  • 규칙: 사용자가 입력한 서브도메인을 그대로 DNS 레코드 이름으로 사용. acmeacme.pregoi.com (A 또는 CNAME).
  • 내부 식별: tenant_id(UUID)는 D1 등에만 저장. DNS 이름과 tenant 매핑은 D1 tenants_master.subdomain_slug(unique)로 보관.
장점단점
구현 단순. DNS 레코드 1개/테넌트.서브도메인 중복·예약어·부적절어 검증 필수.
사용자에게 보이는 URL과 DNS가 동일해 이해하기 쉬움.사용자 서브도메인 변경 시 DNS 수정·SSL 재발급·캐시 무효 등 부가 작업.
UUID 등이 URL에 안 보여서 “내부용 이름”이 드러나지 않음.

적합: 서브도메인 변경을 거의 허용하지 않고, 검증 규칙을 엄격히 둘 수 있을 때.


방식 B: 내부 canonical만 사용 (tenant-{id}.pregoi.com)

  • 규칙: DNS에는 내부 이름만 등록. 예: tenant-{short_id}.pregoi.com (short_id = tenant_id 앞 8자 또는 숫자 부여). 사용자 입력 서브도메인은 DNS에 반영하지 않고 D1에만 저장(표시·리다이렉트용).
  • 사용자 경험: 앱에서 “귀사 주소: acme.pregoi.com”처럼 표시만 하거나, 접속 시 acme.pregoi.com → 301 리다이렉트로 tenant-abc12def.pregoi.com으로 보낸다. 리다이렉트를 위해 acme.pregoi.com도 DNS가 필요하면 결국 방식 C와 유사해짐.
장점단점
DNS·라우팅이 내부 ID 기준으로만 동작해 단순·안정.사용자가 실제로 접속하는 URL이 tenant-xxx.pregoi.com이면 브랜딩·신뢰도가 떨어짐.
사용자 입력 검증 부담이 적음 (DNS와 무관).“acme.pregoi.com”으로 접속하려면 어딘가에 acme → tenant-xxx 매핑 + DNS 또는 Worker 리다이렉트 필요.
예약어·중복을 DNS와 분리해 관리 가능.리다이렉트용으로 acme.pregoi.com을 등록하면 방식 C와 거의 동일.

적합: “예쁜 URL”이 필수가 아니고, 내부 운영 단순화가 최우선일 때.


방식 C: 내부 canonical + 사용자 서브도메인 CNAME (권장)

  • 규칙
    • 내부(항상 존재): tenant-{short_id}.pregoi.com → 서버 IP 또는 Tunnel(CNAME). Frappe·앱은 Host가 tenant-{short_id}.pregoi.com으로 들어오는 것을 기준으로 테넌트 식별.
    • 사용자(선택): 사용자가 입력한 서브도메인 acmeacme.pregoi.comCNAME으로 tenant-{short_id}.pregoi.com을 가리키게 등록.
  • 라우팅: acme.pregoi.com으로 요청이 오면 DNS 해석으로 tenant-xxx.pregoi.com으로 연결되고, 동일 서버/오리진이므로 Frappe는 Host 헤더로 사이트를 구분할 수 있음.
    • Host가 acme.pregoi.com인 요청도 오리진에 도달하므로, 오리진(Frappe/앱)에서 Host → tenant 매핑(D1/KV 조회: acme.pregoi.com → tenant_id)으로 처리하거나, 항상 canonical Host로 정규화해도 됨.
장점단점
내부 ID는 불변: 서버·Tunnel·Frappe site는 tenant-{id} 기준으로만 구성하면 됨.DNS 레코드 2개/테넌트 (canonical A/CNAME + 사용자 CNAME).
사용자 서브도메인 변경 용이: acme → acme-corp 변경 시 CNAME만 수정(또는 삭제 후 신규 생성). canonical은 그대로.사용자 서브도메인에 대한 중복·예약어·형식 검증 필요.
브랜딩: 고객에게 acme.pregoi.com 같은 읽기 쉬운 URL 제공 가능.CNAME만 바꾸면 되므로 “서브도메인 탈취” 등 보안 정책(변경 시 인증) 필요.
장애·이전: canonical만 바라보면 되므로 서버 이전·Tunnel 변경 시 한 곳만 수정.

적합: Prego처럼 B2B·회사 단위 서비스에서 “회사명.pregoi.com” 형태의 URL을 주고, 나중에 회사명(서브도메인) 변경을 허용할 가능성이 있는 경우.


방식 D: 사용자 서브도메인을 “primary”, 내부 ID는 DB만

  • 규칙: DNS에는 사용자 입력만 등록 (acme.pregoi.com). 내부 식별은 tenant_id(UUID). tenants_master.subdomain_slug = acme (unique).
    즉 방식 A와 동일하되, “canonical hostname” 컬럼을 두어 나중에 리다이렉트나 정규화에 쓸 수 있게 함 (초기에는 subdomain_slug와 동일 값으로 둠).
장점단점
DNS 1개, 단순. 사용자 URL = 실제 도메인.방식 A와 동일한 단점(검증·변경 시 DNS/SSL 등).

적합: 서브도메인 변경을 허용하지 않고, 단일 레코드로 운영하고 싶을 때.


4. 비교 요약

항목A: 사용자만B: 내부만C: 내부 + CNAMED: 사용자 primary
DNS 레코드 수/테넌트112 (canonical + CNAME)1
사용자 URL 예시acme.pregoi.comtenant-xxx.pregoi.com (또는 리다이렉트)acme.pregoi.comacme.pregoi.com
서브도메인 변경DNS·SSL 등 수정 필요N/A(표시만)CNAME만 변경DNS·SSL 등 수정 필요
내부 라우팅 안정성Host 기반 매핑 필요매우 높음매우 높음Host 기반 매핑 필요
검증 부담높음(중복·예약어)낮음중간(사용자 slug만)높음
B2B 브랜딩좋음나쁨좋음좋음

5. 권장안: 방식 C (내부 canonical + 사용자 CNAME)

Prego 서비스에는 방식 C를 권장한다.

  1. Frappe 멀티테넌시: 현재 설계가 “1노드·DNS 기반 멀티테넌트”이므로, canonical hostname (tenant-{short_id}.pregoi.com)을 Frappe site와 1:1로 고정해 두면 site 추가·이전 시 라우팅이 단순해진다.
  2. 가입 시 흐름: 사용자가 서브도메인 acme 입력 → 중복·예약어 검증 → tenant_id 생성 및 canonical tenant-{short_id} 결정 → 프로비저닝 단계에서 DNS 2건 생성 (canonical → 오리진, acme → CNAME to canonical).
  3. 변경·이전: 회사명 변경 등으로 서브도메인만 바꿀 때는 CNAME 대상은 그대로 두고, 사용자 레코드만 수정/삭제·추가하면 된다.
  4. 보안: 서브도메인( CNAME) 변경 시에는 “해당 테넌트 관리자 인증 후에만 허용”하는 정책을 두는 것이 좋다.

6. 내부 서브도메인 네이밍 규칙 (canonical) — 채택

  • 형식: tenant-{short_id}.pregoi.com
  • short_id: tenant_id(UUID) 앞 8자 (채택). 예: tenant_id가 a1b2c3d4-e5f6-7890-abcd-ef1234567890이면 short_id = a1b2c3d4 → canonical = tenant-a1b2c3d4.pregoi.com.
  • 특징: tenant_id와 1:1 대응, 별도 시퀀스 없이 유일성 보장. 서버·Tunnel·Frappe site는 이 canonical만 1:1로 사용.
  • 구분자: 하이픈(-)만 사용. (tenant- + short_id. DNS/시스템에서 _ 제한 가능성 대비.)

7. 사용자 서브도메인 규칙 (검증)

  • 문자: 소문자, 숫자, 하이픈만 허용. 정규식 예: ^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$ (길이 3~63, 양 끝은 영숫자).
  • 예약어: www, app, api, admin, mail, staging, dev, prego, pregoi, support, help, status, login, signup, billing, dashboard, node-01 등. (운영에서 사용하는 서브도메인 전부)
  • 중복: D1 tenants_master.subdomain_slug(또는 전용 테이블)에 unique 제약으로 보장.
  • 저장: 가입/체크아웃 시 metadata.subdomain_slug로 전달하고, Control Plane에서 tenants_master에 저장.

8. 데이터·스키마 제안

  • tenants_master (또는 마이그레이션)
    • subdomain_slug TEXT UNIQUE — 사용자 입력 서브도메인 (예: acme).
    • canonical_hostname TEXT NOT NULL — 내부 고정 FQDN (예: tenant-a1b2c3d4.pregoi.com).
  • tenant_runtime
    • host: 이미 있으면 canonical hostname과 일치시키거나, Frappe site의 host로 사용.
    • 필요 시 subdomain_slug는 tenants_master에만 두고, runtime은 host(canonical)만 저장.

프로비저닝 완료 시점에

  1. canonical A/CNAME 생성,
  2. subdomain_slug가 있으면 subdomain_slug.pregoi.com CNAME → canonical_hostname
    순서로 DNS 생성하면 된다.

9. DNS 생성 시점·담당

시점담당비고
프로비저닝 워크플로 내GitHub Actions 단계 또는 전용 WorkerPulumi 이후, Ansible 이후 또는 “DNS 등록” 단계에서 Cloudflare API 호출.
Control Plane에서 직접Control Plane Workerworkflow_dispatch 호출 전/후에 Cloudflare API로 레코드 생성. tenant_id·canonical·subdomain_slug는 D1에서 조회.
PulumiPulumi Cloudflare Provider테넌트 수가 동적이면 pulumi up 시점에 테넌트 목록을 D1/외부에서 읽어와서 cloudflare.Record 동적 생성. 상태 관리가 Pulumi에 쌓임.

권장: 프로비저닝 파이프라인 내 “DNS 등록” 단계에서 Cloudflare API를 호출하는 스크립트/Worker가 담당.

  • Pulumi는 “노드·공통 인프라”만 담당하고, 테넌트별 DNS는 동적이므로 Pulumi 리소스로 넣지 않고, 워크플로/Worker에서 tenant_id·canonical_hostname·subdomain_slug를 받아 Cloudflare API로 CNAME/A 생성·삭제.
  • 이렇게 하면 테넌트 해지 시 같은 Worker/스크립트에서 해당 테넌트 DNS만 삭제하기 쉽다.

10. 요약

  • 채택 방식: 내부 canonical (tenant-{short_id}.pregoi.com) + 사용자 서브도메인 CNAME (acme.pregoi.com → canonical).
  • 내부: tenant-{short_id}.pregoi.com (short_id = tenant_id UUID 앞 8자). 이 주소만 서버/Tunnel·Frappe site와 1:1로 고정.
  • 사용자: 가입 시 입력한 서브도메인 → acme.pregoi.com. DNS는 CNAME으로 canonical을 가리킴. 사용자는 acme.pregoi.com으로 접속하고, 실제 라우팅은 canonical로만 처리.
  • DNS 생성: 프로비저닝 플로우 내 “DNS 등록” 단계에서 Cloudflare API로 canonical + 사용자 CNAME 생성.
  • 스키마: tenants_mastersubdomain_slug(unique), canonical_hostname(not null) 추가.

가입 시 입력한 서브도메인으로 {subdomain}.pregoi.com 형식의 DNS가 등록되며, 내부는 항상 canonical로 안정적으로 라우팅·운영한다.

Help