BuildMay 19, 20264 min read

Building demo mode for a real app, without a database

How Pickleloonies' /demo route lets visitors try the full app — RSVPs, chat, payments — with zero backend writes.

by VincentAI-drafted, edited by Vincent
Two pickleball paddles and three balls resting on a court
Photo by Alex Saks on Unsplash

A landing-page video isn't a demo. A read-only screenshot tour isn't a demo. A real demo lets a stranger drive the app — RSVP, chat, send a payment — and feel what it's like. That's what /demo is for. Here's how it works without a single database write.

Why a demo matters more than a video

People decide whether to use a niche group app in about ninety seconds. A video forces them to watch passively for those ninety seconds. A demo lets them poke around, find the thing that matters to them, and click it. The conversion gap is huge.

But demos have a real cost. They invite spam. They pollute analytics. They risk leaking data if a stranger somehow gets attached to a real account.

Demo mode at Pickleloonies sidesteps all of that by being entirely client-side.

DemoContext and the static dataset

The dataset is a single TypeScript module — a few hundred lines of typed objects modeling a fake pod ("The Loonies"), eight fake members, three fake sessions, a small chat thread, and one open wager. Each entity matches the production type exactly, so the same components render against it without any branching at the UI layer.

That dataset is loaded into a React Context (DemoContext) the first time a visitor lands on /demo. They pick a persona from a user picker (Alex the admin, Jamie the casual, etc.), and currentUser is set in context.

Every RPC function checks: am I in demo mode? If yes, mutate the in-memory copy and return the new value. If no, hit Supabase. The components don't know the difference.

Pickleloonies' proxy middleware redirects unauthenticated users to /login. Demo visitors are unauthenticated — they don't have a session cookie. So how do they get past?

A small cookie, demo_mode=true, gets set when they click "Try the demo" on the landing page. The middleware reads it and lets them pass into the app shell. Critically, the cookie says nothing about who they are — it's a gate, not an identity.

The actual user identity lives in React state. The cookie is dumb. The state is smart. This separation matters because it means a leaked or stolen demo_mode cookie does nothing — there's no demo user to impersonate.

Things that broke

The biggest landmine: router.push() between routes destroyed demo state. The cookie kept the visitor in, but the React tree remounted, the context re-initialized, and currentUser reverted to null. The (app)/layout.tsx guard kicked them back to the user picker.

Fix: don't use full navigation inside demo mode. Use client-side Link clicks that preserve the React tree. Wait — those are already client-side. The actual bug was that one route was using router.push() programmatically after a button click, and that pattern was the culprit. We rewrote those handlers to use anchor clicks.

Lesson saved to the project's lessons file: when automating the in-app demo flow, never use page.goto() between routes. Use client-side navigation only.

What's still rough

The demo seed is small. Eight members, three sessions, one wager. A real pod has hundreds of RSVPs and thousands of chat messages over a year. The demo can't show that scale, which means it can't show the value of attendance stats over time.

The fix is a bigger seeded dataset, generated programmatically (random RSVP history across 52 weeks). I haven't done it because the eight-member version converts well enough, and bigger seeds slow down the initial load.

If you want to see it in action, open the demo. And if you want to know more about the team that builds this stuff, the about page is the place.

Frequently asked questions

+Why use a cookie if the state lives in memory?

The cookie tells the proxy middleware to let unauthenticated visitors past the /login gate. The cookie doesn't carry user data — that lives in React state once you pick a demo persona.

+Can I clear demo data?

Reload the page. There's no persistence by design — demo mode resets every time the React tree mounts.

+Does demo mode affect production data?

No. The DemoContext branch in every RPC short-circuits before any Supabase write. The demo never touches the database.

+Why not use mock service worker or a fake backend?

Both add a layer. A static dataset + context is simpler, the bundle stays small, and the failure modes are obvious — if state resets, you reloaded.