skills/vendix-prisma-scopes/SKILL.md
Multi-tenant Prisma scoping system for Vendix: BasePrismaService, Global/Organization/Store/Ecommerce scoped services, model registration, scoped unique-operation caveats, manual-scope caveats, and withoutScope rules. Trigger: When working with Prisma scoped services, adding models to scopes, or debugging Forbidden/Unauthorized errors or Prisma WhereUnique/AND errors in database queries.
npx skillsauth add rzyfront/vendix vendix-prisma-scopesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
apps/backend/src/prisma/base/base-prisma.service.ts.apps/backend/src/prisma/services/.apps/backend/src/prisma/prisma.module.ts.| Service | Scope | Use |
| --- | --- | --- |
| GlobalPrismaService | none | super-admin, jobs, cross-tenant/system operations |
| OrganizationPrismaService | organization_id | organization admin domains |
| StorePrismaService | store_id plus org where applicable | store admin/POS/store domains |
| EcommercePrismaService | store_id plus user/customer where applicable | customer ecommerce domains |
PrismaModule exports all four services plus RequestContextService, AccessValidationService, and StoreContextRunner.
Current base uses Prisma 7 with pg.Pool, PrismaPg adapter, and new PrismaClient({ adapter, log: ['error', 'warn'] }).
withoutScope() returns the raw base client. It bypasses all tenant isolation.
Getter access is not enough. A model is scoped only if it is registered in the relevant extension arrays/maps used by that service.
When adding a model:
store_id, organization_id) or relational parent scope.scoped_client only when the extension really handles it.OrganizationPrismaService:
create/createMany.roles includes org roles plus system roles where organization_id = null.scoped_client but are not registered in org model lists; inspect source before assuming scope.organization_payment_policies is registered but getter currently returns baseClient.StorePrismaService:
store_id on create only for store_scoped_models.baseClient and require manual scoping, including examples like users, stores, audit_logs, product_categories, customer_queue, and invoice_data_requests.EcommercePrismaService:
store_id; throws if missing.store_id and user/customer fields on create when model type supports it.scoped_client for models not registered in all model lists; inspect source before relying on automatic scope.review_votes and review_reports currently return baseClient.Prisma unique operations (findUnique, update, delete, and the where part of upsert) require a WhereUniqueInput. Scoped Prisma extensions may merge tenant filters into where. If that merge wraps the lookup as { AND: [...] }, Prisma rejects it before SQL execution with errors like:
Argument where of type ...WhereUniqueInput needs at least one of id or store_id arguments
Rules:
findUnique({ where: { id } }) or findUnique({ where: { store_id } }) stays valid after the scope extension runs.findFirst({ where: { id, store_id } }) or findFirst({ where: { store_id } }) so the final query can include both unique and tenant predicates.updateMany/deleteMany with explicit tenant filters, then check the affected count when the caller needs "not found" behavior.upsert only after confirming the scoped service preserves a top-level unique field in where; otherwise split the flow into scope-safe read/create/update steps.withoutScope() in request handlers. Use withoutScope() only for the approved cases in this skill and add explicit tenant filters manually.store_settings, should live behind a helper that encodes the safe query shape.Example:
// Avoid in scoped services when the extension may add tenant filters.
await prisma.store_settings.findUnique({ where: { store_id } });
await prisma.domain_settings.update({ where: { id }, data });
// Prefer scope-safe shapes.
await prisma.store_settings.findFirst({ where: { store_id } });
await prisma.domain_settings.updateMany({
where: { id, store_id, domain_type: 'ecommerce' },
data,
});
withoutScope() in request handlers unless explicitly approved.ForbiddenException: missing store/org context or model expects scope but context was not populated.UnauthorizedException: no request context, often a job/script using a scoped service.Argument where of type ...WhereUniqueInput... with where: { AND: [...] }: a scoped extension transformed a unique query into a non-unique shape; replace it with a scope-safe findFirst, updateMany, or split upsert flow.baseClient or model missing from extension registration.vendix-multi-tenant-contextvendix-prisma-schemavendix-prisma-migrationsvendix-backend-domaindevelopment
Mobile app development rules for Vendix Expo/React Native project. Trigger: When editing, creating, or modifying any file under apps/mobile, or when developing mobile-specific features.
development
Feature gating by store subscription state: global store write guard, AI feature gate, Redis feature resolution, quota consumption, frontend paywall interceptor, banner, and subscription UI states. Trigger: When adding feature gates, paywalls, subscription-based access control, protecting store write operations, AI feature gates, or rollout flags.
testing
SaaS subscription billing for Vendix stores: plan pricing, invoices, Wompi platform payments, manual payments, partner commissions, payouts, proration, and dunning. Trigger: When creating SaaS invoices, working with partner rev-share, margin/surcharge pricing, invoice sequence allocation, partner payout batches, subscription payments, manual payments, or dunning flows.
development
Periodic quota counters with Redis, UTC period keys, Lua-based idempotent AI quota consumption, request-id deduplication, and post-success consumption. Trigger: When building quota counters, enforcing monthly/daily feature caps, or reusing AI quota patterns for uploads, emails, exports, or rate-limited features.