A visitor lands on your pricing page. They are interested. They click "Start free trial" and see a signup form.
Half of them close the tab.
Not because they did not want your product -- because they were not ready to hand over an email to something they had never actually used. Demo mode solves this. Instead of a friction wall, you offer a "Try the demo" button that drops the visitor directly into a working version of your app, no signup required. They explore, get value, and convert later with far less resistance.
Here is how to build it in a Next.js SaaS.
A demo account is a real user account with a fixed set of seeded data -- realistic records that make the product feel alive without exposing any real user data. The session is:
The simplest version -- a shared demo account with read-only middleware -- takes an afternoon to ship.
Free trials work when your product's value is obvious the moment you sign up. But if your product is visual, data-dependent, or complex -- a dashboard, a CRM, an analytics tool, a project manager -- visitors need to see realistic data before they trust the promise.
Demo mode moves the "aha moment" before the signup wall. Visitors experience the product, decide they want it, and sign up already motivated. That sequence converts better than asking for commitment upfront.
The tradeoff: you invest an afternoon seeding realistic data and wiring the demo login. For most B2B SaaS products, that pays back in the first month of signups.
The cleanest implementation uses the same JWT auth layer the Next.js SaaS Boilerplate already provides, with one addition: a demo role in the token payload.
When someone clicks "Try the demo":
{ role: 'demo', userId: '<demo-user-id>' }role flag and blocks mutating routes for demo sessionsEvery read-only feature works. Write actions show a polite "Sign up to save changes" prompt instead of silently failing.
For more on how the JWT layer is structured, see JWT auth in Next.js App Router without NextAuth.
Your demo is only as compelling as the data inside it. Empty tables do not show value -- realistic records do. Add a seed script that runs after migrations:
// db/seed-demo.ts
const DEMO_USER_ID = 'demo-user-fixed-id'
await db.insert(projectTable).values([
{ id: DEMO_PROJECT_ID, name: 'Q4 Campaign', userId: DEMO_USER_ID },
{ id: uuid(), name: 'Website Redesign', userId: DEMO_USER_ID },
])
// Keep this idempotent -- check if demo user exists before inserting
Run it with npx tsx db/seed-demo.ts after each migration. Keep it idempotent so re-runs on Vercel preview deploys do not duplicate records.
The guard is three lines in your existing middleware.ts:
const isDemo = payload?.role === 'demo'
const isMutation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method ?? '')
if (isDemo && isMutation) {
return NextResponse.json({ error: 'Sign up to save changes' }, { status: 403 })
}
On the client side, catch 403 responses and show a toast: "You are in demo mode -- [Sign up to continue]". That single prompt, shown at the exact moment a visitor wants to take action, is one of the highest-converting touchpoints in your entire funnel. The visitor is already sold -- they just tried to do the thing they came to do.
See user impersonation in Next.js for a related pattern using the same JWT-based identity switching.
The Next.js SaaS Boilerplate ships with:
Adding demo mode on top means one seed script, three lines in middleware, and one server action for the demo login button. No new dependencies, no separate infrastructure.
If your SaaS shows its value through data or a visual interface, demo mode is one of the highest-ROI changes you can make to your landing page. Most founders delay it because they assume it is complex. With the JWT and Drizzle layers already in place, it is a focused afternoon build.
Pick a realistic dataset that shows your product at its best. Wire the demo login server action. Add the "Sign up to save" prompt on mutations. Ship it.
Get the Next.js SaaS Boilerplate and have demo mode live before the end of the week.