All data is synthetic. Meridian Fabrication Co. is a fictional company created for integration demonstrations. No real companies, part numbers, or employees are represented.
A self-contained FastAPI + PostgreSQL sandbox that ships 5,000+ parts, 400 suppliers, 20,000 change orders, and 30,000 purchase orders, all seeded with deterministic, configurable messiness.
Built for engineers who need a realistic target system to build and demo data pipelines, ETL connectors, or AI-assisted data cleaning without touching production.
| Entity | Rows (medium messiness) | Key quirks |
|---|---|---|
| Parts | 5,000 | Three part-number formats from three eras |
| Part Revisions | ~12,000 | Mixed revision schemes (A/B/C vs 1/2/3) |
| Suppliers | 400 | Near-duplicate names, invalid emails |
| Change Orders | 20,000 | 5 state vocabulary variants, impossible dates |
| Purchase Orders | 30,000 | Mixed currencies, price magnitude errors |
| Users | 150 | Inactive users still referenced in COs |
| Audit Log | auto | Missing actors |
See QUIRKS.md for the full defect catalog.
graph TD
Browser["Browser / API Client"] --> API["FastAPI :8000"]
API --> DB["PostgreSQL 16"]
API --> Seed["Seed Engine"]
Seed --> Gen["generators.py<br>5K parts · 400 suppliers<br>20K COs · 30K POs"]
Seed --> Manifest["mess_manifest.json<br>(ground truth)"]
Seed --> Exporter["exporter.py<br>CSV · XLSX · legacy encodings"]
API --> UI["Jinja2 + HTMX<br>Dashboard · Parts · Suppliers · COs"]
Point DATABASE_URL at SQLite and scale the row counts down so a laptop seeds in seconds:
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export DATABASE_URL="sqlite:///acme.db"
export PARTS_COUNT=500 CHANGE_ORDERS_COUNT=2000 PURCHASE_ORDERS_COUNT=3000 AUDIT_LOG_COUNT=5000
python -m app.seed.run
uvicorn app.main:app --reloadOpen http://localhost:8000/. Drop the count overrides to seed the full-size dataset (slower on SQLite).
git clone https://github.com/RedBeret/acme-parts-cloud.git
cd acme-parts-cloud
docker compose up --buildOpen http://localhost:8000/; the seeder runs automatically on first boot. On Windows, run.bat does the venv setup for you.
export DATABASE_URL=postgresql+psycopg2://acme:acme@localhost:5432/acmeparts
export SEED=42
export MESSINESS=medium # clean | medium | chaos
python -m app.seed.run
uvicorn app.main:app --reloadControl defect injection with the MESSINESS environment variable:
| Level | Configurable injection rate | Use case |
|---|---|---|
clean |
~2% | Baseline; test happy-path pipelines |
medium |
~10% | Default; realistic enterprise system |
chaos |
~25% | Stress-test your cleaning and dedup logic |
These rates apply to configurable format, duplicate, email, state, date, and price injections. Structural quirks such as mixed currencies, inactive records, missing actors, and export schemas remain present at fixed rates.
The seeder writes mess_manifest.json after each run. Manifest v2 records a stable row key for every injected defect and derives its summary counts from those keys.
| Endpoint | Method | Description |
|---|---|---|
/api/parts |
GET | List/search parts (search, status, category, after) |
/api/parts/{id} |
GET | Get one part |
/api/suppliers |
GET | List/search suppliers (search, active, after) |
/api/suppliers/{id} |
GET | Get one supplier |
/api/change-orders |
GET | List COs (search, state, priority, after) |
/api/change-orders/{id} |
GET | Get one change order |
/admin/healthz |
GET | Liveness check |
/admin/reset |
POST | Opt-in reseed with ?seed=N |
/docs |
GET | Interactive Swagger UI |
All list endpoints use cursor pagination via the after parameter (last seen id).
The samples/ directory contains small committed samples of each export format:
samples/
parts_v1_sample.csv — 2019-era column names
parts_v2_sample.csv — schema drift (extra legacy_ref column)
change_orders_sample.xlsx — merged header row, embedded newlines
suppliers_legacy_sample.csv — UTF-8 preview of the Windows-1252 full export
Spot the drift — same part, two export versions:
# parts_v1 (2019-era headers)
partNo,partName,cat,measure,partStatus
# parts_v2 (renamed uom column, new legacy_ref, different column order)
part_number,name,uom,category,status,superseded_by,created_at,legacy_refThe committed samples contain both legacy part-number formats. The seeder records each affected key in mess_manifest.json, so any cleanup tool you point at this data can be scored, not just eyeballed.
Full exports are available at runtime:
curl http://localhost:8000/exports/parts/v1 > parts_v1.csv
curl http://localhost:8000/exports/parts/v2 > parts_v2.csv
curl http://localhost:8000/exports/suppliers/legacy > suppliers_legacy.csv
curl http://localhost:8000/exports/change-orders > change_orders.xlsxScenario: An engineer is building a Snowflake connector to ingest AcmeParts supplier data for procurement analytics. The pipeline breaks on first run.
Root cause chain discovered using this sandbox:
SELECT * FROM suppliers WHERE active = true— silently drops 8% of historical PO data because defunct suppliers are still referenced inpurchase_orders.GROUP BY supplier_name— returns 3 rows for "Vortex Metals" because of near-duplicate name variants (Vortex Metals,Vortex Metals Inc.,VORTEX METALS). Entity resolution required before aggregation.SUM(unit_price)across POs — produces a meaningless number becauseunit_priceis in mixed currencies (USD, EUR, GBP) with no exchange rate table.- Opening
suppliers_legacy.csvas UTF-8 — garbled output because the file is Windows-1252 encoded.
Every one of those defects is recorded in mess_manifest.json when the data is seeded, so you can check what your pipeline caught against ground truth instead of guessing. That is the whole point of the sandbox: the bugs are known, counted, and reproducible from a seed.
# Via API (disabled by default)
ADMIN_RESET_TOKEN="choose-a-local-token" ENABLE_ADMIN_RESET=true docker compose up -d
curl -X POST -H "X-Admin-Reset-Token: choose-a-local-token" \
"http://localhost:8000/admin/reset?seed=99"
# Via CLI (inside container)
docker compose exec api python -m app.seed.run --resetSame SEED + MESSINESS always produces byte-identical manifest output, and the optional count environment variables let you scale the dataset down for quick CI runs or up for heavier demos.
See CONTRIBUTING.md.
MIT — see LICENSE.
