For the complete documentation index, see llms.txt. Prefer markdown by appending .md to documentation URLs or sending Accept: text/markdown.

Onboarding flow

Add a first-run experience to the TurboStarter browser extension - welcome state, web auth, and optional plan-gated empty screens.

Browser extensions should stay thin. TurboStarter already authenticates the extension against the web Better Auth session and shows plan badges from the same billing API. There is no built-in multi-step wizard in the extension - and you usually should not build a full one inside the popup.

This recipe covers a practical first-run path: welcome → sign in on the web → optional hard empty-state until the user has a paid plan.

TL;DR

  1. Detect first open with chrome.storage / browser.storage (or skip if session exists).
  2. Send unauthenticated users to the web /auth/login (existing pattern).
  3. Prefer completing rich onboarding on web; sync completion via the API/session.
  4. Soft monetization: show upgrade links to web pricing. Hard: block premium UI until getActivePlan() is paid.
  5. Never trust the extension alone - enforce plans on the API.

Web vs extension responsibilities

ConcernWeb appExtension
Multi-step profile / workspace wizardYes - web onboardingLink out or skip
AuthFull Better Auth UIOpens web login
CheckoutStripe / Lemon / Polar / …Link to web pricing or portal
First-run chromeDashboard onboarding routePopup / options empty state
Source of truthPostgres + sessionAPI summary + storage flags

If you already require onboarding completion on the web, the extension only needs to handle logged-out and logged-in-but-unpaid states.

When the popup opens:

  1. No session - show a short welcome and a Sign in button that opens web /auth/login.
  2. Session + soft mode - render the normal popup / sidepanel.
  3. Session + hard mode + Free plan - show a “Choose a plan” empty state that links to web /pricing or /dashboard/choose-plan.

Add a first-run flag (optional)

Use extension storage for UI that should show once per install (tooltips, permission explanations). Do not use it as the only “onboarding completed” signal if the same user might finish setup on the web first.

apps/extension/src/modules/onboarding/storage.ts
const KEY = "extension.firstRunSeen";

export const getFirstRunSeen = async () => {
  const result = await chrome.storage.local.get(KEY);
  return Boolean(result[KEY]);
};

export const setFirstRunSeen = async () => {
  await chrome.storage.local.set({ [KEY]: true });
};

Show a short welcome card in the popup when !session && !firstRunSeen, then set the flag when they dismiss or click Sign in.

Reuse web auth

The extension auth client already talks to the web app. Login UX should open the web login page (same as the user menu / e2e flows):

const loginUrl = `${webAppUrl}/auth/login`;
// chrome.tabs.create({ url: loginUrl }) or window.open in the popup

After login, the session cookie / token flow used by apps/extension/src/lib/auth hydrates authClient.useSession(). Prefer redirecting new users to web onboarding when you need profile data:

const loginUrl = `${webAppUrl}/auth/login?redirectTo=/dashboard/onboarding`;

Only build in-extension forms when the answer is extension-specific (e.g. “which sites to enable”).

Soft empty state for logged-out users

Keep the popup useful even before auth: brand, one sentence, primary CTA.

apps/extension/src/modules/onboarding/welcome.tsx
import { Button } from /* your extension UI */;

interface WelcomeProps {
  readonly onSignIn: () => void;
}

export const Welcome = ({ onSignIn }: WelcomeProps) => {
  return (
    <div className="flex flex-col gap-3 p-4">
      <h1 className="text-base font-semibold">Welcome to TurboStarter</h1>
      <p className="text-muted-foreground text-sm">
        Sign in to sync your account and unlock extension features.
      </p>
      <Button onClick={onSignIn}>Sign in</Button>
    </div>
  );
};

Render Welcome when !session; otherwise render the normal popup content (header already shows the user when signed in).

Optional: hard paywall empty state

Extensions rarely run native IAP. “Hard paywall” means: hide product UI until the billing summary shows a paid plan, and send the user to the web to checkout.

apps/extension/src/modules/onboarding/choose-plan.tsx
interface ChoosePlanProps {
  readonly pricingUrl: string;
}

export const ChoosePlanEmpty = ({ pricingUrl }: ChoosePlanProps) => {
  return (
    <div className="flex flex-col gap-3 p-4">
      <h1 className="text-base font-semibold">Subscription required</h1>
      <p className="text-muted-foreground text-sm">
        Choose a plan on the web to use this extension.
      </p>
      <a href={pricingUrl} target="_blank" rel="noreferrer">
        View plans
      </a>
    </div>
  );
};

Resolve the plan with the same summary query as feature-based access, then compose in the popup root:

import { BillingPlan, getActivePlan } from "@workspace/billing";

const summary = useQuery(billing.queries.summary.get(user.id));
const activePlan = getActivePlan(summary.data);

if (!session) {
  return <Welcome onSignIn={openLogin} />;
}

if (REQUIRE_PAID_PLAN && activePlan === BillingPlan.FREE) {
  return <ChoosePlanEmpty pricingUrl={pricingUrl} />;
}

return <PopupApp />;

Point pricingUrl at marketing /pricing or your web choose-plan route from the web onboarding recipe.

Prefer feature gates when only some actions are paid - hard-blocking the whole popup is for paid-only products.

Keep content scripts honest

Content scripts and background workers can call your API with the user’s session. Treat them like any other client:

  • Soft: degrade UI when Free.
  • Hard: do not inject premium behaviors until the plan check passes.
  • Always enforce on the server with the same middleware as web.

If onboarding answers live only on the web, fetch them via API when the extension needs them - do not re-collect in the popup.

Checklist

  • Logged-out popup has a clear Sign in path to the web app
  • Rich profile/workspace onboarding lives on web when possible
  • First-run storage is UX-only, not the security boundary
  • Hard mode links to web checkout; plan resolved with getActivePlan(summary)
  • API enforces paid features (feature-based access)

Other platforms

How is this guide?

Last updated on

On this page

Ship your startup everywhere. In minutes.Try TurboStarter