The Subscription & Billing architecture controls the financial lifecycle of Malet Owners and Organizations on the Mallnline platform. It ensures that account limits (such as the number of active Malets or team members) are rigorously enforced server-side, preventing exploitation while maintaining a seamless user experience.
Single Sources of Truth
We maintain strict parity between the frontend display and the backend enforcement logic using two single sources of truth for tier limits:
- Frontend:
ngwenya-front/src/lib/config/plans.ts(Drives the UI on pricing pages and checkout funnels) - Backend:
ngwenya-federation/libs/common/src/config/tier-limits.ts(Drives the enforcement guards across the supergraph)
When adjusting the metrics for the Starter, Pro, or Enterprise plans, both files must be updated simultaneously.
// Example: libs/common/src/config/tier-limits.ts
export const TIER_LIMITS: Record<TierSlug, TierLimits> = {
starter: { maxMalets: 1, maxMembers: 3 },
pro: { maxMalets: 5, maxMembers: 10 },
enterprise: { maxMalets: Infinity, maxMembers: Infinity }
};
The Payments Subgraph
The payments subgraph is the canonical owner of the Subscription entity. Rather than forcing every other subgraph to hit the database to query billing status, the payments service exposes a lightweight TCP Microservice Transport on port 3018.
It runs a SubscriptionTcpController that handles the get_active_tier message pattern.
@MessagePattern('get_active_tier')
async getActiveTier(@Payload() data: { userId?: string, orgId?: string }) {
// Finds the active subscription in the DB
// Returns { tier: 'pro', limits: { maxMalets: 5, ... } }
}
Enforcement Guards
Other subgraphs act as TCP Clients to the payments service whenever an action occurs that is gated by a billing tier.
Malet Creation Limits
When a user calls createOneMalet in the malets subgraph, the resolver:
- Counts the existing Malets owned by that user/org.
- Queries
get_active_tierfrompayments. - Evaluates
currentCount >= limits.maxMalets. - Throws a
ForbiddenExceptioninstructing the user to upgrade to Pro if the limit is exceeded.
Organization Member Limits
When an admin calls inviteMember in the organizations subgraph, the resolver:
- Counts the existing users in the Organization.
- Queries
get_active_tierfrompayments. - Evaluates
currentCount >= limits.maxMembers. - Throws a
ForbiddenExceptioninstructing the admin to upgrade if the limit is exceeded.
Fail-Open / Graceful Degradation
If the payments TCP service is unreachable (e.g., during a deployment rollout or unexpected downtime), the TCP clients are configured with a strict 3-second timeout.
Instead of crashing the request, the clients employ a Fail-Open (Downgrade) strategy. They will automatically assume the user is on the Starter tier. This ensures that free-tier users can always access their basic limits, while temporarily preventing systemic outages from blocking core functionalityโthough it may temporarily block Pro users from exceeding Starter limits during the outage.
Enterprise Plan (Contact Sales)
The Enterprise tier has no self-serve pricing. Both the storefront (/owner/pricing) and The Deck (/orgs/upgrade) display "Custom" pricing for Enterprise and route the CTA to a contact form (/contact?subject=enterprise). The PLAN_TIERS definition in @ngwenya/queries/subscriptions and the plans.ts config in ngwenya-front both set contactSales: true with monthlyPrice: 0 / yearlyPrice: 0.
Enterprise provisioning is sales-assisted: the sales team creates the org with custom tier limits via the Tower admin dashboard. Future: Stripe seat-based subscriptions for per-member billing.
Organization Billing Security
All org-scoped billing endpoints in the payments subgraph are protected by a dual-guard chain:
- GqlAuthGuard โ verifies the request is authenticated
- OrgMembershipGuard โ verifies the user is a member of the target organization by querying the shared
membershipsMongoDB collection
This prevents cross-organization data leakage โ a user authenticated as OrgA cannot query billing data for OrgB. The guard is implemented as a reusable @UseGuards(GqlAuthGuard, OrgMembershipGuard) decorator stack applied to all 8 org-scoped endpoints.
See Payments Subgraph โ Organization Membership Security for full details.
Related
- Payments Subgraph โ Full API reference, guard matrix, and payment method lifecycle
- Subscription Checkout Identity & Idempotency โ How orgId flows through checkout, idempotency guards, and debug tracing
- Org Subscription Bootstrap & Ownership Picker โ Auto-Starter provisioning on org creation and the OwnershipPicker wizard component
- Organizations โ Details the membership structures that are constrained by tier limits.
- Custom RBAC โ The granular permissions matrix that is fully unlocked on the Enterprise tier.
- Org Context & Tier Access โ Frontend canCreateOrganization() gate, upgrade wall, and OrgSwitcher tier-gated link.
- Owner Destinations โ Pricing โ Standalone
/owner/pricingpage consumingplans.tswith billing toggle, feature matrix, and commission rates. - Stripe Invoice Integration โ End-to-end invoice data flow from Stripe API to frontend billing page.
- Starter to Pro Upgrade Flow โ How billing models and tier limits are managed during an upgrade and transfer.
- Subscription Lifecycle Management โ Grace periods, payment failure handling, and org status transitions on cancellation.