From 6f4880c2a4892f10a10e83d8d61df44d1471541e Mon Sep 17 00:00:00 2001 From: vedavith Date: Tue, 9 Jun 2026 06:35:38 +0530 Subject: [PATCH 1/2] Update docs to reflect JwtTenantResolver CONCEPT.md: added JwtTenantResolver to the TenantResolver section with all three config keys documented. ARCHITECTURE.md: added JwtTenantResolver to the system map and a new Tenant Resolvers section with a config table and YAML example. CLEANUP.md: replaced the "No JWT resolver" note with the current state and flagged session-based resolution as the remaining gap. --- docs/ARCHITECTURE.md | 29 +++++++++++++++++++++++++++-- docs/CLEANUP.md | 2 +- docs/CONCEPT.md | 7 ++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3b7a6ab..4b99bee 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -58,7 +58,7 @@ src/ ├── TenantContext.php — static singleton holding current tenant ID ├── TenantGuard.php — throws if TenantContext is empty ├── TenantConnectionResolver.php — resolves + caches PDO connection per tenant - ├── TenantResolverFactory.php — creates header or subdomain resolver + ├── TenantResolverFactory.php — creates header, subdomain, or jwt resolver ├── TenantResolverInterface.php ├── TenantProvisioner.php — creates/drops tenant DB, runs migrations ├── TenantRepository.php — CRUD on the main DB tenants table @@ -66,7 +66,8 @@ src/ ├── RequestLifecycle.php — clears context + connection cache per request └── Resolver/ ├── HeaderTenantResolver.php — reads tenant from a request header - └── SubdomainTenantResolver.php — extracts tenant from subdomain + ├── SubdomainTenantResolver.php — extracts tenant from subdomain + └── JwtTenantResolver.php — verifies Bearer JWT, extracts tenant claim ``` --- @@ -127,6 +128,30 @@ entity_forge_corp ← tenant DB: all application data --- +## Tenant Resolvers + +Configured via `tenancy.resolver` in `application.yaml`. + +| Resolver | Config keys | How it works | +|---|---|---| +| `header` | `header_key` (default: `X-Tenant-ID`) | Reads the named HTTP header from the request context | +| `subdomain` | `subdomain_depth`, `subdomain_min_parts` (default: 3) | Extracts the leading subdomain from the `host` context key. Set `subdomain_min_parts: 2` for two-part hosts like `acme.io` | +| `jwt` | `jwt_public_key`, `jwt_algorithm` (default: `RS256`), `jwt_tenant_claim` (default: `tenant_id`) | Decodes and verifies a Bearer JWT from the `Authorization` header, then extracts the named claim | + +Example for JWT: + +```yaml +tenancy: + resolver: jwt + jwt_public_key: /path/to/public.pem + jwt_algorithm: RS256 + jwt_tenant_claim: tenant_id +``` + +Add custom resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. + +--- + ## Tenant Lifecycle ### Onboarding diff --git a/docs/CLEANUP.md b/docs/CLEANUP.md index 0c65e2d..c637c90 100644 --- a/docs/CLEANUP.md +++ b/docs/CLEANUP.md @@ -8,7 +8,7 @@ This document tracks deliberate gaps and deferred work in the current codebase. **SubdomainTenantResolver minimum parts are configurable.** The default requires 3-part hosts (`acme.example.com`). Set `tenancy.subdomain_min_parts: 2` in `application.yaml` to support two-part hosts like `acme.io`. Single-part hosts (e.g. `localhost`) always throw regardless of this setting. -**No JWT or session resolver.** `TenantResolverInterface` is designed for extension. Wire a new implementation into `TenantResolverFactory` and configure `tenancy.resolver` accordingly. +**`JwtTenantResolver` is implemented.** Decodes and verifies a Bearer JWT, extracts a configurable claim (default: `tenant_id`). Configure via `tenancy.jwt_public_key`, `tenancy.jwt_algorithm`, and `tenancy.jwt_tenant_claim`. Session-based resolution is not yet implemented — add a `SessionTenantResolver` following the same pattern. --- diff --git a/docs/CONCEPT.md b/docs/CONCEPT.md index a175e76..539ab07 100644 --- a/docs/CONCEPT.md +++ b/docs/CONCEPT.md @@ -28,11 +28,12 @@ A static singleton that holds the current tenant ID for the lifetime of a reques Resolves and caches the PDO connection for the current tenant. In `shared` mode it returns a connection to the main DB. In `database` mode it connects to `{base_db}_{tenantId}`. Connections are pooled in a static registry; call `flush()` to clear the cache. ## TenantResolver -Extracts a tenant ID from request context. Two implementations ship: -- `HeaderTenantResolver` — reads a configurable header (default: `X-Tenant-ID`) +Extracts a tenant ID from request context. Three implementations ship: +- `HeaderTenantResolver` — reads a configurable header (default: `X-Tenant-ID`). Configure via `tenancy.header_key`. - `SubdomainTenantResolver` — extracts the leading subdomain from the host (e.g. `acme.example.com` → `acme`). Set `tenancy.subdomain_min_parts: 2` to support two-part hosts like `acme.io`. +- `JwtTenantResolver` — decodes and verifies a Bearer JWT from the `Authorization` header, then extracts a configurable claim (default: `tenant_id`). Configure via `tenancy.jwt_public_key`, `tenancy.jwt_algorithm` (default `RS256`), and `tenancy.jwt_tenant_claim`. -Configured via `tenancy.resolver` in `application.yaml`. Add new resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. +Configured via `tenancy.resolver: header | subdomain | jwt` in `application.yaml`. Add new resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. ## TenantService The intended entry point for tenant lifecycle operations: From 825bfcb0ad792e9054cfb59218677e41ee2aa980 Mon Sep 17 00:00:00 2001 From: vedavith Date: Tue, 9 Jun 2026 08:09:16 +0530 Subject: [PATCH 2/2] Update readme to document JwtTenantResolver Added jwt row to the Tenant Resolution table with all three config keys. Updated the quick-start config block to show JWT options commented out. Removed JWT from the roadmap as it is now implemented. --- readme.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 088a0ee..6e8e315 100644 --- a/readme.md +++ b/readme.md @@ -47,8 +47,12 @@ composer require entity-forge/entity-forge tenancy: enabled: true strategy: shared # or: database - resolver: header # or: subdomain + resolver: header # or: subdomain | jwt header_key: X-Tenant-ID + # resolver: jwt + # jwt_public_key: /path/to/public.pem + # jwt_algorithm: RS256 + # jwt_tenant_claim: tenant_id database: driver: mysql @@ -182,7 +186,8 @@ Configure via `tenancy.resolver` in `application.yaml`: | Resolver | Config keys | How it works | |---|---|---| | `header` | `header_key` (default: `X-Tenant-ID`) | Reads the named HTTP header from the request context | -| `subdomain` | `subdomain_depth`, `subdomain_min_parts` | Extracts the leading subdomain from the `host` context key (`acme.example.com` → `acme`). Set `subdomain_min_parts: 2` to support two-part hosts like `acme.io` | +| `subdomain` | `subdomain_depth`, `subdomain_min_parts` (default: 3) | Extracts the leading subdomain from the `host` context key (`acme.example.com` → `acme`). Set `subdomain_min_parts: 2` for two-part hosts like `acme.io` | +| `jwt` | `jwt_public_key`, `jwt_algorithm` (default: `RS256`), `jwt_tenant_claim` (default: `tenant_id`) | Decodes and verifies a Bearer JWT from the `Authorization` header, then extracts the named claim | Add custom resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. @@ -414,7 +419,7 @@ vendor/bin/phpunit tests/Path/To/SomeTest.php # single file ## Roadmap -- [ ] JWT / session-based tenant resolver +- [ ] Session-based tenant resolver - [ ] Artisan-style scaffolding for middleware and controllers - [ ] Official Packagist release