Demo mode for a SaaS app: sample data users can leave
How I built demo mode for a SaaS app: a sample data workspace, view as personas for each role, one request header, and exit paths that always land users home.
By Hamza Ghouri
A new account in a business app is mostly empty screens. Dashboards with no numbers, lists with no rows, reports with nothing to report. A demo mode for a SaaS app fixes that by letting people try every screen on realistic sample data before they set up their own. I recently added one to a B2B app that companies run across several sites: a separate demo workspace that a real company account can enter, explore as different roles and leave again. This post covers the API shape, the "view as" personas, what the demo refuses to do, and the exit paths, which took the most care.
Why a demo mode for a SaaS app beats an empty screen
Nielsen Norman Group's article on designing empty states in complex applications notes that empty states are common during onboarding in apps the user has not configured yet, and that a screen left blank causes confusion and lowers confidence. Good empty states explain what will appear there and link to the first task.
A demo workspace goes one step further. Instead of describing a filled dashboard, it shows one. My view is that this matters most for apps with several roles, because a company admin signing up cannot easily picture what a technician or a site manager will see. A demo can show them.
I have no conversion numbers to share on this one, so I'll stick to how it is built.
What is a demo mode in a SaaS app?
A demo mode is a separate workspace filled with sample data that a real account can enter and leave at will. Every screen works as usual, but reads and writes go to the demo data instead of the account's own. Leaving it returns the user to their real workspace with nothing mixed in.
The API: three verbs and one header
The backend side is small. One resource represents the account's demo:
GET /me/demoreads whether a demo exists and what is in it.POST /me/democreates it and enters it.DELETE /me/demoremoves it.
Once the app is in demo mode, every request carries a header with the active demo user's id. The backend applies it on workspace routes and ignores it elsewhere, so things like the profile and subscription screens keep showing the real account. The app adds the header in one HTTP interceptor, so no screen has to know demo mode exists. Trimmed and renamed, the Dio interceptor looks like this:
class DemoHeaderInterceptor extends Interceptor {
DemoHeaderInterceptor({required this.personaId, required this.onInvalid});
final String? Function() personaId;
final void Function() onInvalid;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final id = personaId();
if (id != null) options.headers['X-Demo-Persona'] = id;
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final data = err.response?.data;
if (personaId() != null && data is Map && data['code'] == 'demo_persona_invalid') {
onInvalid();
}
handler.next(err);
}
}
Interceptor order matters. The demo interceptor runs before the app's error interceptor, so it still sees the raw error code from the backend before anything else handles it.
I prefer a header over a separate demo login. The user stays signed in as themselves the whole time, the backend decides what the header may touch, and leaving demo mode is as simple as dropping it.
View as: one demo, every role
The part I like most is "view as". The demo comes with personas, and the user picks one. The app then overrides the role to that persona's role, so an admin can see the app exactly as a technician or a site manager would: their tabs, their dashboard, their permissions.
This uses the real role layouts, with no special demo screens. That keeps the demo honest, since what the admin sees as a technician is what a technician will get.
The active persona is held in memory above the app's state and is never persisted. A cold start always opens the real workspace. For a business app that is the safer default: nobody should reopen the app tomorrow and start entering real work into sample data by mistake.
Entering the demo, switching persona and leaving all rebuild the app's state the same way sign-out does, so no real data stays on screen after entering and no demo data stays after leaving. I covered that mechanism in how to reset Riverpod state on logout. A site switch inside the demo keeps demo mode on; only signing out ends it.
Decide what the demo cannot do
A demo that can do everything will eventually do something real. So some actions are blocked or hidden in demo mode:
- Staff creation, invites and CSV imports route to a "not available in the demo" screen. Invites send email to real people, and imports are a one-way door, so neither belongs in sample data.
- The AI tile is turned off. If AI features count against a plan allowance, a demo should not spend it. I wrote about allowances in AI assistant usage limits for SaaS plans.
- A setup reminder is hidden, since it would flag seeded records for something that does not apply to them.
- Editing staff email addresses is off, because an email address is how a real person gets contacted.
My rule of thumb: anything that leaves the system (email, push, payment, export to a third party) or spends something the account pays for is off in the demo. Everything else should work, including creating and editing records, because that is what people came to try.
Make demo mode obvious and easy to leave
A ribbon marks demo mode on every screen. There is also a Demo Data screen explaining what the demo holds, a demo sheet with the persona list and exit actions, and several entry points from empty screens and help. People should always know which workspace they are in.
Leaving is where most of the edge cases sit. Each exit path got its own rule:
- The demo disappears under you. If the backend answers with the "demo user invalid" code, for example because the demo was reset or removed outside this session, the app leaves demo mode and shows a toast saying the demo workspace is no longer available.
- A push notification arrives. Notifications are about real work. Tapping one, or an item in the Notifications list, exits the demo first and then opens the target in the real workspace.
- The user taps Exit. The app lands on the Dashboard tab of the real workspace.
The exit toasts ("Demo data removed" and the no-longer-available message) first died along with the old state, because they were shown just before the rebuild. They now run after the real workspace has landed. If you build anything that rebuilds app state, any message shown during that rebuild needs to wait for the new screen.
Data isolation is the backend's half of the job. My default is that demo rows never feed billing, notifications or the real account's reports, and that the tenant boundary checks I describe in multi-tenant SaaS architecture apply to the demo header like any other.
Write the demo as a spec first
A demo mode touches every screen, so I wrote it down before building it: entry points, the Demo Data screen, the ribbon, view-as personas and every exit path. The acceptance criteria were corrected during review, which is cheaper than finding the gaps in code. I covered the format in writing specs that AI coding agents can follow.
Tests cover parsing of the demo responses, the header interceptor, the persona override and the state reset. Those are the four places where a quiet bug would show real data in the demo or demo data in the real workspace.
Where I'd start
- Model the demo as its own workspace behind one resource with read, create and delete.
- Send the active demo user in one header from one HTTP interceptor, and let the backend decide which routes honour it.
- Add view-as personas that reuse the real role layouts.
- Keep the persona in memory only, so a cold start is always the real workspace.
- Block anything that contacts people, spends plan allowance or leaves the system.
- Write every exit path into the spec, and show exit messages only after the real workspace has loaded.
If you want a demo workspace like this in your own product, it is the kind of work I take on through my services.