From ae7cbe841a317410b92900164725ac22996b6b0e Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 27 May 2026 16:12:40 -0700 Subject: [PATCH] Add enterprise billing plan --- apps/cloud/autumn.config.ts | 15 ++ .../src/auth/organization-limits.node.test.ts | 3 + apps/cloud/src/org/member-limits.node.test.ts | 1 + apps/cloud/src/org/member-limits.ts | 1 + apps/cloud/src/routes/billing.tsx | 1 + apps/cloud/src/routes/billing_.plans.tsx | 133 +++++++++--------- apps/cloud/src/services/autumn-plans.ts | 4 +- autumn.config.ts | 19 +++ 8 files changed, 106 insertions(+), 71 deletions(-) diff --git a/apps/cloud/autumn.config.ts b/apps/cloud/autumn.config.ts index 79e07be51..7497a87f2 100644 --- a/apps/cloud/autumn.config.ts +++ b/apps/cloud/autumn.config.ts @@ -68,3 +68,18 @@ export const team = plan({ }), ], }); + +export const enterprise = plan({ + id: "enterprise", + name: "Enterprise", + items: [ + item({ + featureId: executions.id, + unlimited: true, + reset: { interval: "month" }, + }), + item({ + featureId: domainVerification.id, + }), + ], +}); diff --git a/apps/cloud/src/auth/organization-limits.node.test.ts b/apps/cloud/src/auth/organization-limits.node.test.ts index ceec9da42..b80ead2a5 100644 --- a/apps/cloud/src/auth/organization-limits.node.test.ts +++ b/apps/cloud/src/auth/organization-limits.node.test.ts @@ -11,6 +11,9 @@ describe("organization limits", () => { it("treats active and trialing paid org subscriptions as paid", () => { expect(hasPaidOrganizationSubscription([{ planId: "team", status: "active" }])).toBe(true); expect(hasPaidOrganizationSubscription([{ planId: "team", status: "trialing" }])).toBe(true); + expect(hasPaidOrganizationSubscription([{ planId: "enterprise", status: "active" }])).toBe( + true, + ); }); it("does not treat inactive paid plans or free plans as paid", () => { diff --git a/apps/cloud/src/org/member-limits.node.test.ts b/apps/cloud/src/org/member-limits.node.test.ts index 86f80fb8a..bc027bd17 100644 --- a/apps/cloud/src/org/member-limits.node.test.ts +++ b/apps/cloud/src/org/member-limits.node.test.ts @@ -33,6 +33,7 @@ describe("member limits", () => { it("resolves member limits from the selected plan", () => { expect(getMemberLimitForPlan("free")).toBe(3); expect(getMemberLimitForPlan("team")).toBeNull(); + expect(getMemberLimitForPlan("enterprise")).toBeNull(); expect(getMemberLimitForPlan("unknown")).toBe(3); }); }); diff --git a/apps/cloud/src/org/member-limits.ts b/apps/cloud/src/org/member-limits.ts index 4d04ff39e..8cff10069 100644 --- a/apps/cloud/src/org/member-limits.ts +++ b/apps/cloud/src/org/member-limits.ts @@ -4,6 +4,7 @@ const MEMBER_LIMITS: Record = { free: 3, "free-pay-as-you-go": 3, team: null, + enterprise: null, }; export const DEFAULT_MEMBER_LIMIT = 3; diff --git a/apps/cloud/src/routes/billing.tsx b/apps/cloud/src/routes/billing.tsx index ac4731671..7df812086 100644 --- a/apps/cloud/src/routes/billing.tsx +++ b/apps/cloud/src/routes/billing.tsx @@ -12,6 +12,7 @@ export const Route = createFileRoute("/billing")({ const PLAN_TAGLINES: Record = { free: "Free for up to 3 members", team: "$49 per organization", + enterprise: "Custom enterprise agreement", }; function BillingPage() { diff --git a/apps/cloud/src/routes/billing_.plans.tsx b/apps/cloud/src/routes/billing_.plans.tsx index 2f97428cd..4597217e0 100644 --- a/apps/cloud/src/routes/billing_.plans.tsx +++ b/apps/cloud/src/routes/billing_.plans.tsx @@ -20,6 +20,14 @@ export const Route = createFileRoute("/billing_/plans")({ component: PlansPage, }); +const ENTERPRISE_FEATURES = [ + "Self-hosted or dedicated cloud deployment support", + "SSO / SAML & SCIM provisioning", + "Audit logs for every tool call", + "Dedicated support & onboarding", + "Security reviews, DPA & SOC 2 on request", +]; + const PLAN_META: Record = { free: { tagline: "For small teams getting started", @@ -40,6 +48,11 @@ const PLAN_META: Record = { @@ -50,28 +63,7 @@ const ACTION_LABELS: Record = { purchase: "Purchase", }; -const ENTERPRISE_FEATURES = [ - "Self-hosted or dedicated cloud deployment support", - "SSO / SAML & SCIM provisioning", - "Audit logs for every tool call", - "Dedicated support & onboarding", - "Security reviews, DPA & SOC 2 on request", -]; - -const ENTERPRISE_MAILTO = `mailto:rhys@executor.sh?subject=${encodeURIComponent( - "Executor Enterprise inquiry", -)}&body=${encodeURIComponent( - [ - "Hi,", - "", - "We're interested in Executor Enterprise.", - "", - "Company:", - "Team size:", - "Use case:", - "Requirements (SSO, self-hosted, compliance, etc.):", - ].join("\n"), -)}`; +const PLAN_ORDER = ["free", "team", "enterprise"]; function PlansPage() { const { attach, openCustomerPortal, isLoading: customerLoading } = useCustomer(); @@ -80,8 +72,8 @@ function PlansPage() { const isLoading = customerLoading || plansLoading; - const selfServePlans = (plans ?? ([] as Plan[])).filter( - (p: Plan) => p.id === "free" || p.id === "team", + const selfServePlans = PLAN_ORDER.flatMap((id) => + (plans ?? ([] as Plan[])).filter((plan: Plan) => plan.id === id), ); return ( @@ -136,6 +128,7 @@ function PlansPage() { const isScheduled = status === "scheduled"; const label = isCanceling ? "Resume" : (ACTION_LABELS[action] ?? "Select"); const isUpgradeAction = action === "upgrade" || action === "activate"; + const isEnterprise = plan.id === "enterprise"; return (
- ${plan.price?.amount ?? 0} + {plan.id === "enterprise" ? "Custom" : `$${plan.price?.amount ?? 0}`} - {plan.price?.interval && ( + {plan.id !== "enterprise" && plan.price?.interval && ( USD / org / {plan.price.interval} )} - {!plan.price?.interval && ( + {plan.id !== "enterprise" && !plan.price?.interval && ( USD )}
@@ -190,6 +183,8 @@ function PlansPage() {
{isCurrent ? "Current plan" : "Scheduled"}
+ ) : isEnterprise ? ( + ) : isCanceling ? ( + + + + Talk to us about Enterprise + + Add{" "} + + rhys@executor.sh + {" "} + on Slack Connect and send the org name, team size, and anything you need covered. + + + + + + + + + + + ); +} + function SlackContactCta() { const [open, setOpen] = useState(false); diff --git a/apps/cloud/src/services/autumn-plans.ts b/apps/cloud/src/services/autumn-plans.ts index 6250da651..9e112a945 100644 --- a/apps/cloud/src/services/autumn-plans.ts +++ b/apps/cloud/src/services/autumn-plans.ts @@ -1,5 +1,5 @@ -import { team } from "../../autumn.config"; +import { enterprise, team } from "../../autumn.config"; -export const PAID_AUTUMN_PLAN_IDS = new Set([team.id]); +export const PAID_AUTUMN_PLAN_IDS = new Set([team.id, enterprise.id]); export const ACTIVE_AUTUMN_SUBSCRIPTION_STATUSES = new Set(["active", "trialing"]); diff --git a/autumn.config.ts b/autumn.config.ts index b10914a49..31b3774c3 100644 --- a/autumn.config.ts +++ b/autumn.config.ts @@ -83,3 +83,22 @@ export const team = plan({ }), ], }); + +export const enterprise = plan({ + id: "enterprise", + name: "Enterprise", + addOn: false, + autoEnable: false, + items: [ + item({ + featureId: executions.id, + unlimited: true, + reset: { + interval: "month", + }, + }), + item({ + featureId: domainVerification.id, + }), + ], +});