Mocking the sides

Running the whole loop

Both mocks and the Center at once — a full round trip, a dead letter, and a replay.

Each mock is useful alone. Together with the Center they close the loop: you send a message from the left, watch it arrive on the right, break the right side, watch the Center cope, and fix it — all on one machine, in a few minutes.

Start everything

From the repo root:

./dev.sh

That brings up the infrastructure (the Supabase stack and NATS) along with the API, the web app and this documentation site. The two mocks are separate — start each in its own terminal:

cd source-test && npm run start:dev    # :3003
cd sink-test   && npm run start:dev    # :3004

What ends up running:

PortWhat
3000API — /api/v1, Swagger at /docs
3001Web app
3002These docs
3003Mock source — console at /ui
3004Mock sink — console at /ui
The mocks need the Center running, not the other way round
The mock source posts at the API, and the Center delivers to the mock sink. Start the API first, or the first emit will simply fail to connect.

Wire the Center to both mocks

Once, in the app:

  1. Source — an ordinary source of the technology the mock impersonates (Sigfox for Sigfox mode, The Things Stack for TTN mode — there is no "mock" technology, see Mock source). Paste its ingest URL and webhook secret into the mock's connection panel, or set them in source-test/.env; confirm with curl -s localhost:3003/health and check the ingestUrl it reports. Without the secret, expect 401s.
  2. Sink — set your SINK integration's endpoint URL to http://localhost:3004/sink and press Test. It should go green.
  3. Routing — open Flow → Routing and wire the source to the sink. Without a route the project broadcasts to every active sink, which works but is worth making explicit.

Open both consoles side by side — :3003/ui and :3004/ui — and leave them open.

Loop 1 — the happy path

curl -X POST localhost:3004/control -H 'content-type: application/json' -d '{"mode":"ok"}'
curl -X POST localhost:3003/emit/bidir

What you should see, in order:

  1. The mock source console records the emission.
  2. Analytics in the Center: Received +1, then Delivered +1.
  3. The mock sink console flashes a new row, labelled DATA_BIDIR (0/3) and marked awaits reply.

Click the row in the sink console to see exactly what the Center built — this is the fastest way to check an output structure is doing what you meant.

Loop 2 — a dead letter, and its replay

Now break the right side and watch the delivery contract work:

# The sink starts refusing everything
curl -X POST localhost:3004/control -H 'content-type: application/json' \
     -d '{"mode":"fail","failStatus":503}'

curl -X POST localhost:3003/emit/bidir

Analytics shows Failed climbing as attempts are retried with backoff. After eight attempts the delivery lands in Dead letters with its attempt count.

Fix the "outage" and replay:

curl -X POST localhost:3004/control -H 'content-type: application/json' -d '{"mode":"ok"}'

Open Dead letters in the Center and press Replay. The sink console shows it arriving, this time accepted.

That is the whole contract, demonstrated: at-least-once, backoff, dead letter, replay — and replay uses today's configuration, which is why fixing the cause first works.

This is the one that needs both consoles, because neither can answer it alone.

Give the sink an answer to return, then send an uplink that asks for one:

curl -X POST localhost:3004/control -H 'content-type: application/json' \
     -d '{"downlinkHex":"deadbeef00000001"}'

curl -X POST localhost:3003/emit/bidir

Read it across the two consoles:

WhereTells you
Mock sinkThe delivery arrived, awaits reply, and what hex went back to the Center
Mock sourceWhether that downlink actually reached the device — the round-trip panel

A downlink shown in the sink console was handed to the Center. Only the source console closes the loop. If the sink says it answered and the source says nothing came back, the problem is between them — most often a contract that is not bidirectional or a hex value that is not exactly 16 characters.

Loop 4 — duplicates

The Center delivers at least once, so your real sink must tolerate a repeat. Prove yours does:

curl -X POST localhost:3003/emit/bidir -H 'content-type: application/json' -d '{"seqNumber": 77}'
curl -X POST localhost:3003/emit/bidir -H 'content-type: application/json' -d '{"seqNumber": 77}'

Both carry the same idempotencyKey; the sink console flags the second DUP. Filter the console by problems to see it among anything else that was not a clean accept.

Resetting between runs

curl -X DELETE localhost:3003/log        # clear the source's emissions
curl -X DELETE localhost:3004/log        # clear the sink's deliveries
curl -X POST   localhost:3004/control -H 'content-type: application/json' -d '{"mode":"ok"}'
curl -X POST   localhost:3003/v2/_reset  # only if you have been testing provisioning

What this does and does not prove

It exercises the Center's real ingest, decoding, rules, routing, transform and delivery paths — the same code that runs in production.

It does not prove your actual device transmits correctly, that your real network is configured, or that your real consuming application behaves like the mock. Those are the parts the mocks are standing in for. Treat a green loop here as "the Center is configured correctly", not as "the deployment is done".

Next

Mock source
Every option: sessions, timings, overrides, the fake Sigfox API.
Mock sink
Every mode, and frame-driven downlink rules.
Copyright © 2026