# How Billing Is Handled in Twenty CRM: Stripe Integration and Entitlement Architecture

> Discover how Twenty CRM handles billing using Stripe integration and entitlement architecture. Learn about feature flags, metered product gating, and server-side NestJS modules for access control.

- Repository: [Twenty/twenty](https://github.com/twentyhq/twenty)
- Tags: architecture
- Published: 2026-03-27

---

**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`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/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:

```typescript
@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`](https://github.com/twentyhq/twenty/blob/main/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:

```typescript
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`](https://github.com/twentyhq/twenty/blob/main/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:

```typescript
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`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-website/src/app/api/enterprise/portal/route.ts) generates Stripe Billing Portal sessions for invoice management and payment method updates:

```typescript
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`](https://github.com/twentyhq/twenty/blob/main/src/engine/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts) validates billing permissions before running metered workflows:

```typescript
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.