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.
| Type | Rule | Example |
|---|---|---|
| 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 |
| Routing | User 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.comin 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.comCNAME → 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_ida1b2c3d4. - 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_slugUNIQUE. - Storage: Signup/checkout →
metadata.subdomain_slug; Control Plane stores in tenants_master.
8. Schema
- tenants_master:
subdomain_slugTEXT UNIQUE,canonical_hostnameTEXT NOT NULL. - tenant_runtime:
hostaligned to canonical (or Frappe site host). - At provision complete: (1) create canonical A/CNAME, (2) if subdomain_slug present, create
subdomain_slug.pregoi.comCNAME → 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 domain | pregoi.com. Zone은 Cloudflare에서 관리. |
3. 방식 비교
방식 A: 사용자 서브도메인만 사용 (1:1 DNS)
- 규칙: 사용자가 입력한 서브도메인을 그대로 DNS 레코드 이름으로 사용.
acme→acme.pregoi.com(A 또는 CNAME). - 내부 식별:
tenant_id(UUID)는 D1 등에만 저장. DNS 이름과 tenant 매핑은 D1tenants_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으로 들어오는 것을 기준으로 테넌트 식별. - 사용자(선택): 사용자가 입력한 서브도메인
acme→acme.pregoi.com을 CNAME으로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로 정규화해도 됨.
- 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: 내부 + CNAME | D: 사용자 primary |
|---|---|---|---|---|
| DNS 레코드 수/테넌트 | 1 | 1 | 2 (canonical + CNAME) | 1 |
| 사용자 URL 예시 | acme.pregoi.com | tenant-xxx.pregoi.com (또는 리다이렉트) | acme.pregoi.com | acme.pregoi.com |
| 서브도메인 변경 | DNS·SSL 등 수정 필요 | N/A(표시만) | CNAME만 변경 | DNS·SSL 등 수정 필요 |
| 내부 라우팅 안정성 | Host 기반 매핑 필요 | 매우 높음 | 매우 높음 | Host 기반 매핑 필요 |
| 검증 부담 | 높음(중복·예약어) | 낮음 | 중간(사용자 slug만) | 높음 |
| B2B 브랜딩 | 좋음 | 나쁨 | 좋음 | 좋음 |
5. 권장안: 방식 C (내부 canonical + 사용자 CNAME)
Prego 서비스에는 방식 C를 권장한다.
- Frappe 멀티테넌시: 현재 설계가 “1노드·DNS 기반 멀티테넌트”이므로, canonical hostname (
tenant-{short_id}.pregoi.com)을 Frappe site와 1:1로 고정해 두면 site 추가·이전 시 라우팅이 단순해진다. - 가입 시 흐름: 사용자가 서브도메인
acme입력 → 중복·예약어 검증 →tenant_id생성 및 canonicaltenant-{short_id}결정 → 프로비저닝 단계에서 DNS 2건 생성 (canonical → 오리진, acme → CNAME to canonical). - 변경·이전: 회사명 변경 등으로 서브도메인만 바꿀 때는 CNAME 대상은 그대로 두고, 사용자 레코드만 수정/삭제·추가하면 된다.
- 보안: 서브도메인( 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_slugTEXT UNIQUE — 사용자 입력 서브도메인 (예:acme).canonical_hostnameTEXT NOT NULL — 내부 고정 FQDN (예:tenant-a1b2c3d4.pregoi.com).
- tenant_runtime
host: 이미 있으면 canonical hostname과 일치시키거나, Frappe site의 host로 사용.- 필요 시
subdomain_slug는 tenants_master에만 두고, runtime은 host(canonical)만 저장.
프로비저닝 완료 시점에
- canonical A/CNAME 생성,
- subdomain_slug가 있으면
subdomain_slug.pregoi.comCNAME → canonical_hostname
순서로 DNS 생성하면 된다.
9. DNS 생성 시점·담당
| 시점 | 담당 | 비고 |
|---|---|---|
| 프로비저닝 워크플로 내 | GitHub Actions 단계 또는 전용 Worker | Pulumi 이후, Ansible 이후 또는 “DNS 등록” 단계에서 Cloudflare API 호출. |
| Control Plane에서 직접 | Control Plane Worker | workflow_dispatch 호출 전/후에 Cloudflare API로 레코드 생성. tenant_id·canonical·subdomain_slug는 D1에서 조회. |
| Pulumi | Pulumi 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_master에subdomain_slug(unique),canonical_hostname(not null) 추가.
가입 시 입력한 서브도메인으로 {subdomain}.pregoi.com 형식의 DNS가 등록되며, 내부는 항상 canonical로 안정적으로 라우팅·운영한다.