ADR-003 — Brand layer
18935df. Phases 2 through 7 followed across the subsequent week and are live. The full ADR also tracks the per-Brand BYO Entra, per-Brand BYO bot reg, and per-Brand provider config phases that are now in production.Context
Before this ADR Aroovo was a flat multi-tenant platform: a Tenant model with tenantId columns on 13+ tables, per-tenant audit / rate-limit / spend cap / standups / broadcast / vault isolation, and a 3-step wizard at /admin/tenants/new. Selling that as a white-label platform-as-a-service to agencies and partners needed a layerabove Tenant: the reseller's commercial relationship with us, their branding, their customers' isolation from each other.
Decision
Introduce a Brand model above Tenant. Every existing Tenant gets a mandatory brandId FK pointing at one Brand row. A single aroovo Brand is seeded by an idempotent boot hook; the existing default Tenant backfills to it. Reseller onboarding adds Brand rows; their Tenants point at those rows. Platform superadmin is the Owner of the aroovo Brand; Brand-Owner is a new sibling role scoped to a single Brand.
Phase plan
- Phase 1 — Schema + read APIs.
Brandmodel,brandIdFK on Tenant,GET /api/admin/brands. - Phase 2 — Brand CRUD. Create / patch / suspend / offboard / activate / delete; admin UI at
/admin/brands. - Phase 3 — Theming + hostname routing. Per-Brand
themeJson+primaryDomain+ DNS-TXT verification; edge middleware attachesx-brand-id. - Phase 4 — BrandOwner + BYO Entra. New role; per-Brand
entraTenantId+entraClientId; per-Brand Bot Framework app registrations. - Phase 5 — Offboarding lifecycle. 30-day soft-delete with bundle export of every audit row, tenant config, KB file, and secret reference.
- Phase 6 — Per-Brand provider config.
providerConfigJsonwith secret references for Anthropic / OpenAI / OpenRouter / OpenClaw keys. - Phase 7 — Brand-Owner analytics. Request count + latency rollups per Brand.
Schema (excerpt)
model Brand {
id String @id @default(cuid())
slug String @unique
name String
status String @default("active")
primaryDomain String? @unique
themeJson String?
// BYO Entra
entraTenantId String?
entraClientId String?
// BYO Bot Framework
botRegistrationJson String?
// BYO LLM provider config
providerConfigJson String?
// BYO OIDC (ADR-004)
authProvider String? // "entra" | "google" | "okta" | "auth0"
authProviderConfigJson String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenants Tenant[]
}Consequences
- Positive. Multi-tenant + white-label simultaneously, with structural enforcement. Per-Brand IdP + provider keys mean Aroovo doesn't pay the customer's LLM bill. Hostname routing makes the platform appear to be the Brand's own software.
- Negative. Every per-tenant feature now has a Brand dimension to reason about. Backfilling 13+ tables was a one-shot migration. BrandOwner role added complexity to RBAC.
References
Full markdown: adr-003-brand-layer.md.
Related: Concepts — Platform → Brand → Tenant → Agent, ADR-004 Alt auth providers.