vendix-multi-tenant-context
Explains the 'Context Bridge' pattern where Middleware resolves the tenant (domain/store), stores it in the Request object, and an Interceptor unifies it with user authentication into AsyncLocalStorage.
安装与使用
请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。
查看来源 ↗使用指南技能说明
Multi-Tenant Context Bridge
Vendix uses a Context Bridge pattern to manage multi-tenancy. This pattern ensures that every request has a verified store_id and organization_id available throughout the execution flow, without relying on passing parameters through every function call.
1. Middleware Resolution
The DomainResolverMiddleware is the first line of defense. It identifies the tenant based on the hostname or a specific header.
// apps/backend/src/common/middleware/domain-resolver.middleware.ts
async use(req: Request, res: Response, next: NextFunction) {
const hostname = this.extractHostname(req);
const x_store_id = req.headers['x-store-id'];
// Priority 1: x-store-id header (development/manual override)
if (x_store_id) {
req['domain_context'] = { store_id: Number(x_store_id) };
return next();
}
// Priority 2: Hostname resolution (production)
const domain = await this.publicDomains.resolveDomain(hostname);
req['domain_context'] = {
store_id: domain.store_id,
organization_id: domain.organization_id,
};
next();
}
2. Request Bridging
Middleware cannot use AsyncLocalStorage directly if it needs to coexist with NestJS Interceptors that also manage context. Instead, it "bridges" the information by attaching it to the Request object.
req["domain_context"] = { store_id, organization_id };
3. Interceptor Unification
The RequestContextInterceptor merges user authentication (from req.user) with the domain context (from req['domain_context']) and initializes the AsyncLocalStorage.
// apps/backend/src/common/interceptors/request-context.interceptor.ts
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const user = req.user;
const domain_context = req['domain_context'];
const contextObj: RequestContext = {
user_id: user?.id,
organization_id: user?.organization_id || domain_context?.organization_id,
store_id: user?.store_id || domain_context?.store_id,
is_super_admin: roles.includes('super_admin'),
// ...
};
return RequestContextService.asyncLocalStorage.run(contextObj, () => {
return next.handle();
});
}
4. Safe Context Service
The RequestContextService provides a static API to access the context. It must never provide static fallbacks or "mock" data if the context is missing, as this could lead to data leakage between tenants.
// apps/backend/src/common/context/request-context.service.ts
export class RequestContextService {
public static asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
static getContext(): RequestContext | undefined {
return this.asyncLocalStorage.getStore();
}
static getStoreId(): number | undefined {
return this.getContext()?.store_id;
}
}
5. Scoped Prisma Usage
The context flows into 4 domain-scoped Prisma services that use Prisma Client Extensions to automatically intercept ALL queries and inject tenant filters:
| Service | Scope Applied | Domain |
|---|---|---|
GlobalPrismaService |
None | Superadmin |
OrganizationPrismaService |
organization_id |
Org admin |
StorePrismaService |
store_id + organization_id |
Store admin |
EcommercePrismaService |
store_id + user_id |
E-commerce |
// Scoping is transparent - no manual filtering needed
constructor(private readonly prisma: StorePrismaService) {}
async findProducts() {
// Extensions auto-inject WHERE store_id = ctx.store_id
return this.prisma.products.findMany();
}
Scoped services are mandatory.
withoutScope()requires explicit user approval. Seevendix-prisma-scopesskill for model registration rules and complete documentation.
Troubleshooting 403 Forbidden
If you encounter a 403 Forbidden error in a scoped service:
-
Check Middleware: Ensure
DomainResolverMiddlewareis applied to the route. -
Check Interceptor: Ensure
RequestContextInterceptoris active (usually global). -
Verify Header: If testing via API, ensure
x-store-idis sent or theHostheader matches a registered domain. -
Context Presence: Use
RequestContextService.getContext()to debug if the store is being resolved correctly. -
Model Registration: Ensure the model is registered in the scoped service's model arrays (see
vendix-prisma-scopes).
使用示例
查看来源仓库中的使用示例与配置说明。
查看来源 ↗来源历史
- 收录时间
- 2026-01-20
- 索引更新时间
- 2026-03-03
- Branch
- main
收录与同步日期属于本站记录。源码修改历史请查看来源仓库。
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!