Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,16 @@ 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
├── TenantService.php — onboard/suspend/resume/offboard
├── 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
```

---
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/CLEANUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
7 changes: 4 additions & 3 deletions docs/CONCEPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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

Expand Down
Loading