Local Emulator
A toggle inside the cPod portal that flips every EDM call from the real backend to local seed data. No separate process. No Docker. No MongoDB. No env config beyond a single feature flag.
Try it now → Open the portal in Emulator mode
(assumes the portal is running on :4000 — see Quickstart below)
What's emulated, what's not#
Only EDM domain endpoints (/api/v1/<edm-domain>/...) are intercepted. Everything else passes through to the real backend.
| Path shape | Emulator mode | Why |
|---|---|---|
/api/v1/crm/*, /api/v1/helpdesk/*, /api/v1/hr/*, etc. | emulator | These are the typed EDM CRUD endpoints — the surface devs build apps against. |
/api/v1/registry/* | live | Registry data lives in the real DB; emulating it would mask real state. |
/api/v1/auth/*, /api/v1/admin/* | live | Auth, tenants, sessions — system of record only. |
/api/portal/me, /api/portal/settings | live | Portal metadata. |
That's the rule the proxy route enforces. Even when you flip the toggle, registry / auth / admin / settings calls always go to the real backend — the inspector still tags them with mode=emulator so you can see exactly what hit live vs what didn't.
Why this exists#
When you're building a cPod app, you want to:
- See the exact request that goes out and the response that comes back.
- Try
create → list → update → deleteflows on EDM data without spinning up the backend. - Hand a teammate a working portal that runs anywhere with
git clone && pnpm install && pnpm dev.
The Emulator gives you all three from a chip in the portal header.
How it works#
Toggle ON + EDM path → /api/portal/proxy → in-portal emulator (JSON seeds + in-memory store)
Toggle ON + non-EDM → /api/portal/proxy → forward to Python backend (unchanged)
Toggle OFF → /api/portal/proxy → forward to Python backend (unchanged)
- The toggle sends an
x-cpod-mode: emulatorheader on every call. - The proxy route checks the path's first segment against an EDM-domain allowlist. Only EDM paths are dispatched to the emulator.
- The emulator module is dynamic-imported — when the toggle is off, none of it is loaded.
- Data lives in JSON files under
portal/src/lib/emulator/seeds/edm/. Each file is one collection, keyed by URL path slug (e.g.crm-accounts.json↔/api/v1/crm/accounts). Hand-editable. - Writes mutate an in-memory layer on top of the seeds. "Reset data" in the inspector rolls back to the seed JSONs.
- Resets also happen automatically when the Next.js process restarts (HMR, code edits).
Quickstart#
Enable the feature flag#
Add to portal/.env.local:
NEXT_PUBLIC_EMULATOR_ENABLED=trueWithout this flag the toggle chip is hidden and the portal behaves exactly as it did before. Safe default for prod.
Start the portal#
cd cpod-sdk/portal
pnpm install
pnpm devPortal is at http://localhost:4000.
Flip the toggle#
In the top-right header chip, click Emulator. The chip turns amber.
You can also land directly in Emulator mode by linking to /portal?mode=emulator — the URL param wins over localStorage on load.
Open the Inspector#
Click Inspector in the header. A docked panel slides up showing every API call made through the portal, with:
- Method · path · status · latency · mode (live / emulator)
- Click any row → side-by-side request and response JSON
- Copy buttons on both sides
- Reset data button (Emulator mode only) → rolls in-memory writes back to seeds
What's emulated (EDM domains)#
Any path under one of these prefixes is served by the emulator when the toggle is on:
approvals · catalog · cloud-resources · compliance-controls · contracts ·
credentials · crm · datasources · edm-projects · employee · entitlements ·
finance · grc · group-metadata · groups · helpdesk · hr · integration ·
investments · knowledge · learning · licenses · notifications · okr · org ·
people · performance · physical-assets · policies · procurement ·
relationships · rfp · risk · soc · storage · technology · vulnerabilities ·
work
Every domain supports the five standard verbs via one generic CRUD handler:
| Method | Path shape | Emulator behavior |
|---|---|---|
GET | /api/v1/<domain>/<entity> | Return all rows from the seed file, paginated if page/pageSize is in the query. |
GET | /api/v1/<domain>/<entity>/<id> | Return the row, or 404 Not found. |
POST | /api/v1/<domain>/<entity> | Generate id, append to in-memory layer, return 201. |
PATCH / PUT | /api/v1/<domain>/<entity>/<id> | Merge patch into the row. |
DELETE | /api/v1/<domain>/<entity>/<id> | Remove from in-memory layer. |
Endpoints without a seed file start empty — GET returns [], POST works, you're off to the races.
Editing seed data#
Files live at portal/src/lib/emulator/seeds/edm/. Filename = URL path slug. Examples:
seeds/edm/
├─ crm-accounts.json (/api/v1/crm/accounts)
├─ crm-contacts.json (/api/v1/crm/contacts)
├─ crm-deals.json (/api/v1/crm/deals)
├─ helpdesk-tickets.json (/api/v1/helpdesk/tickets)
├─ hr-employees.json (/api/v1/hr/employees)
└─ work-tasks.json (/api/v1/work/tasks)
Each file is a flat JSON array of records. Every record needs an id field; everything else is up to your schema.
Add a new seed file → pnpm dev HMR reloads → the next request to that path returns your data.
Custom verbs#
The generic dispatcher only knows the five CRUD verbs. EDM endpoints with custom verbs (/publish, /decide, /complete, sub-resource paths like /tickets/{id}/comments) hit the fallback handler and return:
{ "title": "Not emulated", "status": 404,
"detail": "... doesn't match the generic EDM ... pattern. Custom verbs ... need a bespoke handler in src/lib/emulator/dispatcher.ts." }To add one, append a route in portal/src/lib/emulator/dispatcher.ts. Pattern + handler. That's the whole change.
Safety#
- The toggle chip is gated by
NEXT_PUBLIC_EMULATOR_ENABLED. In prod/staging it's not set → no chip, no emulator code path, identical to today. - The
_emulatorfield on responses is the easiest way to verify which world you're talking to (status+latencyMscome back from the in-portal emulator only). - Seeds are committed to git — your team can replay each other's state by sharing JSON edits.
- Registry, auth, admin, and settings calls always go to the real backend, even when the toggle is on. There's no way for the emulator to accidentally serve identity or registry data.