ADR-003 — Brand layer

Phase 1 shipped 2026-05-24 in merge 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

  1. Phase 1 — Schema + read APIs. Brand model, brandId FK on Tenant, GET /api/admin/brands.
  2. Phase 2 — Brand CRUD. Create / patch / suspend / offboard / activate / delete; admin UI at /admin/brands.
  3. Phase 3 — Theming + hostname routing. Per-Brand themeJson + primaryDomain + DNS-TXT verification; edge middleware attaches x-brand-id.
  4. Phase 4 — BrandOwner + BYO Entra. New role; per-Brand entraTenantId + entraClientId; per-Brand Bot Framework app registrations.
  5. Phase 5 — Offboarding lifecycle. 30-day soft-delete with bundle export of every audit row, tenant config, KB file, and secret reference.
  6. Phase 6 — Per-Brand provider config. providerConfigJson with secret references for Anthropic / OpenAI / OpenRouter / OpenClaw keys.
  7. 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.