Skip to content

Instantly share code, notes, and snippets.

@matthew-gerstman
Created February 8, 2026 00:15
Show Gist options
  • Select an option

  • Save matthew-gerstman/15821ac95e5b4d9c9e4e1a6d59f545f8 to your computer and use it in GitHub Desktop.

Select an option

Save matthew-gerstman/15821ac95e5b4d9c9e4e1a6d59f545f8 to your computer and use it in GitHub Desktop.
Arbitrary SKUs: Architecture Changes for Enterprise Deals & Employee Plans

Arbitrary SKUs: Architecture Changes

Problem

Plans are hardcoded as a TypeScript union type and a static PLAN_METADATA object. Adding a new plan requires a code change and deploy. We need to support:

  1. Enterprise deals — custom plan per customer (custom credits, custom price, custom features)
  2. Employee accounts — free team plan with unlimited credits, no Stripe subscription
  3. Future — any one-off SKU without touching application code

Current Architecture (What's Hardcoded)

1. Plan IDs are a closed union type

plan-utils.ts:17
type PlanId = 'personal_standard' | 'personal_pro' | 'team_standard' | 'team_pro' | 'enterprise'

Every function that touches plans uses this type. normalizePlanId() returns null for anything not in this list. The entire billing system is downstream of this type.

2. Plan metadata is a static object

billing.config.ts:436
const PLAN_METADATA: Record<PlanId, Omit<BillingPlan, 'productId' | 'priceId'>> = { ... }

Credits, pricing, limits, features — all baked into this object at build time. There's no way to say "workspace X gets 500 credits/month" without changing this file.

3. Feature gating is a static mapping

plan-features.ts:149
const PLAN_FEATURES: Record<PlanId, PlanFeatureId[]> = { ... }

planHasFeature() does a lookup into this static map. A custom enterprise plan can't have a custom feature set.

4. Stripe price → plan resolution is static

stripe-webhooks.ts:902
function determineTierFromPriceId(priceId: string): string {
  const plan = getBillingPlanByPriceId(priceId)  // scans BILLING_PLANS
  ...
}

Every webhook resolves the internal plan by matching a Stripe price ID against the 5 hardcoded plans. A custom Stripe product for an enterprise customer won't match.

5. Credit grants are derived from static plan config

stripe-webhooks.ts:183
const creditsPerSeat = plan.limits?.includedCredits || 0

There's no per-workspace override. Credits come from the plan definition.

Proposed Changes

Phase 1: Workspace-Level Overrides (Minimum Viable)

The cheapest path that unblocks enterprise deals and employee accounts without rearchitecting the entire billing system. The idea: keep the 5 base plans, but let individual workspaces override any plan property.

1a. New DB columns on workspaces table

ALTER TABLE workspaces ADD COLUMN plan_overrides JSONB DEFAULT NULL;
ALTER TABLE workspaces ADD COLUMN is_custom_plan BOOLEAN DEFAULT FALSE;

plan_overrides schema:

interface PlanOverrides {
  // Override the base plan this workspace inherits from
  // e.g., an enterprise deal might use 'team_pro' as the base
  basePlanId?: PlanId

  // Override credits per seat (or total for personal plans)
  includedCredits?: number  // null = use base plan, -1 = unlimited

  // Override credit price
  creditPrice?: number  // null = use base plan, 0 = free

  // Override monthly base price (informational — Stripe is source of truth)
  monthlyBase?: number

  // Additional features beyond the base plan
  additionalFeatures?: PlanFeatureId[]

  // Override seat limit
  seatLimit?: number  // null = use base plan, -1 = unlimited

  // Human-readable label for this custom plan
  planLabel?: string  // e.g., "Acme Corp Enterprise", "Employee Plan"

  // Disable credit expiration (purchased behavior for subscription credits)
  creditsNeverExpire?: boolean

  // Skip Stripe billing entirely (for gifted/internal plans)
  skipBilling?: boolean
}

1b. Resolver function: getEffectivePlan(workspaceId)

Replace all direct getBillingPlan(workspace.subscriptionTier) calls with a resolver that merges overrides:

function getEffectivePlan(workspace: Workspace): EffectiveBillingPlan {
  const basePlan = getBillingPlan(
    workspace.planOverrides?.basePlanId ?? workspace.subscriptionTier
  )

  if (!workspace.planOverrides || !workspace.isCustomPlan) {
    return basePlan  // No overrides, return base plan as-is
  }

  const overrides = workspace.planOverrides
  return {
    ...basePlan,
    limits: {
      ...basePlan.limits,
      includedCredits: overrides.includedCredits ?? basePlan.limits.includedCredits,
      members: overrides.seatLimit ?? basePlan.limits.members,
    },
    pricing: {
      ...basePlan.pricing,
      creditPrice: overrides.creditPrice ?? basePlan.pricing.creditPrice,
      monthlyBase: overrides.monthlyBase ?? basePlan.pricing.monthlyBase,
    },
    name: overrides.planLabel ?? basePlan.name,
  }
}

1c. Feature gating changes

function planHasFeature(workspace: Workspace, featureId: PlanFeatureId): boolean {
  // Check base plan features
  const basePlanId = workspace.planOverrides?.basePlanId ?? workspace.subscriptionTier
  const baseHas = PLAN_FEATURES[basePlanId]?.includes(featureId) ?? false

  // Check additional features from overrides
  const additionalHas = workspace.planOverrides?.additionalFeatures?.includes(featureId) ?? false

  return baseHas || additionalHas
}

Breaking change: planHasFeature currently takes a planId string. It needs to take a workspace (or at least the override data) instead. Every call site needs updating.

1d. Credit grant changes

In stripe-webhooks.ts and credit-balance.service.ts, replace:

const creditsPerSeat = plan.limits?.includedCredits || 0

with:

const effectivePlan = getEffectivePlan(workspace)
const creditsPerSeat = effectivePlan.limits?.includedCredits || 0

For unlimited credits (includedCredits: -1), skip credit deduction entirely in the consumption path.

1e. Webhook changes for custom Stripe products

The determineTierFromPriceId() function currently falls back to personal_standard for unknown price IDs. For custom enterprise Stripe products:

Option A (simpler): Store the mapping in plan_overrides:

interface PlanOverrides {
  ...
  stripePriceIds?: string[]  // Custom Stripe price IDs for this workspace
}

Then in the webhook, when getBillingPlanByPriceId() returns null, look up the workspace by stripeCustomerId and use its plan_overrides.basePlanId.

Option B: Use Stripe product metadata to store the base plan ID. Add obvious_base_plan: team_pro to Stripe product metadata, read it in the webhook.

Recommendation: Option A. Keeps the source of truth in our DB, not split across Stripe metadata.

1f. Skip-billing path for employee/gifted plans

For skipBilling: true workspaces:

  • Set subscriptionTier to the base plan (e.g., team_pro)
  • Set subscriptionStatus to active
  • Don't create a Stripe subscription
  • Grant credits via a new admin endpoint or cron job (not via Stripe webhooks)
  • For unlimited credits: set includedCredits: -1 and short-circuit the deduction path

New admin endpoint:

POST /admin/workspaces/:id/plan-overrides
Body: PlanOverrides
Auth: admin-only

Phase 2: Admin UI & Tooling

2a. Admin dashboard for managing custom plans

  • View all workspaces with custom plans
  • Create/edit plan overrides for a workspace
  • Grant one-time credit bonuses
  • View credit usage for custom-plan workspaces

2b. Employee plan provisioning

Automated flow:

  1. Employee signs up with @obvious.com email
  2. System auto-creates workspace with overrides:
    {
      "basePlanId": "team_pro",
      "includedCredits": -1,
      "creditPrice": 0,
      "skipBilling": true,
      "planLabel": "Employee Plan"
    }
  3. No Stripe subscription needed

2c. Enterprise deal provisioning

Sales workflow:

  1. Sales creates custom Stripe product/price for the customer
  2. Admin sets plan_overrides on the workspace with deal terms
  3. Customer subscribes via normal checkout (Stripe handles billing)
  4. Webhooks resolve the workspace via customer ID, use overrides for credit grants

Phase 3: Fully DB-Driven Plans (Future)

Move PLAN_METADATA and PLAN_FEATURES into the database entirely. This is a much larger effort and isn't needed for the immediate use cases. Phase 1 handles enterprise and employee plans without this.

Files That Need Changes

File Change Effort
apps/api/src/db/schema.ts Add plan_overrides, is_custom_plan columns S
apps/api/src/config/plan-utils.ts No changes (base plan types stay)
apps/api/src/config/billing.config.ts Add getEffectivePlan() resolver M
apps/api/src/config/plan-features.ts Update planHasFeature() to accept overrides M
apps/api/src/routes/stripe-webhooks.ts Use getEffectivePlan() for credit calcs, handle unknown price IDs M
apps/api/src/services/credit-balance.service.ts Handle unlimited credits (-1), use effective plan M
apps/api/src/services/seat-management.service.ts Respect seat limit overrides S
apps/api/src/routes/billing.ts Use getEffectivePlan() in status/checkout endpoints M
apps/api/src/routes/admin.ts (new) Admin endpoint for plan overrides M
dashboard/ (billing UI) Show custom plan label, hide upgrade CTAs for custom plans S

Concrete Use Cases

Use Case 1: Enterprise Deal — "Acme Corp"

{
  "basePlanId": "team_pro",
  "includedCredits": 500,
  "creditPrice": 0.70,
  "planLabel": "Acme Corp Enterprise",
  "additionalFeatures": ["infra_dedicated", "sla_custom"],
  "seatLimit": 50
}

Acme gets team_pro features + enterprise features, 500 credits/seat, discounted credit price, 50 seat cap. Stripe handles billing with a custom price.

Use Case 2: Employee Account

{
  "basePlanId": "team_pro",
  "includedCredits": -1,
  "creditPrice": 0,
  "skipBilling": true,
  "planLabel": "Employee Plan",
  "additionalFeatures": ["infra_dedicated"]
}

Employee gets team_pro + unlimited credits + no billing. Provisioned automatically on signup with @obvious.com email.

Use Case 3: Investor/Advisor Gift

{
  "basePlanId": "personal_pro",
  "includedCredits": 1000,
  "creditPrice": 0,
  "skipBilling": true,
  "planLabel": "Advisor Plan"
}

Personal pro with 1000 credits/month, no charge. Set manually by admin.

Migration Strategy

  1. Add plan_overrides and is_custom_plan columns (nullable, no breaking change)
  2. Introduce getEffectivePlan() — all existing workspaces return their base plan unchanged
  3. Update call sites one-by-one (feature gating, credit grants, webhooks)
  4. Build admin endpoint for setting overrides
  5. Test with one enterprise customer
  6. Build employee auto-provisioning

No existing functionality breaks at any step. Workspaces without overrides behave identically.

Open Questions

  1. Credit tracking for unlimited plans — Do we still track usage for unlimited credit plans (for analytics), or skip the ledger entirely?
  2. Override inheritance — If an enterprise customer upgrades their base Stripe subscription (team_standard → team_pro), should overrides carry over automatically?
  3. Audit trail — Do we need a log of who changed plan overrides and when?
  4. Dashboard visibility — Should custom plan workspaces see a different billing page (no "change plan" button)?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment