OAuth2 IdP + gatewayThe single front door: issues JWTs, verifies them, and proxies every service behind one trust boundary.
Shared platform component. Domain applications are documented separately.
Canopy is the canopy: everything passes under it. It is the OAuth2 identity provider that issues and verifies JWTs (with per-service audiences), and the API gateway that proxies the browser to Grove and Acorn. It also resolves a user’s tenant: at login it mints an enterprise_id claim from the user’s active membership, and on every proxied call it forwards a trusted X-User-Enterprise header, so multi-tenant isolation is anchored at the one boundary that already owns identity.
OAuth2 (authorization-code + password grant) with per-service audiences; the shared oak-tokens contract lets every service verify a token the same way.
Proxies /grove/* and /api/acorn/* from the browser, holding the service key so end users never touch services directly.
Mints the enterprise_id claim from a user’s active membership and forwards X-User-Enterprise on every call, resolved server-side, so a client can never spoof its tenant.
Grove and Acorn sit behind Canopy; identity AND tenancy are established once, at one auditable surface, not scattered per-service.
In a multi-service system, the most dangerous thing is identity logic copied into every service. Canopy collapses it: it is the only thing that issues tokens and the only thing the browser talks to. Sky logs in against Canopy; Canopy proxies /grove/* and /api/acorn/*, carrying the service key the end user never sees.
Tokens are JWTs with per-service audiences, verified via Canopy's JWKS using the shared oak-tokens contract, so Grove, Acorn, and Ranger all check identity the same way. Canopy validates the user's token at the edge and forwards the resolved user downstream, which is exactly why a service like Grove can trust a forwarded user identity without re-deriving it.
Multi-tenant isolation is only as strong as where the tenant is decided. Canopy decides it: at token-mint it reads the user's active EnterpriseMembership (from the shared Timber model) and stamps an enterprise_id claim; on every proxied request it re-resolves and forwards a trusted X-User-Enterprise header, so a revoked membership takes effect immediately, and a forged ?enterprise= or body value is ignored. Grove never has to guess which tenant a request belongs to; the answer arrives, already verified, at the edge.
Because every request enters through Canopy, the trust boundary is a single auditable surface. Identity is centralized, while each serving service still enforces role, tenant and action permissions.