Walkthrough

Create a test organization in one keystroke

PR #5214 — an internal-only command for spinning up throwaway orgs

PR #5214·2026-06-30·✓ QA-verified end-to-end

Testing a feature in a clean org used to mean clicking through signup, onboarding, billing, and a feature-flag flip before you could even start. This PR collapses all of that into a single command-menu item — Create test organization — that an internal user can trigger from anywhere in the app. One keystroke produces a fresh org with sane defaults and drops you straight into it. The immediate driver is exercising the new MotherDuck warehouse-provisioning flow in a clean org; the longer-term win is cheap, repeatable ephemeral orgs for reproducing bugs and trying features.

Step 1 · The entry point

An internal-only command, gated behind useIsInternalUser

Open the command menu in any org and search “test”. Internal (@basedash.com) users see a new Create test organization item under the Organization group, sitting right next to the existing internal-only “SQL playground” entry it mirrors.

Selecting it doesn’t open a form or a wizard. It fires a single POST to a new action route and closes the menu — the server does the rest and navigates you into the result.

An internal-only command, gated behind useIsInternalUser
The command menu for an internal user, filtered to “create test org”.
app/routes/_app.orgs.$orgSlug/OrganizationCommandItems.tsx ↗ view in PR
onSelect={() => {
  // Navigation submit (not a fetcher): closeCommandMenu unmounts
  // this component, which would cancel an in-flight fetcher before
  // the action's redirect lands. A navigation submit is owned by
  // the router and survives the unmount.
  void submit(null, {
    method: 'post',
    action: '/api/create-test-org',
  });
  closeCommandMenu();
}}
Step 2 · One action, a ready org

A fresh org with sane defaults — and no wizard

The action route generates a memorable, slug-safe name (test-clever-gecko-49), runs it through createUniqueOrgSlug, and calls the shared createOrganization with the current user as ADMIN, onboarding marked COMPLETED, and isBillable: false so there are no Stripe side effects.

It then puts the org on an active GROWTH plan with a long trial and enables the MOTHERDUCK_WAREHOUSE flag in a single transaction — so feature gates pass without manual flips and the MotherDuck connect flow works immediately. Finally it redirects to /orgs/<slug>. You land in a fully usable org, ready to connect a data source — not the onboarding wizard.

A fresh org with sane defaults — and no wizard
Landing in the freshly created org — usable immediately, prompting to connect a data source.
app/routes/api.create-test-org.ts ↗ view in PR
const organization = await createOrganization({
  prisma: context.prisma,
  slug,
  email: user.email,
  userId: user.id,
  name,
  companySize: null,
  onboardingStatus: 'COMPLETED', // skip the wizard
  isBillable: false,             // no billing side effects
});

await context.prisma.$transaction([
  context.prisma.organization.update({
    where: { id: organization.id },
    data: { plan: Plan.GROWTH, trialEndsAt },
  }),
  context.prisma.organizationFeatureFlag.create({
    data: { id: generateIdWithPrefix('flag'), organizationId: organization.id,
            featureFlag: 'MOTHERDUCK_WAREHOUSE', enabled: true },
  }),
]);

return redirect(`/orgs/${organization.slug}`);
Step 3 · Gated where it counts

Non-internal users never see it — and can’t reach it

The same command menu, for a non-internal user (agent@example.com), returns No results found — the item simply isn’t rendered.

But client gating isn’t the security boundary. The action route re-checks isVerifiedInternalUser server-side and returns 403 for anyone who isn’t a verified @basedash.com user, exactly like the existing admin.* routes. It also returns 403 on self-hosted and 401 when unauthenticated. An integration test covers all four paths.

Non-internal users never see it — and can’t reach it
The identical search for a non-internal user — the item is absent.
app/routes/api.create-test-org.ts ↗ view in PR
if (isDeployedAsSelfHosted()) {
  throw new Response('Forbidden', { status: 403 });
}
if (!context.userId) {
  throw new Response('Unauthorized', { status: 401 });
}

const user = await context.prisma.user.findUnique({
  where: { id: context.userId },
});
if (!user || !isVerifiedInternalUser(user.email, user.isEmailVerified)) {
  throw new Response('Forbidden', { status: 403 });
}
Step 4 · What the QA run caught

A redirect that got cancelled — fixed before merge

The screenshots above come from a real end-to-end run, and that run surfaced a genuine bug. The first implementation used a useFetcher to POST the action. Clicking the item did create the org — but the browser never navigated into it.

The cause: closeCommandMenu() unmounts the component that owns the fetcher, cancelling the in-flight submission before React Router applies the action’s redirect. The fix is to use a navigation submit instead — it’s owned by the router, survives the unmount, and carries the redirect through. The org-creation backend was already correct; only the client navigation was dropping. Worth confirming with a manual click-through that the redirect lands every time.