Seeding Demo Data Nobody Can Mistake for a Real Customer
Seeding Demo Data Nobody Can Mistake for a Real Customer
Every product needs demo data. Screenshots for the marketing site, a workspace to record a video in, something to look at while building a report that would otherwise render an empty state.
So you seed an organisation. Eight employees, a few projects, two months of plausible time entries. It looks great, which is the point.
Three months later somebody asks how many customers are actually using the product, and the answer includes it.
Why it is so easy to be fooled

Seeded data is the healthiest data in the system, by design. It has eight users where real organisations have one, a thousand time entries where real ones have six, and two months of continuous activity where real ones have an afternoon.
So it dominates every aggregate. Average entries per organisation, total hours tracked, most active workspace — all of them are mostly the seed.
And it is invisible in exactly the places you would look. A list of organisations shows a name. A chart shows a bar. Nothing about either says “this one is not real”.
We had this. A seeded organisation sat in the database for three months with 1,035 time entries, against two real users who had logged six entries and one entry respectively. Every number anyone quoted about usage was mostly that one row.
Mark it in four ways

You want all four, because they serve different audiences.
A boolean column on the organisation. is_demo, defaulting to false. This is the one that matters, because it is the only one a query can use. Every internal report and every count of customers filters on it.
Add it before you seed anything. Retrofitting means identifying the demo rows by hand later, which is exactly the task you were trying to avoid.
An obviously fake email domain. @demo.invalid or @example.com. .invalid is reserved and can never resolve, which has a second benefit: no mail will ever accidentally be sent to it.
The failure here is using a plausible fake domain. @northwind.studio looks exactly like a real customer in a user list, and nobody scanning the screen will stop on it.
Timestamps that give it away on inspection. If every seeded user was created at exactly 00:00:00 on the same date, anyone who looks closely can tell immediately. Keep that — it is a useful tell. The temptation to randomise timestamps “so it looks realistic” is making the data harder to identify in exchange for nothing.
A name that says what it is. “Demo Workspace” rather than “Northwind Studio”. The only argument for a realistic name is screenshots, and that is solvable — see below.
Exclude it everywhere, by default
Admin dashboards, usage counts, “how many customers do we have”, any export that goes to a manager.
The right default is excluded, with an explicit option to include. The other way round means every new report somebody writes includes it until they notice, and they will notice after quoting the number.
Screenshots without a realistic name
The reason people give seeded data a believable name is marketing screenshots, and it is a fair reason.
Two better options. Rename the demo organisation just before a screenshot session and rename it back — a minute of work, a few times a year. Or keep a separate seeded organisation purely for screenshots, clearly marked in the database and short-lived, created and dropped around the session.
Either beats a permanent fixture that looks like a customer.
Make it reproducible, and make it removable
The seed should be a command that can be run again. Fixed names, deterministic structure, so re-running produces the same workspace rather than a second slightly different one.
Equally important, a command that removes it cleanly — and that is harder than it sounds, because a workspace with time entries, tasks, comments, invoices and audit rows has a lot of children, and they have to be deleted in the right order.
Work it out while building the seed, not two months later under pressure.
We removed ours eventually: one organisation, eight users, 1,424 related rows across seventeen tables, and the order mattered — leave requests before leave types, because the foreign key there is RESTRICT and refuses otherwise.
Never seed into production without the column
The simplest rule, and the one that would have prevented all of this.
If the is_demo column does not exist, the seed command refuses to run. Three lines in the seeder, and the whole problem goes away.

