@bluealba/platform-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/index.js +278 -15
  2. package/docs/404.mdx +5 -0
  3. package/docs/architecture/api-explorer.mdx +478 -0
  4. package/docs/architecture/architecture-diagrams.mdx +12 -0
  5. package/docs/architecture/authentication-system.mdx +903 -0
  6. package/docs/architecture/authorization-system.mdx +886 -0
  7. package/docs/architecture/bootstrap.mdx +1442 -0
  8. package/docs/architecture/gateway-architecture.mdx +845 -0
  9. package/docs/architecture/multi-tenancy.mdx +1150 -0
  10. package/docs/architecture/overview.mdx +776 -0
  11. package/docs/architecture/scheduler.mdx +818 -0
  12. package/docs/architecture/shell.mdx +885 -0
  13. package/docs/architecture/ui-extension-points.mdx +781 -0
  14. package/docs/architecture/user-states.mdx +794 -0
  15. package/docs/development/overview.mdx +21 -0
  16. package/docs/development/workflow.mdx +914 -0
  17. package/docs/getting-started/core-concepts.mdx +892 -0
  18. package/docs/getting-started/installation.mdx +780 -0
  19. package/docs/getting-started/overview.mdx +83 -0
  20. package/docs/getting-started/quick-start.mdx +940 -0
  21. package/docs/guides/adding-documentation-sites.mdx +1367 -0
  22. package/docs/guides/creating-services.mdx +1736 -0
  23. package/docs/guides/creating-ui-modules.mdx +1860 -0
  24. package/docs/guides/identity-providers.mdx +1007 -0
  25. package/docs/guides/mermaid-diagrams.mdx +212 -0
  26. package/docs/guides/using-feature-flags.mdx +1059 -0
  27. package/docs/guides/working-with-rooms.mdx +566 -0
  28. package/docs/index.mdx +57 -0
  29. package/docs/platform-cli/commands.mdx +604 -0
  30. package/docs/platform-cli/overview.mdx +195 -0
  31. package/package.json +5 -2
  32. package/skills/ba-platform/platform-cli.skill.md +26 -0
  33. package/skills/ba-platform/platform.skill.md +35 -0
  34. package/templates/application-monorepo-template/gitignore +95 -0
  35. package/templates/bootstrap-service-template/Dockerfile.development +1 -1
  36. package/templates/bootstrap-service-template/gitignore +57 -0
  37. package/templates/bootstrap-service-template/package.json +1 -1
  38. package/templates/bootstrap-service-template/src/main.ts +6 -16
  39. package/templates/customization-ui-module-template/Dockerfile.development +1 -1
  40. package/templates/customization-ui-module-template/gitignore +73 -0
  41. package/templates/nestjs-service-module-template/Dockerfile.development +1 -1
  42. package/templates/nestjs-service-module-template/gitignore +56 -0
  43. package/templates/platform-init-template/{{platformName}}-core/gitignore +97 -0
  44. package/templates/platform-init-template/{{platformName}}-core/local/.env.example +1 -1
  45. package/templates/platform-init-template/{{platformName}}-core/local/platform-docker-compose.yml +1 -1
  46. package/templates/platform-init-template/{{platformName}}-core/local/{{platformName}}-core-docker-compose.yml +0 -1
  47. package/templates/react-ui-module-template/Dockerfile +1 -1
  48. package/templates/react-ui-module-template/Dockerfile.development +1 -3
  49. package/templates/react-ui-module-template/caddy/Caddyfile +1 -1
  50. package/templates/react-ui-module-template/gitignore +72 -0
  51. package/templates/react-ui-module-template/Dockerfile_nginx +0 -11
  52. package/templates/react-ui-module-template/nginx/default.conf +0 -23
@@ -0,0 +1,1150 @@
1
+ ---
2
+ title: Multi-Tenancy Architecture
3
+ description: Deep dive into the Blue Alba Platform multi-tenancy architecture - tenant isolation, context resolution, URL patterns, and data segregation
4
+ ---
5
+
6
+ import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
7
+
8
+ The Blue Alba Platform is designed with **multi-tenancy** as a core architectural principle, enabling a single platform instance to serve multiple customers or organizations with complete data and configuration isolation.
9
+
10
+ ## Multi-Tenancy Overview
11
+
12
+ Multi-tenancy allows the platform to:
13
+
14
+ <CardGrid stagger>
15
+ <Card title="Serve Multiple Customers" icon="codeberg">
16
+ Run multiple isolated customer instances on shared infrastructure
17
+ </Card>
18
+
19
+ <Card title="Data Isolation" icon="seti:lock">
20
+ Complete segregation of tenant data at database and application levels
21
+ </Card>
22
+
23
+ <Card title="Custom Configuration" icon="setting">
24
+ Per-tenant settings, branding, and feature flags
25
+ </Card>
26
+
27
+ <Card title="Cost Efficiency" icon="rocket">
28
+ Shared infrastructure reduces operational costs
29
+ </Card>
30
+
31
+ <Card title="Simplified Operations" icon="seti:config">
32
+ Deploy once, serve many customers
33
+ </Card>
34
+
35
+ <Card title="Scalability" icon="star">
36
+ Add new tenants without infrastructure changes
37
+ </Card>
38
+ </CardGrid>
39
+
40
+ ---
41
+
42
+ ## Tenant Entity
43
+
44
+ ### Tenant Structure
45
+
46
+ ```typescript
47
+ interface Tenant {
48
+ id: string; // Unique identifier (UUID)
49
+ code: string; // Short code (e.g., "acme", "contoso")
50
+ name: string; // Display name (e.g., "ACME Corporation")
51
+ status: TenantStatus; // ACTIVE | SUSPENDED | DELETED
52
+
53
+ // Configuration
54
+ config?: {
55
+ mode: 'SINGLE_TENANT' | 'MULTI_TENANT';
56
+ clientSideMode?: 'SUBDOMAIN' | 'PATH' | 'COOKIE' | 'SESSION_STORAGE' | 'QUERY_PARAM';
57
+ features?: Record<string, boolean>;
58
+ settings?: Record<string, any>;
59
+ };
60
+
61
+ // Branding
62
+ branding?: {
63
+ logo?: string;
64
+ favicon?: string;
65
+ primaryColor?: string;
66
+ secondaryColor?: string;
67
+ theme?: 'light' | 'dark';
68
+ };
69
+
70
+ // Subscription
71
+ plan?: string;
72
+ subscriptionStartDate?: Date;
73
+ subscriptionEndDate?: Date;
74
+ maxUsers?: number;
75
+
76
+ // Audit
77
+ createdAt: Date;
78
+ createdBy: string;
79
+ updatedAt: Date;
80
+ updatedBy: string;
81
+ }
82
+
83
+ type TenantStatus = 'ACTIVE' | 'SUSPENDED' | 'DELETED';
84
+ ```
85
+
86
+ **Example**:
87
+
88
+ ```json
89
+ {
90
+ "id": "550e8400-e29b-41d4-a716-446655440000",
91
+ "code": "acme",
92
+ "name": "ACME Corporation",
93
+ "status": "ACTIVE",
94
+ "config": {
95
+ "mode": "MULTI_TENANT",
96
+ "clientSideMode": "SUBDOMAIN",
97
+ "features": {
98
+ "habits": true,
99
+ "scheduler": true,
100
+ "reports": false
101
+ }
102
+ },
103
+ "branding": {
104
+ "logo": "https://cdn.acme.com/logo.png",
105
+ "primaryColor": "#0066cc",
106
+ "secondaryColor": "#ff6600"
107
+ },
108
+ "plan": "enterprise",
109
+ "maxUsers": 500
110
+ }
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Tenant Modes
116
+
117
+ The platform supports two tenant modes:
118
+
119
+ <Tabs>
120
+ <TabItem label="SINGLE_TENANT">
121
+ **Single Tenant Mode**
122
+
123
+ - Platform serves a single organization
124
+ - No tenant resolution needed
125
+ - Simpler deployment and configuration
126
+ - Tenant context is fixed
127
+
128
+ ```typescript
129
+ const TENANT_CONFIG = {
130
+ mode: 'SINGLE_TENANT',
131
+ defaultTenantCode: 'acme'
132
+ };
133
+ ```
134
+
135
+ **Use Case**: Dedicated deployment for a single large customer
136
+ </TabItem>
137
+
138
+ <TabItem label="MULTI_TENANT">
139
+ **Multi-Tenant Mode**
140
+
141
+ - Platform serves multiple organizations
142
+ - Tenant must be resolved from request
143
+ - URL patterns, cookies, or headers identify tenant
144
+ - Full tenant isolation enforced
145
+
146
+ ```typescript
147
+ const TENANT_CONFIG = {
148
+ mode: 'MULTI_TENANT',
149
+ clientSideMode: 'SUBDOMAIN' // or PATH, COOKIE, etc.
150
+ };
151
+ ```
152
+
153
+ **Use Case**: SaaS deployment serving many customers
154
+ </TabItem>
155
+ </Tabs>
156
+
157
+ ---
158
+
159
+ ## Tenant Context Resolution
160
+
161
+ In multi-tenant mode, the gateway must determine which tenant is making the request.
162
+
163
+ ### Resolution Strategies
164
+
165
+ The platform supports multiple strategies for tenant resolution:
166
+
167
+ <Tabs>
168
+ <TabItem label="Subdomain">
169
+ **Subdomain-Based Resolution**
170
+
171
+ Extract tenant from subdomain:
172
+
173
+ ```
174
+ https://acme.platform.com → tenant: "acme"
175
+ https://contoso.platform.com → tenant: "contoso"
176
+ https://platform.com → no tenant (login page)
177
+ ```
178
+
179
+ **Implementation**:
180
+
181
+ ```typescript
182
+ class SubdomainPatternMatcher implements TenantPatternMatcher {
183
+ async match(req: FastifyRequest): Promise<TenantContext | null> {
184
+ const host = req.headers.host; // "acme.platform.com"
185
+ const parts = host.split('.');
186
+
187
+ if (parts.length < 3) {
188
+ return null; // Not a subdomain
189
+ }
190
+
191
+ const subdomain = parts[0]; // "acme"
192
+
193
+ return {
194
+ type: 'SUBDOMAIN',
195
+ value: subdomain
196
+ };
197
+ }
198
+ }
199
+ ```
200
+
201
+ **Configuration**:
202
+
203
+ ```typescript
204
+ // Tenant URL pattern
205
+ {
206
+ tenantId: 'acme-tenant-id',
207
+ type: 'SUBDOMAIN',
208
+ pattern: 'acme.platform.com',
209
+ priority: 10
210
+ }
211
+ ```
212
+ </TabItem>
213
+
214
+ <TabItem label="Path">
215
+ **Path-Based Resolution**
216
+
217
+ Extract tenant from URL path:
218
+
219
+ ```
220
+ https://platform.com/tenants/acme/app → tenant: "acme"
221
+ https://platform.com/t/contoso/dashboard → tenant: "contoso"
222
+ https://platform.com/ → no tenant
223
+ ```
224
+
225
+ **Implementation**:
226
+
227
+ ```typescript
228
+ class PathPatternMatcher implements TenantPatternMatcher {
229
+ async match(req: FastifyRequest): Promise<TenantContext | null> {
230
+ const url = req.url; // "/tenants/acme/app"
231
+
232
+ // Match pattern: /tenants/:tenantCode/*
233
+ const match = url.match(/^\/tenants\/([^\/]+)/);
234
+
235
+ if (!match) {
236
+ return null;
237
+ }
238
+
239
+ const tenantCode = match[1]; // "acme"
240
+
241
+ return {
242
+ type: 'PATH',
243
+ value: tenantCode
244
+ };
245
+ }
246
+ }
247
+ ```
248
+
249
+ **Configuration**:
250
+
251
+ ```typescript
252
+ {
253
+ tenantId: 'acme-tenant-id',
254
+ type: 'PATH',
255
+ pattern: '/tenants/acme/*',
256
+ priority: 10
257
+ }
258
+ ```
259
+ </TabItem>
260
+
261
+ <TabItem label="Custom Domain">
262
+ **Custom Domain Resolution**
263
+
264
+ Map entire domains to tenants:
265
+
266
+ ```
267
+ https://acme.com → tenant: "acme"
268
+ https://www.acme.com → tenant: "acme"
269
+ https://contoso.com → tenant: "contoso"
270
+ ```
271
+
272
+ **Implementation**:
273
+
274
+ ```typescript
275
+ class CustomDomainPatternMatcher implements TenantPatternMatcher {
276
+ async match(req: FastifyRequest): Promise<TenantContext | null> {
277
+ const host = req.headers.host; // "acme.com"
278
+
279
+ // Normalize (remove www)
280
+ const domain = host.replace(/^www\./, '');
281
+
282
+ return {
283
+ type: 'CUSTOM_DOMAIN',
284
+ value: domain
285
+ };
286
+ }
287
+ }
288
+ ```
289
+
290
+ **Configuration**:
291
+
292
+ ```typescript
293
+ {
294
+ tenantId: 'acme-tenant-id',
295
+ type: 'CUSTOM_DOMAIN',
296
+ pattern: 'acme.com',
297
+ priority: 20 // Higher priority than subdomain
298
+ }
299
+ ```
300
+ </TabItem>
301
+
302
+ <TabItem label="Header">
303
+ **Header-Based Resolution**
304
+
305
+ Read tenant from custom header:
306
+
307
+ ```http
308
+ GET /api/data
309
+ Host: platform.com
310
+ x-tenant-id: acme
311
+ ```
312
+
313
+ **Implementation**:
314
+
315
+ ```typescript
316
+ class HeaderPatternMatcher implements TenantPatternMatcher {
317
+ async match(req: FastifyRequest): Promise<TenantContext | null> {
318
+ const tenantCode = req.headers['x-tenant-id'] as string;
319
+
320
+ if (!tenantCode) {
321
+ return null;
322
+ }
323
+
324
+ return {
325
+ type: 'HEADER',
326
+ value: tenantCode
327
+ };
328
+ }
329
+ }
330
+ ```
331
+
332
+ **Use Case**: Development, testing, API clients
333
+ </TabItem>
334
+
335
+ <TabItem label="Cookie">
336
+ **Cookie-Based Resolution**
337
+
338
+ Read tenant from cookie:
339
+
340
+ ```http
341
+ GET /api/data
342
+ Cookie: tenant=acme
343
+ ```
344
+
345
+ **Implementation**:
346
+
347
+ ```typescript
348
+ class CookiePatternMatcher implements TenantPatternMatcher {
349
+ async match(req: FastifyRequest): Promise<TenantContext | null> {
350
+ const tenantCode = req.cookies['tenant'];
351
+
352
+ if (!tenantCode) {
353
+ return null;
354
+ }
355
+
356
+ return {
357
+ type: 'COOKIE',
358
+ value: tenantCode
359
+ };
360
+ }
361
+ }
362
+ ```
363
+
364
+ **Use Case**: After tenant selection by user
365
+ </TabItem>
366
+
367
+ <TabItem label="Query Parameter">
368
+ **Query Parameter Resolution**
369
+
370
+ Read tenant from query string:
371
+
372
+ ```
373
+ https://platform.com?tenant=acme
374
+ https://platform.com/app?tenant=contoso
375
+ ```
376
+
377
+ **Implementation**:
378
+
379
+ ```typescript
380
+ class QueryParamPatternMatcher implements TenantPatternMatcher {
381
+ async match(req: FastifyRequest): Promise<TenantContext | null> {
382
+ const tenantCode = req.query.tenant as string;
383
+
384
+ if (!tenantCode) {
385
+ return null;
386
+ }
387
+
388
+ return {
389
+ type: 'QUERY_PARAM',
390
+ value: tenantCode
391
+ };
392
+ }
393
+ }
394
+ ```
395
+
396
+ **Use Case**: Development, testing, explicit tenant switching
397
+ </TabItem>
398
+ </Tabs>
399
+
400
+ ### Resolution Priority
401
+
402
+ Multiple patterns can be configured with priority levels. Higher priority patterns are checked first:
403
+
404
+ ```typescript
405
+ const urlPatterns = [
406
+ { type: 'CUSTOM_DOMAIN', pattern: 'acme.com', priority: 100 },
407
+ { type: 'SUBDOMAIN', pattern: 'acme.platform.com', priority: 50 },
408
+ { type: 'PATH', pattern: '/tenants/acme/*', priority: 30 },
409
+ { type: 'HEADER', pattern: '*', priority: 20 },
410
+ { type: 'COOKIE', pattern: '*', priority: 10 },
411
+ { type: 'QUERY_PARAM', pattern: '*', priority: 5 }
412
+ ];
413
+
414
+ // Sorted by priority (highest first)
415
+ // Checked in order until a match is found
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Tenant Resolution Flow
421
+
422
+ ```
423
+ ┌──────────┐
424
+ │ Request │
425
+ └────┬─────┘
426
+
427
+
428
+ ┌─────────────────────────────────────┐
429
+ │ 1. Extract Host/Path/Headers │
430
+ │ host: "acme.platform.com" │
431
+ │ path: "/api/orders" │
432
+ │ headers: { x-tenant: "acme" } │
433
+ └────┬────────────────────────────────┘
434
+
435
+
436
+ ┌─────────────────────────────────────┐
437
+ │ 2. Get Tenant URL Patterns │
438
+ │ from database, sorted by │
439
+ │ priority │
440
+ └────┬────────────────────────────────┘
441
+
442
+
443
+ ┌─────────────────────────────────────┐
444
+ │ 3. Try Each Pattern Matcher │
445
+ │ - CustomDomainMatcher │
446
+ │ - SubdomainMatcher │
447
+ │ - PathMatcher │
448
+ │ - HeaderMatcher │
449
+ │ - CookieMatcher │
450
+ │ - QueryParamMatcher │
451
+ └────┬────────────────────────────────┘
452
+
453
+
454
+ ┌─────────────────────────────────────┐
455
+ │ 4. First Match Found? │
456
+ │ → TenantContext { type, value } │
457
+ └────┬────────────────────────────────┘
458
+
459
+
460
+ ┌─────────────────────────────────────┐
461
+ │ 5. Query Tenant by Context │
462
+ │ SELECT * FROM tenants │
463
+ │ WHERE code = 'acme' │
464
+ └────┬────────────────────────────────┘
465
+
466
+
467
+ ┌─────────────────────────────────────┐
468
+ │ 6. Validate Tenant │
469
+ │ - Status = ACTIVE? │
470
+ │ - User has access? │
471
+ │ - Subscription valid? │
472
+ └────┬────────────────────────────────┘
473
+
474
+
475
+ ┌─────────────────────────────────────┐
476
+ │ 7. Store in Platform Context │
477
+ │ paeContext.setTenant(tenant) │
478
+ └─────────────────────────────────────┘
479
+ ```
480
+
481
+ **Implementation**:
482
+
483
+ ```typescript
484
+ // apps/pae-nestjs-gateway-service/src/tenants/tenant-resolver.service.ts
485
+
486
+ @Injectable()
487
+ export class TenantResolverService {
488
+ constructor(
489
+ @Inject('TENANT_STRATEGIES')
490
+ private readonly tenantStrategies: Record<string, TenantStrategy>,
491
+ private readonly tenantService: TenantsService,
492
+ private readonly parametersService: ParametersService
493
+ ) {}
494
+
495
+ async getTenantFromRequest(
496
+ request: FastifyRequest,
497
+ response: FastifyReply,
498
+ allowedTenants: Tenant[]
499
+ ): Promise<Tenant | null> {
500
+ // Get active strategies based on configuration
501
+ const strategies = await this.resolveStrategies(this.tenantStrategies);
502
+
503
+ // Try each strategy in order
504
+ for (const strategy of strategies) {
505
+ const tenantContext = await strategy.getTenantFromRequest(request);
506
+
507
+ if (tenantContext) {
508
+ // Found tenant context, look up tenant
509
+ const tenant = await this.tenantService.findTenantByContext(tenantContext);
510
+
511
+ if (tenant && allowedTenants.some(t => t.id === tenant.id)) {
512
+ // Valid tenant that user has access to
513
+ return tenant;
514
+ }
515
+ }
516
+ }
517
+
518
+ return null; // No tenant found
519
+ }
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Tenant Isolation
526
+
527
+ ### Database-Level Isolation
528
+
529
+ All data tables include a `tenant_id` column for isolation:
530
+
531
+ ```sql
532
+ -- Example: Orders table
533
+ CREATE TABLE orders (
534
+ id UUID PRIMARY KEY,
535
+ tenant_id UUID NOT NULL REFERENCES tenants(id),
536
+ customer_id UUID NOT NULL,
537
+ total DECIMAL(10,2) NOT NULL,
538
+ status VARCHAR(50) NOT NULL,
539
+ created_at TIMESTAMP DEFAULT NOW(),
540
+ -- ... other columns
541
+ CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
542
+ );
543
+
544
+ -- Index for fast tenant filtering
545
+ CREATE INDEX idx_orders_tenant ON orders(tenant_id);
546
+
547
+ -- All queries MUST filter by tenant_id
548
+ SELECT * FROM orders WHERE tenant_id = :tenantId;
549
+ ```
550
+
551
+ <Aside type="caution" title="Critical: Always Filter by Tenant">
552
+ **Every database query MUST include a `tenant_id` filter.** Failing to do so can expose data across tenant boundaries, which is a serious security breach. Use automated tests to verify tenant isolation.
553
+ </Aside>
554
+
555
+ ### Application-Level Isolation
556
+
557
+ Services automatically enforce tenant filtering:
558
+
559
+ <Tabs>
560
+ <TabItem label="Automatic (Recommended)">
561
+ ```typescript
562
+ // Use platform context decorator
563
+ import { PlatformContext } from '@bluealba/pae-service-nestjs-sdk';
564
+
565
+ @Injectable()
566
+ export class OrdersService {
567
+ async findAll(@PlatformContext() ctx: PlatformContext) {
568
+ // Automatically filters by ctx.tenantId
569
+ return this.ordersRepository.find({
570
+ where: { tenantId: ctx.tenantId }
571
+ });
572
+ }
573
+
574
+ async findOne(id: string, @PlatformContext() ctx: PlatformContext) {
575
+ // Ensure order belongs to tenant
576
+ const order = await this.ordersRepository.findOne({
577
+ where: { id, tenantId: ctx.tenantId }
578
+ });
579
+
580
+ if (!order) {
581
+ throw new NotFoundException('Order not found');
582
+ }
583
+
584
+ return order;
585
+ }
586
+ }
587
+ ```
588
+ </TabItem>
589
+
590
+ <TabItem label="Manual (Advanced)">
591
+ ```typescript
592
+ // Explicitly pass tenant ID
593
+ @Injectable()
594
+ export class OrdersService {
595
+ async findAll(tenantId: string) {
596
+ return this.ordersRepository.find({
597
+ where: { tenantId }
598
+ });
599
+ }
600
+
601
+ async findOne(id: string, tenantId: string) {
602
+ const order = await this.ordersRepository.findOne({
603
+ where: { id, tenantId }
604
+ });
605
+
606
+ if (!order) {
607
+ throw new NotFoundException('Order not found');
608
+ }
609
+
610
+ return order;
611
+ }
612
+ }
613
+ ```
614
+ </TabItem>
615
+
616
+ <TabItem label="Query Builder">
617
+ ```typescript
618
+ // Using query builder (Knex)
619
+ @Injectable()
620
+ export class OrdersService {
621
+ async findAll(tenantId: string) {
622
+ return this.knex('orders')
623
+ .where('tenant_id', tenantId)
624
+ .select('*');
625
+ }
626
+
627
+ async create(data: CreateOrderDto, tenantId: string) {
628
+ const [order] = await this.knex('orders')
629
+ .insert({
630
+ ...data,
631
+ tenant_id: tenantId, // Always include tenant_id
632
+ created_at: new Date()
633
+ })
634
+ .returning('*');
635
+
636
+ return order;
637
+ }
638
+ }
639
+ ```
640
+ </TabItem>
641
+ </Tabs>
642
+
643
+ ### Tenant Context Propagation
644
+
645
+ The gateway adds tenant context to forwarded requests:
646
+
647
+ ```http
648
+ POST /api/orders HTTP/1.1
649
+ Host: orders-service:4001
650
+ x-pae-user-id: john@acme.com
651
+ x-pae-user: john@acme.com
652
+ x-pae-tenant: eyJ0ZW5hbnRJZCI6ImFjbWUiLCAibmFtZSI6IkFDTUUgQ29ycCJ9
653
+ x-pae-forwarded-tenant-config: eyJtb2RlIjoibXVsdGktdGVuYW50In0=
654
+
655
+ {
656
+ "customerId": "cust-123",
657
+ "total": 99.99
658
+ }
659
+ ```
660
+
661
+ Services extract tenant context:
662
+
663
+ ```typescript
664
+ // NestJS interceptor extracts and decodes headers
665
+ @Injectable()
666
+ export class TenantContextInterceptor implements NestInterceptor {
667
+ intercept(context: ExecutionContext, next: CallHandler) {
668
+ const request = context.switchToHttp().getRequest();
669
+
670
+ // Extract and decode tenant from header
671
+ const tenantHeader = request.headers['x-pae-tenant'];
672
+ const tenant = JSON.parse(base64Decode(tenantHeader));
673
+
674
+ // Store in request
675
+ request.tenant = tenant;
676
+ request.tenantId = tenant.id;
677
+
678
+ return next.handle();
679
+ }
680
+ }
681
+ ```
682
+
683
+ ---
684
+
685
+ ## Tenant-Scoped Authorization Rules
686
+
687
+ Authorization rules (the assignments of operations, roles, or applications to users and groups) are scoped to tenants via the `tenant_id` column in the `authz_rules` table.
688
+
689
+ ### How It Works
690
+
691
+ - **Tenant-scoped rules** (`tenant_id` is set): Apply only within the specified tenant. All rules created from the Admin UI are automatically associated with the current tenant.
692
+ - **Global rules** (`tenant_id` is `NULL`): Apply across all tenants. These are typically rules created before tenant scoping was introduced.
693
+
694
+ When the authorization service evaluates permissions for a user, it includes:
695
+ 1. Rules that match the current tenant (`tenant_id = currentTenantId`)
696
+ 2. Global rules (`tenant_id IS NULL`)
697
+
698
+ ### Admin UI Behavior
699
+
700
+ In multi-tenant mode, the Admin UI provides visual cues for tenant-scoped rules:
701
+
702
+ - Rules without a `tenant_id` are displayed with a **"Global"** badge to indicate they apply across all tenants.
703
+ - When **creating** new assignments, the current tenant is automatically associated.
704
+ - When **removing** a global rule, a confirmation dialog warns that the deletion will affect all tenants.
705
+
706
+ <Aside type="caution" title="Global Rule Deletion">
707
+ Deleting a global rule removes the assignment across **all** tenants. The Admin UI displays a confirmation warning before proceeding. Consider whether a tenant-scoped override is more appropriate.
708
+ </Aside>
709
+
710
+ ---
711
+
712
+ ## Tenant-Scoped Applications
713
+
714
+ Applications can be restricted to specific tenants.
715
+
716
+ ### Tenant Application Restrictions
717
+
718
+ ```typescript
719
+ interface TenantApplicationRestriction {
720
+ id: string;
721
+ tenantId: string;
722
+ applicationId: string;
723
+ isRestricted: boolean; // If true, app is BLOCKED for tenant
724
+ createdAt: Date;
725
+ createdBy: string;
726
+ }
727
+ ```
728
+
729
+ **Database**:
730
+
731
+ ```sql
732
+ CREATE TABLE tenant_application_restrictions (
733
+ id UUID PRIMARY KEY,
734
+ tenant_id UUID NOT NULL REFERENCES tenants(id),
735
+ application_id INTEGER NOT NULL REFERENCES applications(id),
736
+ is_restricted BOOLEAN DEFAULT FALSE,
737
+ created_at TIMESTAMP DEFAULT NOW(),
738
+ created_by VARCHAR(255) NOT NULL,
739
+ UNIQUE(tenant_id, application_id)
740
+ );
741
+ ```
742
+
743
+ **Usage**:
744
+
745
+ ```typescript
746
+ // Check if tenant has access to application
747
+ const hasAccess = await this.tenantApplicationRestrictionsService
748
+ .hasAccessToApplication(tenantId, applicationId);
749
+
750
+ if (!hasAccess) {
751
+ throw new ForbiddenException('Application not available for this tenant');
752
+ }
753
+
754
+ // Filter catalog by tenant
755
+ const catalog = await this.catalogService.getCatalog(tenantId);
756
+ // Returns only applications tenant has access to
757
+ ```
758
+
759
+ ---
760
+
761
+ ## Tenant Route Restrictions
762
+
763
+ Specific routes can be restricted per tenant.
764
+
765
+ ### Route Restriction Configuration
766
+
767
+ ```typescript
768
+ interface TenantModuleRestriction {
769
+ id: string;
770
+ tenantId: string;
771
+ moduleId: string;
772
+ routePath: string;
773
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | '*';
774
+ isBlocked: boolean;
775
+ reason?: string;
776
+ createdAt: Date;
777
+ createdBy: string;
778
+ }
779
+ ```
780
+
781
+ **Example**:
782
+
783
+ ```typescript
784
+ // Block DELETE operations for tenant "acme"
785
+ {
786
+ tenantId: 'acme-tenant-id',
787
+ moduleId: 'orders-service',
788
+ routePath: '/api/orders/:id',
789
+ method: 'DELETE',
790
+ isBlocked: true,
791
+ reason: 'Tenant plan does not include delete capability'
792
+ }
793
+
794
+ // Block entire analytics module
795
+ {
796
+ tenantId: 'acme-tenant-id',
797
+ moduleId: 'analytics-service',
798
+ routePath: '*',
799
+ method: '*',
800
+ isBlocked: true,
801
+ reason: 'Analytics not included in tenant plan'
802
+ }
803
+ ```
804
+
805
+ **Enforcement**:
806
+
807
+ ```typescript
808
+ // Gateway checks route restrictions
809
+ const tenant = paeContext.getTenant();
810
+ const targetModule = paeContext.getTargetModule();
811
+
812
+ const isBlocked = await this.tenantRouteRestrictionsService
813
+ .isRouteBlocked(tenant.id, targetModule.id, req.url, req.method);
814
+
815
+ if (isBlocked) {
816
+ throw new ForbiddenException('This route is not available for your tenant');
817
+ }
818
+ ```
819
+
820
+ ---
821
+
822
+ ## Tenant Selection Flow
823
+
824
+ For users with access to multiple tenants:
825
+
826
+ ```
827
+ ┌──────────┐
828
+ │ User │
829
+ │ Logs In │
830
+ └────┬─────┘
831
+
832
+
833
+ ┌─────────────────────────────────────┐
834
+ │ 1. Authenticate User │
835
+ │ Validate credentials │
836
+ └────┬────────────────────────────────┘
837
+
838
+
839
+ ┌─────────────────────────────────────┐
840
+ │ 2. Query User's Allowed Tenants │
841
+ │ SELECT tenants FROM │
842
+ │ user_tenant_assignments │
843
+ │ WHERE user_id = :userId │
844
+ └────┬────────────────────────────────┘
845
+
846
+
847
+ ┌─────────────────────────────────────┐
848
+ │ 3. How Many Tenants? │
849
+ ├─────────────┬───────────────────────┤
850
+ │ Single │ Multiple │
851
+ └─────┬───────┴───────────┬───────────┘
852
+ │ │
853
+ ▼ ▼
854
+ ┌───────────┐ ┌──────────────────┐
855
+ │ Auto │ │ Show Tenant │
856
+ │ Select │ │ Selection Screen │
857
+ └───┬───────┘ └────┬─────────────┘
858
+ │ │
859
+ │ ▼
860
+ │ ┌──────────────────┐
861
+ │ │ User Selects │
862
+ │ │ Tenant │
863
+ │ └────┬─────────────┘
864
+ │ │
865
+ └────────┬────────┘
866
+
867
+ ┌─────────────────────────────────┐
868
+ │ 4. Set Tenant Context │
869
+ │ - Cookie │
870
+ │ - Session storage │
871
+ │ - Redirect to subdomain │
872
+ └─────┬───────────────────────────┘
873
+
874
+
875
+ ┌─────────────────────────────────┐
876
+ │ 5. User Accesses Application │
877
+ └─────────────────────────────────┘
878
+ ```
879
+
880
+ **Frontend Implementation**:
881
+
882
+ ```typescript
883
+ // React component for tenant selection
884
+ function TenantSelectionPage() {
885
+ const [tenants, setTenants] = useState<Tenant[]>([]);
886
+ const navigate = useNavigate();
887
+
888
+ useEffect(() => {
889
+ // Fetch user's allowed tenants
890
+ fetch('/api/me/tenants')
891
+ .then(res => res.json())
892
+ .then(data => setTenants(data));
893
+ }, []);
894
+
895
+ const selectTenant = async (tenant: Tenant) => {
896
+ // Set tenant cookie
897
+ document.cookie = `tenant=${tenant.code}; path=/; secure; samesite=lax`;
898
+
899
+ // Or redirect to tenant subdomain
900
+ window.location.href = `https://${tenant.code}.platform.com`;
901
+ };
902
+
903
+ return (
904
+ <div>
905
+ <h1>Select Organization</h1>
906
+ {tenants.map(tenant => (
907
+ <button key={tenant.id} onClick={() => selectTenant(tenant)}>
908
+ {tenant.name}
909
+ </button>
910
+ ))}
911
+ </div>
912
+ );
913
+ }
914
+ ```
915
+
916
+ ---
917
+
918
+ ## Tenant Provisioning
919
+
920
+ ### Creating a New Tenant
921
+
922
+ ```typescript
923
+ @Post('tenants')
924
+ async createTenant(
925
+ @Body() dto: CreateTenantDto,
926
+ @User() user: AuthUserWithApplications
927
+ ) {
928
+ // 1. Create tenant record
929
+ const tenant = await this.tenantsService.create({
930
+ code: dto.code,
931
+ name: dto.name,
932
+ status: 'ACTIVE',
933
+ config: {
934
+ mode: 'MULTI_TENANT',
935
+ clientSideMode: 'SUBDOMAIN'
936
+ },
937
+ createdBy: user.username
938
+ });
939
+
940
+ // 2. Create URL patterns
941
+ await this.tenantUrlPatternsService.create({
942
+ tenantId: tenant.id,
943
+ type: 'SUBDOMAIN',
944
+ pattern: `${dto.code}.platform.com`,
945
+ priority: 50
946
+ });
947
+
948
+ // 3. Assign default applications
949
+ const defaultApps = await this.applicationsService.findDefaultApps();
950
+ for (const app of defaultApps) {
951
+ await this.tenantApplicationRestrictionsService.create({
952
+ tenantId: tenant.id,
953
+ applicationId: app.id,
954
+ isRestricted: false
955
+ });
956
+ }
957
+
958
+ // 4. Create admin user for tenant
959
+ await this.usersService.create({
960
+ email: dto.adminEmail,
961
+ displayName: dto.adminName,
962
+ tenantId: tenant.id,
963
+ roles: ['admin']
964
+ });
965
+
966
+ // 5. Provision IDP (Okta, EntraId, etc.)
967
+ if (dto.provisionIDP) {
968
+ await this.idpProvisioningService.provisionTenant(tenant);
969
+ }
970
+
971
+ return tenant;
972
+ }
973
+ ```
974
+
975
+ ---
976
+
977
+ ## Configuration Management
978
+
979
+ ### Tenant-Specific Configuration
980
+
981
+ ```typescript
982
+ // Get tenant configuration
983
+ const config = await this.tenantsService.getConfig(tenantId);
984
+
985
+ // Check feature flags
986
+ if (config.features?.habits === true) {
987
+ // Habits feature enabled for this tenant
988
+ }
989
+
990
+ // Get custom settings
991
+ const maxFileSize = config.settings?.maxFileUploadSize || 10 * 1024 * 1024;
992
+ ```
993
+
994
+ ### Configuration Hierarchy
995
+
996
+ ```
997
+ 1. Global Configuration (platform-wide defaults)
998
+
999
+ 2. Tenant Configuration (tenant-specific overrides)
1000
+
1001
+ 3. User Configuration (user preferences)
1002
+ ```
1003
+
1004
+ **Example**:
1005
+
1006
+ ```typescript
1007
+ // Get effective configuration value
1008
+ function getConfigValue(key: string, tenantId?: string, userId?: string): any {
1009
+ // 1. Try user config
1010
+ if (userId) {
1011
+ const userConfig = getUserConfig(userId);
1012
+ if (userConfig[key] !== undefined) {
1013
+ return userConfig[key];
1014
+ }
1015
+ }
1016
+
1017
+ // 2. Try tenant config
1018
+ if (tenantId) {
1019
+ const tenantConfig = getTenantConfig(tenantId);
1020
+ if (tenantConfig[key] !== undefined) {
1021
+ return tenantConfig[key];
1022
+ }
1023
+ }
1024
+
1025
+ // 3. Fall back to global config
1026
+ return getGlobalConfig(key);
1027
+ }
1028
+ ```
1029
+
1030
+ ---
1031
+
1032
+ ## Testing Tenant Isolation
1033
+
1034
+ ### Unit Tests
1035
+
1036
+ ```typescript
1037
+ describe('OrdersService', () => {
1038
+ it('should only return orders for specified tenant', async () => {
1039
+ // Arrange
1040
+ const tenant1Id = 'tenant-1';
1041
+ const tenant2Id = 'tenant-2';
1042
+
1043
+ await ordersRepository.create({ id: '1', tenantId: tenant1Id, total: 100 });
1044
+ await ordersRepository.create({ id: '2', tenantId: tenant2Id, total: 200 });
1045
+ await ordersRepository.create({ id: '3', tenantId: tenant1Id, total: 300 });
1046
+
1047
+ // Act
1048
+ const orders = await ordersService.findAll(tenant1Id);
1049
+
1050
+ // Assert
1051
+ expect(orders).toHaveLength(2);
1052
+ expect(orders.every(o => o.tenantId === tenant1Id)).toBe(true);
1053
+ });
1054
+
1055
+ it('should throw error when accessing other tenant data', async () => {
1056
+ // Arrange
1057
+ const tenant1Id = 'tenant-1';
1058
+ const tenant2Id = 'tenant-2';
1059
+
1060
+ const order = await ordersRepository.create({
1061
+ id: '1',
1062
+ tenantId: tenant1Id,
1063
+ total: 100
1064
+ });
1065
+
1066
+ // Act & Assert
1067
+ await expect(
1068
+ ordersService.findOne(order.id, tenant2Id)
1069
+ ).rejects.toThrow('Order not found');
1070
+ });
1071
+ });
1072
+ ```
1073
+
1074
+ ### Integration Tests
1075
+
1076
+ ```typescript
1077
+ describe('Orders API (E2E)', () => {
1078
+ it('should enforce tenant isolation', async () => {
1079
+ // Create two tenants
1080
+ const tenant1 = await createTenant('acme');
1081
+ const tenant2 = await createTenant('contoso');
1082
+
1083
+ // Create user for tenant1
1084
+ const user1Token = await authenticateUser('user1@acme.com', tenant1.id);
1085
+
1086
+ // Create order for tenant1
1087
+ const order = await request(app.getHttpServer())
1088
+ .post('/api/orders')
1089
+ .set('Cookie', `auth_token=${user1Token}`)
1090
+ .set('x-tenant-id', 'acme')
1091
+ .send({ total: 100 })
1092
+ .expect(201);
1093
+
1094
+ // Try to access order from tenant2 context
1095
+ const user2Token = await authenticateUser('user2@contoso.com', tenant2.id);
1096
+
1097
+ await request(app.getHttpServer())
1098
+ .get(`/api/orders/${order.body.id}`)
1099
+ .set('Cookie', `auth_token=${user2Token}`)
1100
+ .set('x-tenant-id', 'contoso')
1101
+ .expect(404); // Should not find order
1102
+ });
1103
+ });
1104
+ ```
1105
+
1106
+ ---
1107
+
1108
+ ## Security Considerations
1109
+
1110
+ <Aside type="caution" title="Multi-Tenancy Security Best Practices">
1111
+
1112
+ **Tenant Isolation**:
1113
+ - ALWAYS filter database queries by `tenant_id`
1114
+ - NEVER trust client-provided tenant identifiers
1115
+ - Validate tenant context on every request
1116
+ - Use foreign key constraints to enforce database-level isolation
1117
+
1118
+ **Data Leakage Prevention**:
1119
+ - Test tenant isolation extensively
1120
+ - Implement automated tests for cross-tenant access
1121
+ - Use database views or row-level security (RLS) for additional protection
1122
+ - Audit logs should include tenant context
1123
+
1124
+ **Tenant Context Validation**:
1125
+ - Verify user has access to requested tenant
1126
+ - Check tenant status (ACTIVE vs SUSPENDED)
1127
+ - Validate subscription and feature access
1128
+ - Handle tenant switching carefully
1129
+
1130
+ **Performance**:
1131
+ - Index `tenant_id` columns for fast filtering
1132
+ - Consider partitioning large tables by tenant
1133
+ - Cache tenant configuration appropriately
1134
+ - Monitor query performance per tenant
1135
+
1136
+ **Compliance**:
1137
+ - Support data residency requirements per tenant
1138
+ - Enable per-tenant data export and deletion
1139
+ - Maintain audit trails per tenant
1140
+ - Support tenant-specific retention policies
1141
+
1142
+ </Aside>
1143
+
1144
+ ---
1145
+
1146
+ ## Next Steps
1147
+
1148
+ - **[Gateway Architecture](/_/docs/architecture/gateway-architecture/)** - How gateway manages tenant context
1149
+ - **[Authentication System](/_/docs/architecture/authentication-system/)** - Tenant-aware authentication
1150
+ - **[Authorization System](/_/docs/architecture/authorization-system/)** - Tenant-scoped authorization