Skip to main content

Portable mock runtime

A portable mock is a mock bundle served by the same runtime the hosted mock uses — on a laptop, in CI, or inside an air-gapped network. There is no database, no control plane connection, and no tenant credential anywhere in the picture: the bundle is the whole configuration.

CLIapiome mock run BUNDLE
Runtimeapiome-mock run --bundle BUNDLE (apiome-mock ≥ 0.3.0)
Imageghcr.io/apiome/apiome-mock — linux/amd64, linux/arm64
LivenessGET /health
ReadinessGET /ready
Conformanceapiome-mock selftest · apiome-mock conformance --base-url URL
ServerlessServerless mock adapter — the same runtime as a function

Both the CLI and the image execute the identical runtime, and both are held to the same mock conformance corpus — that is what "portable" is allowed to mean here.


Quick start​

Export a bundle for a mock-enabled version, then run it:

curl -sS -H "Authorization: Bearer $APIOME_TOKEN" \
"http://localhost:8000/v1/versions/acme-corp/$PROJECT_ID/$VERSION_RECORD_ID/mock/bundle" \
-o petstore-1.0.0-mock-bundle.json

apiome mock run petstore-1.0.0-mock-bundle.json
Starting mock (local) at http://127.0.0.1:8775/acme-corp/petstore/1.0.0
readiness http://127.0.0.1:8775/ready
command apiome-mock run --bundle /home/you/petstore-1.0.0-mock-bundle.json --host 127.0.0.1 --port 8775 --base-path version
curl http://127.0.0.1:8775/acme-corp/petstore/1.0.0/pets

apiome mock run prefers a locally installed apiome-mock, and otherwise launches the official image. Force either one with --runtime local / --runtime docker, and preview the exact command with --dry-run.

Docker directly​

docker run --rm -p 8775:8775 \
-v "$PWD/petstore-1.0.0-mock-bundle.json:/bundle/mock-bundle.json:ro" \
ghcr.io/apiome/apiome-mock:latest run

The image defaults APIOME_MOCK_BUNDLE=/bundle/mock-bundle.json and APIOME_MOCK_HTTP_HOST=0.0.0.0, so mounting a bundle at that path is all run needs. The same image still runs the hosted, database-backed mock as its default command (serve).

Build it for every supported architecture:

apiome-mock/scripts/build-image.sh --push ghcr.io/apiome/apiome-mock:0.7.0
# PLATFORMS=linux/amd64 apiome-mock/scripts/build-image.sh --load apiome-mock:dev # local testing

URL shape​

By default the runtime serves the spec under the hosted URL shape, so a request that works against the hosted mock works against the portable one by changing only the host:

http://127.0.0.1:8775/{tenant}/{project}/{version}/{spec path}

Pass --base-path root to serve spec paths directly at / instead (http://127.0.0.1:8775/pets). /health and /ready are reserved in both modes and are never routed to the spec, and the __mock__ segment under the mounted prefix is reserved for the data lifecycle control plane (fixture packs, session reset — see Mock fixture packs).

Anything outside the mounted prefix answers 404 as application/problem+json, naming the prefix that is mounted.

Readiness​

EndpointMeaningCodes
GET /healthLiveness. The process is up and serving HTTP.200
GET /readyReadiness. The bundle is verified, compiled, and mounted.200, 503 while starting

Wait on /ready, not /health: a bundle that fails verification never reaches ready (the process exits instead), and /health cannot tell you which bundle answered.

{
"status": "ready",
"runtime": {
"name": "apiome-mock",
"version": "0.7.0",
"mode": "portable",
"basePath": "version",
"mount": "/acme-corp/petstore/1.0.0"
},
"bundle": {
"digest": "sha256:90591f2f…",
"tenant": "acme-corp",
"project": "petstore",
"version": "1.0.0",
"signed": true,
"operations": 5,
"scenarios": ["quota-exceeded"],
"fixtures": []
}
}

bundle.digest is the bundle's stable identity — assert it in CI to prove the job ran against the artifact it thinks it did.

The container image's HEALTHCHECK polls /health. In a Compose or Kubernetes deployment, wire /health to the liveness probe and /ready to the readiness probe.

Structured logs​

Every line on stdout is a single JSON object — including uvicorn's own lifecycle lines — so a log collector needs exactly one parser. Common keys: event, level, timestamp (ISO-8601 UTC).

eventWhenNotable fields
portable_runtime_startingBefore the bundle is loadedruntime_version, host, port, config (secrets redacted), digest
portable_runtime_readyApplication startup completedigest, tenant, project, version, mount, operations, scenarios, signed
mock_requestOne per served requestmethod, path, status, duration_ms, digest
mock_callback_attemptOne per callback delivery attemptcallback, destination (query redacted), attempt, delay_ms, status, error, duration_ms
mock_callback_deliveryOne per callback delivery, terminalcallback, digest, outcome, destination, trigger, status, attempts, detail
portable_runtime_stoppedApplication shutdowndigest
bundle_invalid / bundle_incompatibleThe bundle was rejected at startupproblems[] with stable codes
{"method": "GET", "path": "/acme-corp/petstore/1.0.0/pets", "status": 200, "duration_ms": 0.4, "digest": "sha256:90591f2f…", "event": "mock_request", "level": "info", "timestamp": "2026-07-28T03:18:42.876956Z"}

Turn the per-request line off with --no-access-log (lifecycle events still emit). Raise or lower verbosity with --log-level.

Configuration​

Configuration comes from declared flags and declared environment variables only. No configuration file is read, and no .env file is picked up from the working directory — a laptop and a container given the same flags and environment resolve to identical configuration. A flag always wins over its environment variable; omitting a flag leaves the environment in charge.

FlagEnvironment variableDefaultMeaning
--bundle PATHAPIOME_MOCK_BUNDLE(none)Bundle document to serve. Required.
--host ADDRAPIOME_MOCK_HTTP_HOST127.0.0.1 (0.0.0.0 in the image)Bind address.
--port PORTAPIOME_MOCK_HTTP_PORT8775TCP port.
--base-path {version,root}APIOME_MOCK_BASE_PATHversionURL shape (see above).
--require-signatureAPIOME_MOCK_REQUIRE_SIGNATUREfalseRefuse to start on an unsigned bundle.
(none — env only)APIOME_MOCK_BUNDLE_SECRET(none)Shared HMAC secret the signature must verify against.
--log-level LEVELAPIOME_MOCK_LOG_LEVELINFOStructured log level.
--no-access-logAPIOME_MOCK_ACCESS_LOGtruePer-request mock_request line.
--callbacksAPIOME_MOCK_CALLBACKS_ENABLEDfalseDeliver the bundle's contract callbacks/webhooks outbound. Off by default: a portable mock makes no network connections unless it is told to.
--callback-allow-privateAPIOME_MOCK_CALLBACK_ALLOW_PRIVATEfalsePermit callback destinations on loopback/private addresses, for a CI job whose receiver runs beside the mock.
--callback-timeout SECONDSAPIOME_MOCK_CALLBACK_TIMEOUT_SECONDS5Ceiling on one callback delivery attempt.
--session-ttl SECONDSAPIOME_MOCK_SESSION_TTL_SECONDS3600Sliding TTL for X-Mock-Session state.
--session-max-resources COUNTAPIOME_MOCK_SESSION_MAX_RESOURCES200Resources per session.
--session-max-bytes BYTESAPIOME_MOCK_SESSION_MAX_BYTES1048576JSON bytes per session.
--session-max-sessions COUNTAPIOME_MOCK_SESSION_MAX_SESSIONS10000Concurrent sessions.

The signing secret is deliberately environment-only: a value on a command line is readable by every user on the machine through ps. apiome mock run --runtime docker forwards it by name (--env APIOME_MOCK_BUNDLE_SECRET), so the value passes through the daemon without ever appearing in an argument list.

Print exactly what a given invocation resolved to:

apiome-mock run --bundle ./mock-bundle.json --port 9000 --print-config

Exit codes​

CodeMeaning
0Success.
2Configuration error — a missing or invalid flag/environment value.
3Bundle verification failed — malformed, tampered, unsigned when required, or credential-bearing.
4Bundle is well-formed but incompatible with this runtime version.
5Conformance failures.

Check a bundle before a job depends on it:

apiome-mock verify --bundle ./mock-bundle.json --json

Conformance​

The runtime ships a shared conformance corpus: a declarative set of requests and expected responses, run against a running mock. Passing it is what lets the CLI and the image claim identical behavior.

apiome-mock selftest # serve the packaged bundle, run the corpus
docker run --rm ghcr.io/apiome/apiome-mock:latest selftest
apiome-mock conformance --base-url http://127.0.0.1:8775 # run it against a mock you started

selftest needs no mount, no published port, and no external corpus, so "this image passes the corpus" is one reproducible command. Both commands exit 5 on any failure and print (or, with --json, emit) the failing case, the reason, and what the case exists to pin down.

The corpus covers example-first responses, path parameters, request validation, 405/404/406 handling, forced statuses (Prefer: code=), scenarios and scenario sequences, declarative match rules and bounded templates, chaos injection and delay reporting, session-scoped CRUD and its isolation, fixture packs and the session-reset lifecycle, seeded deterministic synthesis, and the two operational endpoints.

The corpus document declares a corpusVersion and resolves a content digest over its canonical JSON, both reported in --json output under corpus. That identity is what a release-proof mock attestation records, so "which corpus proved this" survives long after the job's logs are gone.

Attesting a run​

attest turns what verify and conformance proved into the record a release proof attaches (PMR-3.2): the bundle digest, this runtime's version, the corpus identity and result, and every fixture-pack digest.

apiome-mock conformance --base-url http://127.0.0.1:8775 --json > conformance.json
apiome-mock attest --bundle ./mock-bundle.json --conformance conformance.json --out mock-attestation.json

A record is always written — a failing corpus produces one saying failed, and no corpus at all produces one saying missing, never silence. The exit code is what fails the job (5 on conformance failure). See Release-proof mock attestation.

Hosted/portable parity​

Passing the corpus proves each deployment is correct on its own. parity proves the two agree — it runs the same corpus against both and diffs every response, which catches drift that per-side pass/fail hides (two runtimes can each satisfy the corpus while disagreeing on something the corpus does not assert):

apiome-mock parity \
--hosted-url https://mock.apiome.dev --hosted-mount /acme/petstore/1.0.0 \
--portable-url http://127.0.0.1:8775
[MATCH] scenario-header-serves-the-canned-response
[DIFF] chaos-delay-is-reported-on-the-response
header x-mock-chaos-delay-ms: hosted '5', portable None
28/28 cases match between hosted and portable (2 deployment-shape cases skipped)

Compared: status, the mock's own semantic headers (X-Mock-*, Content-Type, Allow, Retry-After), and the body — structurally when both sides return JSON, so key order and whitespace never register as a difference. Not compared: transport headers (date, server, content-length), which differ between a container and a hosted service without any behavior differing, and the reserved operational endpoints (/health, /ready), which describe the deployment rather than the contract and are reported as skipped. Exit code is 6 on any difference.

In CI​

The mock action starts a pinned runtime for the rest of a job, hands back a loopback-only service URL, and removes the container automatically when the job ends:

- uses: apiome/apiome/mock-action@v1
id: mock
with:
bundle: petstore-1.0.0-mock-bundle.json
image: ghcr.io/apiome/apiome-mock:0.7.0 # pin a version or a digest
conformance: "true"

- run: npm test
env:
API_BASE_URL: ${{ steps.mock.outputs.service-url }}

The action publishes bundle-digest and runtime-version as outputs and writes them to the job summary. Record or assert them: a green suite proves nothing if nobody can tell which artifact answered it.

As a serverless function​

The same runtime answers a bundle from inside an AWS Lambda, a Google Cloud Run function, or an Azure Function — no container and no open port. It is the same ASGI application this page describes, reached through a narrow event adapter, and the corpus above is run through each provider's real event shape to prove it.

apiome-mock serverless --provider aws-lambda --bundle petstore-1.0.0-mock-bundle.json

Preflight reports the provider's published package, payload, and timeout limits, measures what a cold start actually costs against the provider's budget, and refuses a bundle carrying a cloud credential. Full guide: Serverless mock adapter.

What the portable runtime does not do​

Everything that is inherently hosted stays hosted, because it needs the control-plane database:

  • API-key authentication, private-draft access, per-tenant quotas, and usage accounting;
  • gRPC, SSE, and WebSocket transports (the portable runtime serves HTTP);
  • live publish invalidation — a bundle is a pin, and re-pinning means exporting a new bundle.

Everything else — routing, request validation, example-first resolution, schema synthesis, scenarios, chaos, and stateful CRUD — is literally the same code as the hosted runtime.