Pular para o conteúdo

Rotas públicas — slug, vanity URL e custom domain

Status: corrigir · Escopo: Contrato de rotas públicas e identidade por slug (canal web) · Atualizado em: 2026-08-04

Contrato de identidade pública do canal web (/book/…, embed.js). Separa ID técnico estável (roteamento) de marca visível (nome bonito na UI e, depois, na URL).

Código hoje: demo_sites · demo-sites.repo.ts · public-book-route.ts


CamadaDescriçãoStatus
L0 — Slug técnico globaldemo_sites.slug UNIQUE → (tenant_id, unit_id)
L1 — Vanity pathURLs legíveis (/book/glow-aesthetics) + aliases✅ parcial — public_site_routes + resolver
L2 — Custom domainbook.cliente.com via hostname lookup⏳ Ato 3 (E32.T7)
1 filial = 1 site canônicoUNIQUE (tenant_id, unit_id) em demo_sites⏳ migration

  1. Slug ≠ marca. O visitante vê business_name + tema no widget; o slug é chave de roteamento (pode ficar só no data-slug do script).
  2. 1 slug público → 1 filial (unit_id). Empresa (tenant_id) com N filiais = N rows em demo_sites, N slugs distintos.
  3. Unicidade global do slug. Duas clínicas na plataforma nunca compartilham o mesmo slug — evita colisão em URLs compartilhadas e embed.
  4. Aliases não quebram embed antigo. Renomear vanity URL não exige trocar script no site do cliente (slug técnico ou site_id estável).
  5. Custom domain é Ato 3, não gate do 1º design partner — vanity path (L1) cobre “nome bonito” antes disso.

flowchart LR
URL["/book/glow-medspa"] --> DS[demo_sites.slug]
EMB["embed.js data-slug"] --> DS
DS --> T[tenant_id empresa]
DS --> U[unit_id filial]
U --> BH[business_hours slots]
U --> OFF[service_offerings]
U --> CH[channel web session]
CampoTabelaEscopoExemplo
Slug públicodemo_sites.slugGlobal UNIQUEglow-medspa
Slug da empresatenants.slugGlobal UNIQUEglow-medspa
Slug da filialunits.slugUNIQUE por tenantmain, downtown
Slug de serviçoservice_offerings.slugUNIQUE por (tenant, unit)new-client-consult

Não confundir: service_offerings.slug é serviço bookável dentro da filial; demo_sites.slug é a filial inteira na web.

Lookup (sem RLS — bootstrap público):

SELECT tenant_id, unit_id FROM demo_sites
WHERE slug = $1 AND active = true;

Migration: 0024_demo_web_channel.sql.

Filialunit_iddemo_sites.slug (hoje)Vanity sugerido (L1)
Austin…002glow-austin/book/glow-aesthetics/austin
Dallas…003glow-dallas/book/glow-aesthetics/dallas

Cada site cola seu embed com o slug da filial:

<script src="https://ORIGIN/embed.js" data-slug="glow-austin"></script>

Além da identidade por slug, o Worker expõe estas sub-rotas sob /book/:slug (e /demo/:slug legado):

MétodoSub-rotaHandlerStatus
GET(vazio) ou /widgethandleDemoPageGet
GET/confighandleDemoConfigGet
POST/chathandleDemoChatPost
GET/availability?session_id=handleDemoAvailabilityGet
GET/history?session_id=handleDemoHistoryGet✅ parcial — texto only
POST/contacthandleDemoContactPost✅ parcial
POST/reschedule-sessionhandleDemoRescheduleSessionPost
POST/close-conversationhandleDemoCloseConversationPost✅ — runtime_state=closed + rotaciona session_id
GET·POST/checkouthandleDemoCheckout{Get,Post}✅ — depósito Stripe/Asaas
GET/payment-status?session_id=handleDemoPaymentStatusGet

Mapa completo (embed postMessage, agent loop, config layers): web-booking-wedge.md.


Problema: slug técnico na URL (glow-medspa) não escala branding; cliente quer path legível.

Solução: tabela de rotas públicas — slug técnico permanece; vanity é camada extra.

CREATE TABLE public_site_routes (
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
demo_site_id uuid NOT NULL REFERENCES demo_sites(id) ON DELETE CASCADE,
route_key text NOT NULL UNIQUE, -- normalizado: lowercase, hyphen
kind text NOT NULL CHECK (kind IN ('primary','alias')),
active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX uq_public_site_routes_one_primary
ON public_site_routes (demo_site_id) WHERE kind = 'primary' AND active = true;
ALTER TABLE demo_sites
ADD CONSTRAINT uq_demo_sites_tenant_unit UNIQUE (tenant_id, unit_id);
route_keyResolve para
glow-aestheticsdemo_site Austin (primary)
glow-aesthetics-austinalias → mesmo site
glow-medspaslug legado em demo_sites.slug (backward compat)
  1. Host ≠ default → L2 hostname table (futuro).
  2. Path /book/:route_key/...public_site_routes.route_key.
  3. Fallback → demo_sites.slug (comportamento atual).
✅ Bom❌ Evitar
lakeview-med-spatenant_018fa101
glow-aesthetics-austinglow-demo-001
{marca}-{cidade}slugs genéricos colidindo entre clientes

Para design partner grande / whitelabel (docs/archive/master-strategy-v1.md §6b, docs/archive/competitive-matrix-v1.md §8).

CREATE TABLE public_hostnames (
id uuid PRIMARY KEY DEFAULT uuid_generate_v7(),
demo_site_id uuid NOT NULL REFERENCES demo_sites(id) ON DELETE CASCADE,
hostname text NOT NULL UNIQUE, -- book.glowaesthetics.com
ssl_status text NOT NULL DEFAULT 'pending',
verified_at timestamptz,
active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
);

Fluxo:

  1. Cliente CNAME book.cliente.comsites.tactflow.io (Cloudflare for SaaS).
  2. Worker: header Hostpublic_hostnamesdemo_site_id → tenant scope.
  3. Embed no site do cliente: script no domínio deles; iframe pode ser same-origin ou cross-origin com CSP.

Gate: Ato 2+ ou objeção recorrente na trilha G; não antes do 1º case pago.


SuperfícieHojeCom L1Com L2
URL no browsertactflow.io/book/glow-medspatactflow.io/book/glow-aestheticsbook.glowaesthetics.com
Título / header widgetbusiness_nameidemidem
Atributo do scriptdata-slug="glow-medspa"data-slug ou futuro data-site-ididem
Stripe return URLPUBLIC_BASE_URL/book/:slug?…canonical route ou hostnamehostname do cliente

Nome bonito já existe na UI via demo_sites.business_name e config.theme. L1/L2 só melhoram URL e confiança — não bloqueiam Ato 1.


  • demo_sites.slug UNIQUE global
  • findBySlugrunInTenantScope
  • UNIQUE (tenant_id, unit_id) — migration pendente
  • Tabela public_site_routes + seeder (0027, harness + seed:demo)
  • Resolver unificado (route_key → demo_site, slug legado fallback)
  • Slug legado continua funcionando
  • Aliases adicionais + doc onboarding naming
  • Tabela public_hostnames + verificação DNS
  • Worker routing por Host
  • Stripe redirect URLs respeitam hostname do site

  • web-booking-wedge.md — mapa operacional embed + APIs
  • docs/archive/product-today.md — narrativa do wedge shipped
  • client-config-surface.mddemo_sites.config, depósito, tema (orchestration)
  • docs/archive/v5-walking-skeleton-runbook.md — embed install
  • roadmap.md — E32.T6, E32.T7
  • docs/archive/master-strategy-v1.md §6b — marca por vertical / WL

Criado: 2026-06-12 · owner: founder