From 20c67445683ae3aa06434572939d37052caaccde Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 1 Jul 2026 18:27:33 +0100 Subject: [PATCH 1/2] fix(webapp): activate managed-cloud orgs via select-plan, not at creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New managed-cloud orgs were created already activated, so they skipped the select-plan flow that provisions their billing entitlement — leaving the free-tier usage cap unenforced. Create managed-cloud orgs deactivated so they're routed through select-plan, which activates them once a plan is selected. Self-hosters have no billing gate and remain active immediately. Rename the Organization.v3Enabled Prisma field to isActivated (mapped to the existing v3Enabled column, so the database and billing service are unchanged) to better reflect what the flag now gates. --- .server-changes/fix-managed-cloud-org-activation.md | 6 ++++++ apps/webapp/app/models/admin.server.ts | 2 +- apps/webapp/app/models/organization.server.ts | 7 +++++-- apps/webapp/app/models/project.server.ts | 4 ++-- .../_app.orgs.$organizationSlug_.projects.new/route.tsx | 8 ++++---- .../_app.orgs.$organizationSlug_.select-plan/route.tsx | 2 +- apps/webapp/test/helpers/seedTestEnvironment.ts | 2 +- apps/webapp/test/helpers/seedTestUserProject.ts | 2 +- apps/webapp/test/member.server.test.ts | 2 +- internal-packages/database/prisma/schema.prisma | 2 +- 10 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 .server-changes/fix-managed-cloud-org-activation.md diff --git a/.server-changes/fix-managed-cloud-org-activation.md b/.server-changes/fix-managed-cloud-org-activation.md new file mode 100644 index 00000000000..d9503c573e0 --- /dev/null +++ b/.server-changes/fix-managed-cloud-org-activation.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +New managed-cloud orgs were created already activated, so they skipped the select-plan flow that provisions their billing entitlement — leaving the free-tier usage cap unenforced. Managed-cloud orgs are now created deactivated and routed through select-plan, which activates them once a plan is selected. Self-hosters have no billing gate and remain active immediately. diff --git a/apps/webapp/app/models/admin.server.ts b/apps/webapp/app/models/admin.server.ts index 4963a0befc2..406845e5859 100644 --- a/apps/webapp/app/models/admin.server.ts +++ b/apps/webapp/app/models/admin.server.ts @@ -131,7 +131,7 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se slug: true, title: true, v2Enabled: true, - v3Enabled: true, + isActivated: true, deletedAt: true, members: { select: { diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index 79c1ef1b261..a4b8a8ab5f5 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -86,7 +86,7 @@ export async function createOrganization( ); } - const _features = featuresForUrl(new URL(env.APP_ORIGIN)); + const features = featuresForUrl(new URL(env.APP_ORIGIN)); const organization = await prisma.organization.create({ data: { @@ -102,7 +102,10 @@ export async function createOrganization( role: "ADMIN", }, }, - v3Enabled: true, + // Managed-cloud orgs start deactivated so they're routed through + // select-plan, which provisions their billing entitlement and activates + // them. Self-hosters have no billing gate, so they're active immediately. + isActivated: !features.isManagedCloud, }, include: { members: true, diff --git a/apps/webapp/app/models/project.server.ts b/apps/webapp/app/models/project.server.ts index 5f07d4d28cf..a9feb394754 100644 --- a/apps/webapp/app/models/project.server.ts +++ b/apps/webapp/app/models/project.server.ts @@ -32,7 +32,7 @@ export async function createProject( select: { id: true, slug: true, - v3Enabled: true, + isActivated: true, maximumConcurrencyLimit: true, maximumProjectCount: true, }, @@ -49,7 +49,7 @@ export async function createProject( } if (version === "v3") { - if (!organization.v3Enabled) { + if (!organization.isActivated) { throw new Error(`Organization can't create v3 projects.`); } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index edc625f128e..b5b97a0bb69 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -118,7 +118,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { select: { id: true, title: true, - v3Enabled: true, + isActivated: true, v2Enabled: true, hasRequestedV3: true, _count: { @@ -138,7 +138,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } const { isManagedCloud } = featuresForRequest(request); - if (isManagedCloud && !organization.v3Enabled) { + if (isManagedCloud && !organization.isActivated) { return redirect(selectPlanPath({ slug: organizationSlug })); } @@ -151,7 +151,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { title: organization.title, slug: organizationSlug, projectsCount: organization._count.projects, - v3Enabled: organization.v3Enabled, + isActivated: organization.isActivated, v2Enabled: organization.v2Enabled, hasRequestedV3: organization.hasRequestedV3, }, @@ -324,7 +324,7 @@ export default function Page() { const { organization, message } = useTypedLoaderData(); const lastSubmission = useActionData(); - const canCreateV3Projects = organization.v3Enabled; + const canCreateV3Projects = organization.isActivated; const [form, { projectName, projectVersion }] = useForm({ id: "create-project", diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx index cc93b7bc2df..b613693f38f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx @@ -43,7 +43,7 @@ export const loader = dashboardLoader( throw new Response(null, { status: 404, statusText: "Organization not found" }); } - if (organization.v3Enabled) { + if (organization.isActivated) { return redirect(organizationPath({ slug: organizationSlug })); } diff --git a/apps/webapp/test/helpers/seedTestEnvironment.ts b/apps/webapp/test/helpers/seedTestEnvironment.ts index beca226134c..0f7b1a90718 100644 --- a/apps/webapp/test/helpers/seedTestEnvironment.ts +++ b/apps/webapp/test/helpers/seedTestEnvironment.ts @@ -16,7 +16,7 @@ export async function seedTestEnvironment(prisma: PrismaClient) { data: { title: `e2e-test-org-${suffix}`, slug: `e2e-org-${suffix}`, - v3Enabled: true, + isActivated: true, }, }); diff --git a/apps/webapp/test/helpers/seedTestUserProject.ts b/apps/webapp/test/helpers/seedTestUserProject.ts index cf8ae862613..cfd3caf28b2 100644 --- a/apps/webapp/test/helpers/seedTestUserProject.ts +++ b/apps/webapp/test/helpers/seedTestUserProject.ts @@ -35,7 +35,7 @@ export async function seedTestUserProject( data: { title: `e2e-pat-org-${suffix}`, slug: `e2e-pat-org-${suffix}`, - v3Enabled: true, + isActivated: true, members: { create: { userId: user.id, role: "ADMIN" } }, }, }); diff --git a/apps/webapp/test/member.server.test.ts b/apps/webapp/test/member.server.test.ts index b45dd48c583..8d4a9128642 100644 --- a/apps/webapp/test/member.server.test.ts +++ b/apps/webapp/test/member.server.test.ts @@ -59,7 +59,7 @@ async function seedInviteFixture( data: { title: `invite-org-${suffix}`, slug: `invite-org-${suffix}`, - v3Enabled: true, + isActivated: true, members: { create: { userId: inviter.id, role: "ADMIN" } }, }, }); diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 9df41bcffd0..7cf441eb9f8 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -212,7 +212,7 @@ model Organization { runsEnabled Boolean @default(true) - v3Enabled Boolean @default(false) + isActivated Boolean @default(false) @map("v3Enabled") /// @deprecated v2Enabled Boolean @default(false) From 3cfc8914561ac25e963260c06e0891c5b04629c2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 2 Jul 2026 10:07:25 +0100 Subject: [PATCH 2/2] feat(webapp): let users select the Free plan without GitHub verification Selecting Free now provisions the plan immediately instead of routing through a GitHub connect/verification step. Remove the 'Unlock Free plan' / 'Connect to GitHub' dialog, the GitHub-verified badge, and the account- rejected state from the plan picker, simplify the setPlan free-result handling to a success redirect, and delete the now-unreachable free-connect return routes. The billing service provisions the free plan directly. --- .../fix-managed-cloud-org-activation.md | 2 +- .../route.tsx | 39 -- .../route.tsx | 44 -- .../route.tsx | 1 - ...ces.orgs.$organizationSlug.select-plan.tsx | 393 ++++++------------ .../webapp/app/services/platform.v3.server.ts | 21 +- 6 files changed, 140 insertions(+), 360 deletions(-) delete mode 100644 apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx delete mode 100644 apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx diff --git a/.server-changes/fix-managed-cloud-org-activation.md b/.server-changes/fix-managed-cloud-org-activation.md index d9503c573e0..e9f395ffc66 100644 --- a/.server-changes/fix-managed-cloud-org-activation.md +++ b/.server-changes/fix-managed-cloud-org-activation.md @@ -3,4 +3,4 @@ area: webapp type: fix --- -New managed-cloud orgs were created already activated, so they skipped the select-plan flow that provisions their billing entitlement — leaving the free-tier usage cap unenforced. Managed-cloud orgs are now created deactivated and routed through select-plan, which activates them once a plan is selected. Self-hosters have no billing gate and remain active immediately. +Select plan flow when creating an org. Don't require GitHub verification to access the free plan. diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx deleted file mode 100644 index 941f7e4d9c5..00000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithErrorMessage } from "~/models/message.server"; -import { newProjectPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findFirst({ - select: { - slug: true, - _count: { - select: { - projects: true, - }, - }, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - const url = new URL(request.url); - const searchParams = new URLSearchParams(url.search); - const reason = searchParams.get("reason"); - - let errorMessage = reason ? decodeURIComponent(reason) : "Failed to verify your GitHub account"; - - return redirectWithErrorMessage(newProjectPath({ slug: org.slug }), request, errorMessage); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx deleted file mode 100644 index fb7a005061a..00000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { redirect } from "remix-typedjson"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithSuccessMessage } from "~/models/message.server"; -import { newProjectPath, v3BillingPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findFirst({ - select: { - slug: true, - _count: { - select: { - projects: true, - }, - }, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - const hasProject = org._count.projects > 0; - - if (hasProject) { - return redirectWithSuccessMessage( - v3BillingPath({ slug: org.slug }), - request, - "Free tier unlocked successfully." - ); - } - - return redirect(newProjectPath({ slug: org.slug }, "You're on the Free plan.")); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx index b613693f38f..f5658bc4d8b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx @@ -73,7 +73,6 @@ export default function ChoosePlanPage() { subscription={v3Subscription} organizationSlug={organizationSlug} hasPromotedPlan - showGithubVerificationBadge periodEnd={periodEnd} /> diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index b87c1bfc4c1..02bd91cb9d5 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -1,13 +1,7 @@ -import { - CheckIcon, - ExclamationTriangleIcon, - ShieldCheckIcon, - XMarkIcon, -} from "@heroicons/react/20/solid"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { Form, useLocation, useNavigation } from "@remix-run/react"; import { uiComponent } from "@team-plain/typescript-sdk"; -import { GitHubLightIcon } from "@trigger.dev/companyicons"; import { type AddOnPricing, type FreePlanDefinition, @@ -36,7 +30,6 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { Spinner } from "~/components/primitives/Spinner"; import { TextArea } from "~/components/primitives/TextArea"; import { TextLink } from "~/components/primitives/TextLink"; -import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { prisma } from "~/db.server"; import { redirectWithErrorMessage } from "~/models/message.server"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; @@ -171,10 +164,6 @@ const pricingDefinitions = { title: "Usage", content: "The compute cost when tasks are executing.", }, - freeUsage: { - title: "Free usage", - content: "Requires a verified GitHub account.", - }, concurrentRuns: { title: "Concurrent runs", content: "The number of runs that can be executed at the same time.", @@ -255,7 +244,6 @@ type PricingPlansProps = { subscription?: SubscriptionResult; organizationSlug: string; hasPromotedPlan: boolean; - showGithubVerificationBadge?: boolean; periodEnd: Date; }; @@ -265,7 +253,6 @@ export function PricingPlans({ subscription, organizationSlug, hasPromotedPlan, - showGithubVerificationBadge, periodEnd, }: PricingPlansProps) { return ( @@ -275,7 +262,6 @@ export function PricingPlans({ plan={plans.free} subscription={subscription} organizationSlug={organizationSlug} - showGithubVerificationBadge={showGithubVerificationBadge} periodEnd={periodEnd} /> { setIsDialogOpen(false); @@ -325,253 +308,145 @@ export function TierFree({ return ( -
- - {showGithubVerificationBadge && status === "approved" && ( - - - GitHub verified -
- } - content={ -
-
- - - verified - -
- - You have connected a verified GitHub account. This is required for the Free plan - to prevent malicious use of our platform. + + {subscription?.plan !== undefined && + subscription.plan.type !== "free" && + subscription.canceledAt === undefined ? ( + + +
+ +
+
+ +
+ + + Downgrade plan? +
+ + + Are you sure you want to downgrade? You will lose access to your current plan's + features on{" "} + .
- } - /> - )} -
- {status === "rejected" ? ( -
-
- - - Your Trigger.dev account failed to be verified for the Free plan because your GitHub - account is too new. We require verification to prevent malicious use of our platform. - - - You can still select a paid plan to continue or if you think this is a mistake,{" "} - - get in touch - - } - /> - . - -
-
- ) : ( - <> - {status === "requires_connect" ? ( - - -
- +
+
+ Why are you thinking of downgrading? +
    + {[ + "The Free plan is all I need", + "Subscription or usage costs too expensive", + "Bugs or technical issues", + "No longer need the service", + "Found a better alternative", + "Lacking features I need", + ].map((label, index) => ( +
  • + { + if (label === "Lacking features I need") { + setIsLackingFeaturesChecked(isChecked); + } + }} + /> +
  • + ))} +
- - - - - - Unlock the Free plan -
- - - To unlock the Free plan, we need to verify that you have an active GitHub - account. - - - We do this to prevent malicious use of our platform. We only ask for the - minimum permissions to verify your account. - -
- - - - -
-
- ) : subscription?.plan !== undefined && - subscription.plan.type !== "free" && - subscription.canceledAt === undefined ? ( - - -
- +
+ + {isLackingFeaturesChecked + ? "What features do you need? Or how can we improve?" + : "What can we do to improve?"} + +