Run it locally / self-host
Boot the api and dashboard on your own machine and finish a real signup → gated-route loop with no external inbox — the four env details that trip up a fresh cold start.
Everywhere else in these docs, "sign up" means the hosted dashboard at app.vibemonetize.dev and "the API" means api.vibemonetize.dev. This page is for the other case: you're running apps/api (and optionally apps/dashboard) yourself — local evaluation, a self-hosted deploy, or an autonomous coding agent driving the whole loop with no human inbox to read a verification code from. Four env details decide whether that cold start works on the first try or silently stalls. Get these right and you can go from a fresh checkout to a real signup → gated route in a few minutes, entirely from a terminal.
1. Boot the api with NODE_ENV unset — this is what makes signup work with no inbox
Production developer signup (POST /v1/auth/signup/start / POST /v1/auth/signup/verify) emails a 6-digit code. There's no dashboard, no test mode, no flag that changes this on the hosted API — but a self-hosted api that (a) has no RESEND_API_KEY configured, so it's already logging codes to its own console instead of emailing them, and (b) is not running a production build, echoes that same code back in the JSON response as a devCode field. npx vibemonetize signup reads it automatically and prints the exact follow-up command. The same double-gated echo also covers end-user email sign-in (POST /v1/end-users/identify) for apps that gate on verified email.
The one thing that silently breaks this: booting the api with NODE_ENV=production. There's no error — devCode just stops appearing in the response, exactly as if you were hitting the hosted production API, and signup looks hung waiting on an inbox that will never receive anything. This is the exact trap that blocked an autonomous cold-start run end to end (it re-completed the instant the api was restarted with NODE_ENV unset). If you're scripting this — CI, an agent harness, a Dockerfile — make sure nothing upstream is exporting NODE_ENV=production into the api's environment.
cd apps/api
cp ../../.env.example .env.local # fill in DATABASE_URL + STRIPE_* (test keys only)
pnpm keys:generate -- --write # generates the ES256 entitlement-JWT keypair
pnpm db:migrate # applies db/migrations/ to DATABASE_URL
# Do NOT set NODE_ENV=production here — that's what disables the devCode echo above.
unset NODE_ENV # confirm it isn't already exported by your shell/CI env
pnpm dev # runs the api on :8787 (see the port checklist below)Then sign up with zero inbox involvement:
npx vibemonetize signup --email you@example.com --api-url http://localhost:8787
# Verification code sent to you@example.com (valid 10 minutes, 3 attempts).
# Local/self-hosted api with no email provider configured — dev-only code: 482913
# Finish with:
# vibemonetize signup --email you@example.com --code 482913
npx vibemonetize signup --email you@example.com --code 482913
# → prints your fresh sk_test_/pk_test_ pair (RFC 012: signup always mints test keys)devCode is never present against the hosted production API — this whole affordance only exists on an api instance you're running yourself with no email provider wired up.
2. ENTITLEMENT_JWT_ISSUER must equal the API base URL
@vibemonetize/server's requireEntitlement verifies the entitlement token locally against the api's published JWKS, and by default requires the token's iss claim to exactly equal the apiUrl you passed to createClient(). The api stamps iss from its own ENTITLEMENT_JWT_ISSUER env var at signing time — if that string doesn't match the URL your SDKs are pointed at (scheme, host, and port), every server-side check fails closed with an issuer-mismatch error, even though the signature and every other claim are valid.
# apps/api/.env.local
ENTITLEMENT_JWT_ISSUER=http://localhost:8787 # exactly the URL your SDKs use as apiUrl — no trailing slash,
# no swapped-in hostname like "api.local.<yourdomain>"Env is read once at process boot, so change it and restart the api, not just edit-and-save. If you do get this wrong, the error now names both sides so it's fixable without reading SDK source:
Invalid entitlement token: issuer mismatch — the token's `iss` is "http://localhost:8787" but this
client requires "http://api.local.example". Point the api's ENTITLEMENT_JWT_ISSUER at your API base URL
(@vibemonetize/server verifies `iss === apiUrl` by default), or pass a matching `issuer` to createClient().3. NEXT_PUBLIC_DASHBOARD_URL for local CTAs
Every "Sign up" / "Open your developer dashboard" link across this docs site is built from one origin (src/lib/signup-link.ts), which defaults to the hosted dashboard — https://app.vibemonetize.dev. That's the right default for real visitors, but a dead end if you're running this docs site locally against a self-hosted dashboard: clicking the CTA bounces you to prod, where your local api/account don't exist. If you're also running apps/dashboard locally, point the docs site at it instead:
# apps/docs/.env.local
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3000 # your local apps/dashboard originThis only affects the docs site's own CTAs — it has nothing to do with the CLI loop in section 1, which never touches the dashboard at all. Skip it entirely if you're only running the api and driving everything from npx vibemonetize.
4. Port checklist
None of the local dev servers hardcode a shared port on purpose — each is independently configurable — but these are what a fresh checkout gives you with no flags:
| App | Default local port | Set with |
|---|---|---|
apps/api | 8787 | PORT env var (see apps/api/README.md / repo-root .env.example) |
apps/dashboard | 3000 | plain Next.js default (next dev); override with next dev -p <port> |
apps/docs (this site) | 3007 | pinned in its own package.json's dev script |
apps/store | 3006 | pinned in its own package.json's dev script |
Whatever port the api actually listens on, three other things must agree with it: the ENTITLEMENT_JWT_ISSUER above, every SDK's apiUrl (NEXT_PUBLIC_VIBEMONETIZE_API_URL / VITE_VIBEMONETIZE_API_URL / VIBEMONETIZE_API_URL), and the --api-url flag (or VIBEMONETIZE_API_URL env var) you pass to the CLI. Running two local stacks at once (e.g. a second worktree)? Pick a free high port for the second one and thread that same value through all three — don't assume the numbers above are reserved for you specifically.
Put it together: cold start to a gated route
With section 1's api running and keys in hand, the rest is the same loop as the quickstart, run entirely against your local api:
# From your app's project directory (Next.js App Router or Vite — either works)
export VIBEMONETIZE_API_URL=http://localhost:8787
export VIBEMONETIZE_SECRET_KEY=sk_test_... # from step 1's signup output
npx vibemonetize init # wraps your app in <MonetizeProvider>, writes .env.local
npx vibemonetize app create --template saas # Free + Pro plan, writes the app id + publishable key
npx vibemonetize grant --email you@example.com --plan pro # 10-second unlock, no Stripe checkout needed
npx vibemonetize doctor # exits 0 only once the gate actually verifies server-sideGate one feature per the paywall guide (client <Paywall> + server-side requireEntitlement), sign in as you@example.com in your app, and watch the gate open. vibemonetize doctor is the actual proof — the client paywall fails open by design, so a rendered page proves nothing on its own. For the real end-to-end path (Stripe test checkout instead of a comped grant), click through <CheckoutButton> with test card 4242 4242 4242 4242 once the webhook secret from step 1's .env.local is wired up.