skills/vendix-subscription-gate/SKILL.md
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.
npx skillsauth add rzyfront/vendix vendix-subscription-gateInstall 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/domains/store/subscriptions/services/subscription-access.service.tsapps/backend/src/domains/store/subscriptions/services/subscription-resolver.service.tsapps/backend/src/domains/store/subscriptions/guards/store-operations.guard.tsapps/backend/src/domains/store/subscriptions/guards/ai-access.guard.tsapps/backend/src/domains/store/subscriptions/decorators/skip-subscription-gate.decorator.tsapps/backend/src/domains/store/subscriptions/config/subscription-gate.config.tsapps/backend/src/ai-engine/ai-engine.service.tsStoreOperationsGuard is registered as an APP_GUARD in apps/backend/src/app.module.ts.
It applies only when all of these are true:
POST, PATCH, PUT, or DELETE./api/store/./api/store/subscriptions/**.@SkipSubscriptionGate().req.user.store_id because ALS context is not ready yet in guards.Warn results set X-Subscription-Warning. Block results log STORE_GATE_OBSERVATION and throw only when SubscriptionGateConfig.isEnforce() is true.
Use @SkipSubscriptionGate() for subscription management, platform webhook/payment unblock paths, and other handlers that must remain reachable while blocked.
Primary flag:
STORE_GATE_ENFORCE=true enables enforcement.Deprecated alias:
AI_GATE_ENFORCE=true also enables enforcement and logs a deprecation warning.SUBSCRIPTION_CRON_DRY_RUN=true enables dry-run behavior for some subscription jobs.
Known implementation caveat: SubscriptionAccessService has an internal private isEnforceMode() path that checks only AI_GATE_ENFORCE for internal-error fail-open/fail-closed behavior. Other guard/AI paths use SubscriptionGateConfig.isEnforce().
Current backend stateToMode() behavior:
| State | Mode | Reason |
| --- | --- | --- |
| active, trial | allow | none |
| grace_soft | warn | SUBSCRIPTION_007 |
| grace_hard | warn by default, block if feature degradation='block' | SUBSCRIPTION_009 when blocked |
| suspended | block | SUBSCRIPTION_008 |
| blocked | block | SUBSCRIPTION_009 |
| cancelled, expired | block | SUBSCRIPTION_003 |
| no_plan | block | SUBSCRIPTION_004 |
| draft/default | block | SUBSCRIPTION_002 |
Important nuance: GET/HEAD/OPTIONS requests bypass StoreOperationsGuard, so read routes remain reachable at the guard layer. But canUseModule() currently maps cancelled to block; do not document module checks as read-allow for cancelled unless code changes.
SubscriptionResolverService materializes effective plan features and caches them in Redis.
sub:features:{storeId}.GlobalPrismaService with explicit store_id filters.paid_plan_id as source of truth for paid feature gating; trial uses plan_id.no_plan resolves empty features and null plan id.Feature resolution order:
subscription_plans.ai_feature_flags.Call SubscriptionAccessService.invalidateCache(storeId) synchronously after subscription state, plan, partner override, or promo overlay changes.
Use both declarative guard and inline service checks for AI features.
Controller layer:
@Post('generate')
@UseGuards(AiAccessGuard)
@RequireAIFeature('text_generation')
generate() {}
Inline layer:
AIEngineService.runSubscriptionGate() executes before provider work.AIEngineService.consumeSubscriptionQuota() runs after successful provider output.AI_GATE_CHECK and AI_GATE_OBSERVATION.Do not consume quota before provider success.
consumeAIQuota() uses Redis with request-id dedupe.
ai:quota:{storeId}:{feature}:{period}.ai:quota:dedup:{storeId}:{feature}:{period}.YYYYMMDD, TTL 48h.YYYYMM, TTL 40d.internal-${randomUUID()}.apps/frontend/src/app/core/store/subscription/apps/frontend/src/app/core/interceptors/subscription-paywall.interceptor.tsapps/frontend/src/app/core/services/subscription-access.service.tsapps/frontend/src/app/shared/components/ai-paywall-modal/apps/frontend/src/app/shared/components/subscription-banner/subscription-banner.component.tsapps/frontend/src/app/private/modules/store/subscription/The HTTP interceptor opens a paywall by backend error_code, not strictly by HTTP status.
Codes handled include SUBSCRIPTION_002 through SUBSCRIPTION_009, PLAN_001, and TRIAL_001. It rethrows the original error after opening the modal.
Suppression rules prevent duplicate paywalls on:
/admin/subscription/picker./admin/subscription/dunning./admin/subscription.SubscriptionAccessService.openPaywall(code, message?, details?) maps backend codes and synthetic state codes to variant config. It also supports state-driven paywalls and payment-success microinteraction.
<app-paywall-outlet /> is mounted in store-admin, organization-admin, and super-admin layouts. The subscription banner is store-admin only.
SubscriptionFacade exposes both observables and signals with toSignal(..., { initialValue }).
Important facade concepts:
subscriptionContextChanged(storeId) resets/loads state by store context.subscriptionUiState is the frontend cascade for grace, terminal states, no-plan, pending payment/change, expiring soon, and healthy states./store/subscriptions/current, /current/access, checkout preview/commit, invoices, dunning state, retry payment, coupon validation, and invoice sync from gateway./store/notifications/stream?token=... and reacts to subscription.updated.SubscriptionBannerComponent is store-scoped and visible only for expiring_soon, grace_soft, and grace_hard. Terminal and pending states are handled by subscription pages/paywall variants.
Store dunning page shows deadline, total due, overdue invoices, features lost/kept, retry payment, and support request CTA.
StoreOperationsGuard; current guard intentionally skips reads.AiAccessGuard; internal jobs/services need inline checks.sub:features:{storeId} invalidation after subscription state or plan/override changes.cancelled is allowed by canUseModule() unless the backend mapping is changed.vendix-saas-billing - Subscription invoices, payments, dunning, plans, commissionsvendix-redis-quota - Period keyed Redis quota countersvendix-error-handling - Subscription error codesvendix-multi-tenant-context - Store context resolutionvendix-ai-engine - Inline AI gate consumerdevelopment
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.
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.
data-ai
Product and service variant rules for Vendix: variants as sellable options, inventory-independent availability, service variant booking overrides, and ecommerce/POS/cart behavior. Trigger: When creating, editing, validating, or selling product variants, service variants, products without stock, or any flow where variant availability must not be confused with inventory.