Chapter 1 · Session 4
Why synthetic, and what realistic means
Every screen in the next weeks is built and tested before real data flows. It needs data that behaves like your plant: the right volumes, the right spread, the right rhythm of shifts, and the rare bad days. None of it may be real.
20 minNo real recordsSame schema everywhereRealistic, not random
By the end of this chapter you can
- Explain why development and tests run on synthetic data, never real records.
- Name what makes synthetic data realistic: volume, distribution, rhythm, rare events and mistakes.
- Collect the handful of plant facts the generator needs.
Why not just copy the real data?
Real plant data carries people's names, customer part numbers, prices and sometimes recipes. Copied into a development database, it is exposed to everyone and everything that touches that database: builders, test runs, preview links and AI agents. Removing names is not enough. NIST's guidance on de-identifying data sets explains how records can often be traced back to people by combining the fields that remain [1]. The safe rule is simple: development and tests hold synthetic data only, and real data stays in production.
Synthetic data also lets you start now. Session 4 is in week 2; real data from the floor arrives much later, once the screens, access rules and IT approval are in place. With realistic synthetic data, every screen from Session 8 on is built against data that looks like the plant from the first day.
Realistic is not random
Random numbers between 0 and 100 make every screen look fine and test nothing. Realistic data behaves like the plant in five ways:
| Property | What it means | Stamping plant |
|---|---|---|
| Volume | As many rows as a real quarter would make | About 1,560 lots and 6,240 inspections in 13 weeks |
| Distribution | Values spread the way the process spreads | Flange height around 12.00 mm, most within ±0.06 mm |
| Rhythm | Shifts, weekends, breaks, die changes | Two shifts on weekdays, nothing at night or on weekends |
| Rare events | The bad days happen, at about the real rate | About 1 first piece in 20 fails; a few holds a week |
| Mistakes | Late, missing and corrected entries | Some results typed in late; a few corrections |
Measurements from a stable process usually follow a normal distribution: most values close to the average, fewer further out, in a bell shape [2]. Statistical process control charts are built on exactly that pattern, so data generated this way exercises the charts and limits you will build later [3].
Five facts from the floor
You do not need the real data to make realistic data. You need a few facts, which the person who decides (from Session 2) can give you in ten minutes:
- How many presses, lines or cells, and which shifts they run.
- How many lots (or orders, or batches) each makes in a shift.
- For the key measurement: the target, the limits, and how much it usually varies.
- How often the bad event happens: failures, holds, breakdowns, per week.
- What usually goes wrong with the records: late entries, gaps, typing errors.
Knowledge check
Why not copy production data into the development database with the names removed?
Knowledge check
Which generated data would best test a hold screen?
References
- NIST SP 800-188: De-Identifying Government Datasets. https://csrc.nist.gov/pubs/sp/800/188/final
- NIST/SEMATECH e-Handbook of Statistical Methods. https://www.itl.nist.gov/div898/handbook/
- ASQ: Control chart. https://asq.org/quality-resources/control-chart
Chapter 2 · One seed file
Generate it in Postgres
No extra tool or paid service is needed. Postgres can generate a quarter of plant data in a second, from one file in your repository, and produce exactly the same rows every time.
25 mingenerate_seriesrandom_normalsetseed: same rows every run
By the end of this chapter you can
- Use generate_series to make rows for every day, shift, press and lot.
- Use random_normal and setseed for realistic, repeatable values.
- Keep the generator as supabase/seed.sql in the repository, and run it on a development project.
Three Postgres tools
| Function | What it does | Used for |
|---|---|---|
| generate_series(a, b) | Returns one row for each number from a to b [1] | Days, shifts, presses, lots, hours |
| random_normal(mean, sd) | A random value from a normal distribution (Postgres 16 and later) [2] | Measurements that spread like the process |
| setseed(x) | Fixes the random sequence for the session [2] | The same rows every time the file runs |
Combining several generate_series calls in one query makes every combination: 65 working days × 2 shifts × 6 presses × 2 lots gives 1,560 lots in a single statement. random() picks a part for each lot, and random_normal gives each measurement its spread.
The seed file
Supabase's convention is a file called supabase/seed.sql, which the Supabase CLI loads after the migrations whenever a local database is reset [4]. This is the stamping plant's seed file, written for the schema from Session 3. It expects the two test users from Session 2B, operator.test@example.com and lead.test@example.com, to exist.
-- supabase/seed.sql: the stamping plant's synthetic data. The same rows on every run.
select setseed(0.42);
insert into public.sites (code, name) values ('demo-plant', 'Demo Plant (synthetic)');
insert into public.equipment (site_id, code, name, kind)
select 1, 'press-' || lpad(n::text, 2, '0'), 'Press ' || n, 'press' from generate_series(1, 6) as n;
insert into public.parts (part_number, name)
values ('DEMO-1001', 'Bracket'), ('DEMO-1002', 'Flange'), ('DEMO-1003', 'Hinge plate'), ('DEMO-1004', 'Mount');
insert into public.characteristics (part_id, name, lower_mm, upper_mm)
select id, 'flange height', 11.900, 12.100 from public.parts;
-- 13 weeks of weekdays, 2 shifts, 6 presses, 2 lots per press per shift.
insert into public.lots (site_id, part_id, equipment_id, lot_code, started_at)
select 1, 1 + floor(random() * 4)::int, p,
'L-' || lpad((row_number() over (order by d, s, p, l))::text, 5, '0'),
timestamptz '2026-07-06 06:00 America/New_York' + d * interval '1 day' + s * interval '8 hours 30 minutes' + l * interval '4 hours'
from generate_series(0, 90) d, generate_series(0, 1) s, generate_series(1, 6) p, generate_series(0, 1) l
where d % 7 < 5; -- 6 July 2026 is a Monday: days 5 and 6 of each week are the weekend
-- A first piece (wider spread after the die change) and three hourly checks per lot; the die wears a little each hour.
insert into public.inspections (site_id, lot_id, characteristic_id, kind, value_mm, recorded_by, recorded_at)
select l.site_id, l.id, c.id,
case when h = 0 then 'first_piece' else 'hourly' end,
round(random_normal(12.000 + h * 0.004, case when h = 0 then 0.05 else 0.03 end)::numeric, 3),
(select id from auth.users where email = 'operator.test@example.com'),
l.started_at + h * interval '1 hour' + random() * interval '10 minutes'
from public.lots l join public.characteristics c on c.part_id = l.part_id, generate_series(0, 3) h;
-- The quality lead holds each lot at its first result outside the limits, about 12 minutes later.
insert into public.holds (site_id, lot_id, action, reason, decided_by, decided_at)
select distinct on (i.lot_id) i.site_id, i.lot_id, 'hold',
format('Flange height %s mm outside %s to %s', i.value_mm, c.lower_mm, c.upper_mm),
(select id from auth.users where email = 'lead.test@example.com'),
i.recorded_at + interval '12 minutes'
from public.inspections i join public.characteristics c on c.id = i.characteristic_id
where i.value_mm not between c.lower_mm and c.upper_mm
order by i.lot_id, i.recorded_at;Read it from the top. The reference data comes first. Then come 13 weeks of lots on weekdays and shifts only. Each lot gets a first piece with a wider spread, because set-up is never perfect, and three hourly checks that drift upward a little as the die wears. Last, a hold for every lot at its first out-of-limit result, recorded 12 minutes later. That delay is the stamping plant's target from its intent.
Where to run it
The free way is a second Supabase project for development, on the free plan, which allows two projects [5]. The seed runs there, and your company's main project stays clean for the real data that comes later. Free projects pause after a week without use; open the dashboard to wake one. Apply the same migrations to both projects, so the schema is identical.
Running it again
Because inspections and holds are append-only (Session 3), the seed cannot delete and redo rows one by one: the trigger refuses. On the development project, empty the tables in one statement first. truncate does not fire the row triggers, and restart identity makes the ids start from 1 again.
truncate public.holds, public.inspections, public.lots, public.characteristics, public.parts, public.equipment, public.sites restart identity cascade;Knowledge check
Why start the seed file with setseed?
Knowledge check
Where does the seed file run?
References
- PostgreSQL Documentation: Set Returning Functions (generate_series). https://www.postgresql.org/docs/current/functions-srf.html
- PostgreSQL Documentation: Mathematical Functions (random, random_normal, setseed). https://www.postgresql.org/docs/current/functions-math.html
- G. E. P. Box and M. E. Muller, A Note on the Generation of Random Normal Deviates, Annals of Mathematical Statistics, 1958. https://doi.org/10.1214/aoms/1177706645
- Supabase Docs: Seeding your database. https://supabase.com/docs/guides/local-development/seeding-your-database
- Supabase pricing (free plan limits). https://supabase.com/pricing
Chapter 3 · Before anyone builds on it
Check it looks like your plant
Synthetic data is only useful if it fails in the same places the plant does. Four queries and ten minutes with the person who decides tell you whether it does.
20 min4 check queries1 scorecardHomework 1
By the end of this chapter you can
- Run counts and rates on the generated data and compare them with the plant's own answers.
- Add the awkward cases: late entries, corrections, gaps.
- Show the person who decides, and capture the result for Homework 1.
Four queries
Run these in the SQL editor of the development project [2]. Each one answers a question the person who decides can check from memory: how much, how often, how bad.
-- 1. How many rows, and over how long?
select (select count(*) from public.lots) as lots, (select count(*) from public.inspections) as inspections,
(select count(*) from public.holds) as holds,
(select min(recorded_at)::date from public.inspections) as first_day, (select max(recorded_at)::date from public.inspections) as last_day;
-- 2. Inspections per shift (two shifts a working day).
select round(count(*)::numeric / count(distinct (recorded_at at time zone 'America/New_York')::date) / 2, 1) as per_shift
from public.inspections; -- the plant's own time zone decides which day a shift belongs to
-- 3. Share of first pieces outside the limits.
select round(100.0 * avg(case when i.value_mm not between c.lower_mm and c.upper_mm then 1 else 0 end), 1) as pct_first_piece_out
from public.inspections i join public.characteristics c on c.id = i.characteristic_id
where i.kind = 'first_piece';
-- 4. Holds per week.
select round(count(*)::numeric / count(distinct date_trunc('week', decided_at)), 1) as holds_per_week from public.holds;Add the awkward cases
Real records are messy, and screens must cope. Before you call the data finished, add the cases that break screens on the floor:
- Late entries: about one result in twenty recorded an hour or more after it was measured. Add a second, later time to a random 5% of rows, or record them later.
- Corrections: a few results corrected by a new row, with a reason, as the append-only design expects.
- Gaps: a press down for a shift, so there are no lots at all. Screens must show 'no data', not zero.
- Edge values: results exactly on a limit (12.100), which must count as inside.
- Long text: a hold reason of 300 characters, to check the phone layout does not break.
Volume for later
Thirteen weeks of one site makes about 8,000 rows: enough to build and test every screen. Later sessions on performance generate a year or more the same way, by changing two numbers in the seed. Postgres handles millions of rows like these without special tuning, as long as the columns screens filter on are indexed [1].
You need: A second Supabase project on the free plan, your repository with the Session 3 schema, an AI coding agent, and the five facts from your plant
Use your own intent and schema. The stamping plant's seed file in chapter 2 is a template. Never run any of this on the production project.
Outcome: supabase/seed.sql on main; a development project full of data that looks like the plant on every scorecard line; a screenshot of the seeded data, for Homework 1.
Knowledge check
The generated data shows no late entries, but the plant says about 5% of results are typed in late. What do you do?
Knowledge check
How do you know the synthetic data is realistic enough?
References
- PostgreSQL Documentation: Indexes. https://www.postgresql.org/docs/current/indexes.html
- Supabase Docs: Database (SQL editor and Table Editor). https://supabase.com/docs/guides/database/overview
Chapter 4 · 12 questions · 80% passes
Final assessment
Twelve questions across the element. Score 80% (10 of 12) to pass. Your LMS records your score and each answer; you can review the chapters and try again.
15 min12 questions≈ 15 minutesRetake allowed
Your result
CivOps AI Academy
Data That Looks Like Your Plant: Synthetic Data in Postgres
Element F04 complete · Learner
Your LMS records this completion. For the CivOps Foundation certificate, finish the Foundation Course at https://civops.io/learn.