# How the Multi-Tenant Organization System Works with Org-Specific Billing in Open SEO

> Discover how Open SEO's multi-tenant organization system utilizes org-specific billing and Cloudflare KV caching to manage subscriptions across all server functions and MCP tools.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-07-30

---

**Open SEO binds every user session, data record, and billing transaction to a unique organization ID, using the Autumn billing service with Cloudflare KV caching to enforce per-organization subscription tiers across all server functions and MCP tools.**

Open SEO is architected as a multi-tenant SaaS platform where each logical customer exists as a distinct organization entity. The system isolates data, permissions, and billing through a consistent organization identifier that propagates from the database schema through to third-party billing integrations. Understanding how the multi-tenant organization system works with org-specific billing is essential for developers extending the platform or deploying self-hosted instances.

## Organization Model and Database Schema

The foundation of Open SEO's multi-tenancy resides in [`src/db/better-auth-schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/better-auth-schema.ts), where the **organization** table defines the core tenant boundary【/cache/repos/github.com/every-app/open-seo/main/src/db/better-auth-schema.ts#L11-L21】.

Each user can belong to multiple organizations through the **member** table, which maintains a foreign key relationship (`member.organizationId` → `organization.id`). The **session** record stores the current working context in `session.activeOrganizationId`, ensuring every request operates within a specific organizational scope.

When a request enters the system, the [`ensureUser.ts`](https://github.com/every-app/open-seo/blob/main/ensureUser.ts) middleware resolves the session and constructs an `EnsuredUserContext` object that always carries the `organizationId` derived from the active session. This context object becomes the authoritative tenant identifier for all downstream operations.

## Billing Architecture and Autumn Integration

Per-organization billing data lives in [`src/db/billing.schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/billing.schema.ts), specifically the `billing_customer_status` table【/cache/repos/github.com/every-app/open-seo/main/src/db/billing.schema.ts#L5-L21】. This table maintains:
- `organizationId` as a foreign key to the organization table
- `isPaying`, `paidPlanId`, and `paidPlanStatus` flags
- A cached JSON payload from the Autumn billing service

The billing service implementation in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) treats the organization ID as the canonical customer identifier in the Autumn API:

```typescript
// get or create the Autumn customer for the organization
const customer = await autumn.customers.getOrCreate({
  customerId: context.organizationId,
  email: context.userEmail,
});

```

The `customerHasPaidPlan(orgId)` function checks the Autumn feature flag `AUTUMN_PAID_PLAN_FEATURE_ID` to determine subscription status【/cache/repos/github.com/every-app/open-seo/main/src/server/billing/subscription.ts#L66-L73】. To prevent API throttling, billing status is cached in Cloudflare KV under the key `autumn:customer-ensured:<orgId>` with a 24-hour TTL【/cache/repos/github.com/every-app/open-seo/main/src/server/billing/subscription.ts#L30-L38】.

## Request Flow and Context Propagation

Every authenticated request flows through three distinct phases:

1. **Authentication Layer**: The [`src/middleware/ensureUser.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensureUser.ts) middleware extracts `session.activeOrganizationId` and builds the `EnsuredUserContext` containing the resolved `organizationId`.

2. **Authorization Gateway**: Server-side functions (such as `rank-tracking`, `ai-search`, and `project` APIs) receive `context: EnsuredUserContext` as their first parameter. These functions immediately validate the organization and invoke `customerHasPaidPlan(context.organizationId)` to gate paid features【/cache/repos/github.com/every-app/open-seo/main/src/serverFunctions/rank-tracking.ts#L129-L200】.

3. **MCP Tool Execution**: Model-Context-Protocol (MCP) tools embed the organization ID in `McpToolAuthContext`. The helper `buildBillingCustomer` constructs a `BillingCustomerContext` from this auth data, enabling billing checks within tool execution【/cache/repos/github.com/every-app/open-seo/main/src/server/mcp/context.ts#L19-L29】.

## Multi-Tenant Isolation Guarantees

Open SEO enforces strict tenant isolation across four critical dimensions:

**Data Isolation**: All organization-specific tables—including `billing_customer_status`, `project`, `gsc`, and `reddit_attribution`—include `organizationId` foreign keys. Queries always filter by this identifier, such as `ProjectService.listProjects(context.organizationId)`.

**Permission Enforcement**: The `member.role` field determines user capabilities. Before any mutation, the server verifies that `member.organizationId` matches the `context.organizationId`.

**Billing Isolation**: The Autumn API receives the organization ID as the customer identifier, ensuring each tenant maintains an independent credit balance and subscription plan. Billing checks never execute globally across organizations.

**Cache Safety**: KV cache entries are namespaced by organization (`autumn:customer-ensured:<orgId>`), preventing cross-tenant cache leakage.

## Self-Hosted Mode Behavior

When deployed in self-hosted environments, Open SEO uses the identical schema and code paths but disables paid-plan enforcement. The `isHosted` flag, when set to `false`, causes functions like rank-tracking to skip `customerHasPaidPlan` validation while maintaining the same organization scoping logic【/cache/repos/github.com/every-app/open-seo/main/src/serverFunctions/rank-tracking.ts#L129-L200】. This ensures billing infrastructure remains intact for future migration to hosted mode without schema changes.

## Practical Implementation Examples

### Creating a New Organization

This example demonstrates inserting an organization and establishing the owner relationship:

```typescript
import { db } from "@/db";
import { organization } from "@/db/better-auth-schema";

export async function createOrganization(name: string, slug: string, userId: string) {
  // Insert organization
  const org = await db
    .insert(organization)
    .values({ name, slug })
    .returning()
    .get();

  // Add the creator as a member (owner role could be stored elsewhere)
  await db.insert(member).values({
    organizationId: org.id,
    userId,
    role: "owner",
  });

  return org;
}

```

*Source:* `organization` table definition【/cache/repos/github.com/every-app/open-seo/main/src/db/better-auth-schema.ts#L11-L21】.

### Validating Paid Plan Access

Gate feature execution using the billing subscription checker:

```typescript
import { customerHasPaidPlan } from "@/server/billing/subscription";

export async function canRunPaidTool(context: EnsuredUserContext) {
  const hasPlan = await customerHasPaidPlan(context.organizationId);
  if (!hasPlan) throw new Error("Paid plan required");
  // …run the paid-feature
}

```

*Source:* `customerHasPaidPlan` implementation【/cache/repos/github.com/every-app/open-seo/main/src/server/billing/subscription.ts#L66-L73】.

### Charging Credits in MCP Tools

Manage usage credits within Model-Context-Protocol tools:

```typescript
import { buildBillingCustomer, assertUsageCreditsAvailable } from "@/server/billing/subscription";
import { requireMcpToolAuthContext } from "@/server/mcp/context";

export async function runDataforSEO(extra: ToolExtra, projectId: string) {
  const auth = requireMcpToolAuthContext(extra);
  const billingCtx = buildBillingCustomer(auth, projectId);

  // Ensure the org still has credits
  await assertUsageCreditsAvailable(billingCtx.organizationId);

  // Proceed with DataforSEO call…
}

```

*Sources:* `requireMcpToolAuthContext`【/cache/repos/github.com/every-app/open-seo/main/src/server/mcp/context.ts#L100-L108】, `buildBillingCustomer`【/cache/repos/github.com/every-app/open-seo/main/src/server/mcp/context.ts#L19-L29】, `assertUsageCreditsAvailable`【/cache/repos/github.com/every-app/open-seo/main/src/server/billing/subscription.ts#L82-L93】.

### Listing Organization-Scoped Projects

Retrieve projects filtered by the authenticated organization:

```typescript
import { ProjectService } from "@/server/services/project";
import { requireMcpToolAuthContext } from "@/server/mcp/context";

export async function listProjects(extra: ToolExtra) {
  const auth = requireMcpToolAuthContext(extra);
  const projects = await ProjectService.listProjects(auth.organizationId);
  return projects;
}

```

*Source:* `list_projects` tool implementation【/cache/repos/github.com/every-app/open-seo/main/src/server/mcp/tools/list-projects.ts#L41-L52】.

## Summary

- **Tenant Identification**: Open SEO uses `organization.id` as the root tenant identifier, stored in user sessions and propagated through `EnsuredUserContext` to all server functions.
- **Billing Integration**: The Autumn billing service treats `organizationId` as the customer ID, with payment status cached in Cloudflare KV for 24 hours to optimize API usage.
- **Data Isolation**: All tables containing organization-specific data include `organizationId` foreign keys, and queries always filter by this identifier to prevent cross-tenant access.
- **Feature Gating**: The `customerHasPaidPlan` function in [`src/server/billing/subscription.ts`](https://github.com/every-app/open-seo/blob/main/src/server/billing/subscription.ts) enforces subscription tiers, while MCP tools use `buildBillingCustomer` to validate credit availability.
- **Self-Hosted Compatibility**: The same schema and billing code paths execute in self-hosted mode, with the `isHosted` flag disabling payment checks while preserving organizational scoping.

## Frequently Asked Questions

### How does Open SEO handle users who belong to multiple organizations?

Open SEO supports multi-organization membership through the `member` table, which maintains many-to-many relationships between users and organizations. The `session.activeOrganizationId` field determines which organization context is currently active, and the `EnsuredUserContext` object carries this ID through the request lifecycle. Users switch contexts by updating their active organization in the session, which changes the `organizationId` available to all server functions without altering the underlying membership records.

### What prevents one organization from accessing another organization's data?

Data isolation relies on foreign key relationships and mandatory query filters. Every organization-specific table includes an `organizationId` column, and the application layer enforces that all queries—whether in `ProjectService`, billing checks, or MCP tools—filter by the `organizationId` extracted from the authenticated session. The middleware in [`src/middleware/ensureUser.ts`](https://github.com/every-app/open-seo/blob/main/src/middleware/ensureUser.ts) validates that the user has membership in the claimed organization before constructing the context object, preventing unauthorized cross-tenant access.

### How does the billing system handle organization-specific credit balances?

The Autumn billing service uses the `organizationId` as the unique customer identifier via `autumn.customers.getOrCreate()`. This ensures each organization maintains an independent subscription status and credit balance. The `customerHasPaidPlan` function checks this external service while caching results in Cloudflare KV under org-specific keys (`autumn:customer-ensured:<orgId>`), ensuring billing isolation and reducing API latency for subsequent requests within the 24-hour cache window.

### Can the multi-tenant system run without the Autumn billing service?

Yes. In self-hosted deployments, Open SEO sets the `isHosted` flag to `false`, which bypasses the `customerHasPaidPlan` checks in features like rank-tracking while preserving all organization scoping logic. The same database schema and context propagation mechanisms remain active, allowing seamless migration to hosted mode with billing enabled later. The billing tables and KV caching infrastructure remain in place, ensuring no schema migrations are required when transitioning between modes.