Search

PythonReactSEOPDF
GitHub Skill

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.

Rzyfront☆ 4 Repository stars0 Repository forks

Installation

Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.

View source ↗Guides

Skill instructions

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. See vendix-prisma-scopes skill for model registration rules and complete documentation.

Troubleshooting 403 Forbidden

If you encounter a 403 Forbidden error in a scoped service:

  1. Check Middleware: Ensure DomainResolverMiddleware is applied to the route.
  2. Check Interceptor: Ensure RequestContextInterceptor is active (usually global).
  3. Verify Header: If testing via API, ensure x-store-id is sent or the Host header matches a registered domain.
  4. Context Presence: Use RequestContextService.getContext() to debug if the store is being resolved correctly.
  5. Model Registration: Ensure the model is registered in the scoped service's model arrays (see vendix-prisma-scopes).

Examples

Read the usage examples in the source repository.

View source ↗

Source history

Indexed on
2026-01-20
Index updated
2026-03-03
Branch
main

Index and sync dates describe this directory. View the source repository for code history.

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!