How Billing Is Handled in Twenty CRM: Stripe Integration and Entitlement Architecture
Twenty CRM implements billing as a server-side NestJS module with Stripe integration, using feature flags, entitlement checks, and metered product gating to control access to paid features.
The billing system in Twenty CRM is built as a first-class enterprise feature that operates entirely on the server side. Located within the twentyhq/twenty repository, this architecture manages subscriptions, metered usage, and feature entitlements through a modular NestJS implementation backed by Stripe.
Architecture Overview
The billing handled within the Twenty CRM platform follows a three-layer architecture that separates configuration, domain logic, and external integrations.
The Three-Layer Stack
| Layer | Responsibility | Key Implementation |
|---|---|---|
| Configuration | Feature flag IS_BILLING_ENABLED decides if billing logic should run. |
TwentyConfigService (src/engine/core-modules/twenty-config/twenty-config.service.ts) |
| Domain services | Core billing concepts – subscriptions, products, meters, entitlements, usage. | BillingService, BillingSubscriptionService, BillingProductService, BillingUsageService |
| Integration | Stripe SDK wrappers, webhook listeners, UI helpers for checkout & portal. | StripeModule, stripe-client.ts, checkout & portal API routes |
Database Schema and Entities
All billing-related entities are stored in the core-modules billing schema using PostgreSQL tables such as billingSubscription, billingCustomer, and billingProduct. The BillingModule bundles services, resolvers, and Stripe integration, importing into modules that require entitlement checks like the workflow executor and AI agent.
Core Billing Services
The domain layer centers on two primary services that handle subscription state and entitlement validation.
BillingService: The Central Decision Point
Located in src/engine/core-modules/billing/services/billing.service.ts, the BillingService class acts as the primary façade for all billing decisions. It provides three critical methods:
isBillingEnabled()– Checks theIS_BILLING_ENABLEDenvironment flaghasEntitlement()– Validates workspace access to specific featurescanBillMeteredProduct()– Gates metered usage like AI credits or workflow executions
Here is the core implementation:
@Injectable()
export class BillingService {
// Simple feature‑flag guard
isBillingEnabled() {
return this.twentyConfigService.get('IS_BILLING_ENABLED');
}
// Entitlement check (e.g., “has AI credits?”)
async hasEntitlement(workspaceId: string, entitlementKey: BillingEntitlementKey) {
if (!this.isBillingEnabled()) return true; // billing disabled → always OK
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
workspaceId,
entitlementKey,
);
}
// Metered product gating (used by workflow executor & AI agent)
async canBillMeteredProduct(
workspaceId: string,
productKey: BillingProductKey,
): Promise<boolean> {
const subscription =
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
{ workspaceId },
);
// Only Active or Trialing subscriptions may be billed
const billableStatuses = [SubscriptionStatus.Active, SubscriptionStatus.Trialing];
if (!billableStatuses.includes(subscription.status)) return false;
// Resolve the product definition for the workspace’s plan
const planKey = getPlanKeyFromSubscription(subscription);
const products = await this.billingProductService.getProductsByPlan(planKey);
const targetProduct = products.find(p => p.metadata.productKey === productKey);
if (!targetProduct) return false;
// Find the subscription item for that product and ensure the usage cap isn’t hit
const subscriptionItem = subscription.billingSubscriptionItems.find(
i => i.stripeProductId === targetProduct.stripeProductId,
);
return subscriptionItem?.hasReachedCurrentPeriodCap === false;
}
}
Subscription Retrieval Logic
The BillingSubscriptionService in src/engine/core-modules/billing/services/billing-subscription.service.ts manages subscription lifecycle and database queries. Its getCurrentBillingSubscriptionOrThrow method enforces the invariant that only one non-canceled subscription may exist per workspace:
async getCurrentBillingSubscriptionOrThrow(criteria: {
workspaceId?: string;
stripeCustomerId?: string;
}): Promise<BillingSubscriptionEntity> {
const notCanceled = await this.billingSubscriptionRepository.find({
where: { ...criteria, status: Not(SubscriptionStatus.Canceled) },
relations: ['billingSubscriptionItems', 'billingSubscriptionItems.billingProduct'],
});
if (notCanceled.length > 1) {
throw new BillingException(
`More than one not canceled subscription for workspace ${criteria.workspaceId}`,
BillingExceptionCode.BILLING_TOO_MUCH_SUBSCRIPTIONS_FOUND,
);
}
return notCanceled[0];
}
Stripe Integration and Checkout Flow
The Twenty CRM platform integrates Stripe through dedicated API routes in the website package, handling checkout sessions and billing portal access.
Checkout Session Creation
The endpoint at packages/twenty-website/src/app/api/enterprise/checkout/route.ts creates Stripe Checkout sessions for new subscriptions. It maps billing intervals to Stripe price IDs and attaches the workspace's existing Stripe customer ID:
export async function POST(request: Request) {
const { billingInterval } = await request.json(); // 'monthly' | 'yearly'
const priceId = getEnterprisePriceId(billingInterval); // maps interval to Stripe price
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
// workspace‑specific customer ID stored in DB
customer: workspaceStripeCustomerId,
success_url: `${BASE_URL}/settings/billing?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${BASE_URL}/settings/billing`,
});
return new Response(JSON.stringify({ url: session.url }));
}
Billing Portal Management
For existing subscribers, the portal route at packages/twenty-website/src/app/api/enterprise/portal/route.ts generates Stripe Billing Portal sessions for invoice management and payment method updates:
export async function POST(request: Request) {
const { returnUrlPath } = await request.json();
const session = await stripe.billingPortal.sessions.create({
customer: workspaceStripeCustomerId,
return_url: `${BASE_URL}${returnUrlPath}`,
});
return new Response(JSON.stringify({ url: session.url }));
}
Feature Gating and Entitlement Checks
Billing gates are enforced at the feature level through metered product validation and entitlement lookups.
Metered Product Validation
Before executing billable actions, modules call BillingService.canBillMeteredProduct(). This method verifies that:
- The subscription status is Active or Trialing
- The specific product exists in the workspace's plan
- The usage cap (
hasReachedCurrentPeriodCap) has not been exceeded
If any check fails, the action is blocked before Stripe metering occurs.
Integration with Workflow Executor and AI
The workflow executor in src/engine/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts validates billing permissions before running metered workflows:
if (!await this.billingService.canBillMeteredProduct(workspaceId, productKey)) {
throw new BadRequestException(BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE);
}
Similarly, the AI agent module uses AiBillingService (wrapping BillingService) to enforce credit limits on inference requests. The workspace cleaner also interacts with BillingSubscriptionService.deleteSubscriptions to cancel Stripe subscriptions when workspaces are deleted.
Summary
- Billing handled within the Twenty CRM platform operates as a NestJS module in
src/engine/core-modules/billing, using a feature flag (IS_BILLING_ENABLED) to toggle functionality. - Core services include
BillingServicefor entitlement decisions andBillingSubscriptionServicefor subscription state management. - Stripe integration occurs through
checkoutandportalAPI routes in the website package, creating sessions via the Stripe SDK. - Metered gating prevents usage overages by checking
hasReachedCurrentPeriodCapand subscription status before executing paid features. - Database persistence uses TypeORM entities in the
billingschema to track customers, subscriptions, products, and usage records.
Frequently Asked Questions
How does Twenty CRM handle billing when the feature flag is disabled?
When IS_BILLING_ENABLED is set to false, BillingService.isBillingEnabled() returns false, causing all entitlement checks like hasEntitlement() and canBillMeteredProduct() to short-circuit and return true. This allows the platform to behave as if every workspace has an active subscription without requiring Stripe configuration.
What database entities store billing information in Twenty?
The system uses TypeORM entities stored in PostgreSQL tables within the billing schema, including billingSubscription, billingCustomer, billingProduct, and billingSubscriptionItem. These entities track Stripe customer IDs, subscription statuses, product configurations, and usage caps.
How does the workflow executor check billing permissions?
Before executing a metered workflow action, the workflow executor calls BillingService.canBillMeteredProduct(workspaceId, productKey). If the workspace lacks an active subscription or has exceeded its usage cap for the specific product key, the service throws a BadRequestException and prevents execution.
Can self-hosted instances of Twenty CRM disable billing entirely?
Yes. Self-hosted instances can disable the billing system by setting the environment variable IS_BILLING_ENABLED=false in their configuration. This removes all billing gates and allows unlimited access to features that would normally require a subscription or metered credits.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →