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:

  1. isBillingEnabled() – Checks the IS_BILLING_ENABLED environment flag
  2. hasEntitlement() – Validates workspace access to specific features
  3. canBillMeteredProduct() – 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 BillingService for entitlement decisions and BillingSubscriptionService for subscription state management.
  • Stripe integration occurs through checkout and portal API routes in the website package, creating sessions via the Stripe SDK.
  • Metered gating prevents usage overages by checking hasReachedCurrentPeriodCap and subscription status before executing paid features.
  • Database persistence uses TypeORM entities in the billing schema 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →