Run Guide¶
This guide covers the public-safe PoC paths in this repository.
Prerequisites¶
- Python
3.12+ uv- Node.js
22+ - Docker with Compose support for the containerized stack
- Xcode for the native iPhone scanner app
Environment Setup¶
From backend:
uv sync --frozen
This creates or updates .venv using pyproject.toml and uv.lock.
From frontend:
npm install
This creates the React 19 / Vite 8 / Tailwind v4 frontend workspace used for the replacement verifier workbench.
Test Suite¶
From backend:
PYTHONPATH=.. ./.venv/bin/pytest
Coverage includes:
- unit tests for replay guard, payload revalidation, and signed schema
- API tests for
/verifier/* - QR artifact tests for PNG render, decode, and scanned verification
- optional Playwright browser regression for the legacy backend-served lab
- live HTTP integration coverage when socket binding is available
- CI and release review rerun the backend, frontend, Compose, navigation, and release-boundary gates; do not infer current results from a historical count
Frontend validation:
From frontend:
npm run lint
npm run build
Current baseline:
- TypeScript build succeeds
- Vite production bundle succeeds
- ESLint passes on the frontend workspace
Documentation Site¶
The repository uses MkDocs for a filtered, locally browsable technical site. From the repository root:
make docs-build
make docs-serve
Open http://127.0.0.1:8088/. docs-build runs MkDocs in strict mode.
docs-serve serves only the generated site/ directory, rather than exposing
the maintainer workspace directly. The release exporter separately checks all
relative links against the complete public repository tree.
Rendered architecture and decision diagrams open in an interactive modal.
Select a diagram, then use the mouse wheel or +/- to zoom, drag to pan,
press 0 to fit, 1 for actual size, and Esc to close.
Links from the documentation to implementation files open generated, read-only source views. The build creates those pages only for files referenced by rendered documentation and only within the public source boundary; it never serves the maintainer workspace directly.
Native iPhone app build validation:
From the repository root:
make smoke-ios
make build-ios
Current baseline:
- the SwiftUI iPhone scanner compiles for the iOS Simulator SDK
- the app includes native QR camera capture, scanner-decision submission, end-user green/orange/red results, local history, Learn, Settings, and immediate haptic/audio feedback
make smoke-ioschecks that developer-only verifier/admin/demo surfaces stay out of the end-user app
Optional browser-layer regression for the legacy static lab:
cd backend
./.venv/bin/python -m playwright install chromium
PYTHONPATH=.. ./.venv/bin/pytest tests/test_verifier_lab_browser.py
This test exercises the real lab page in a headless browser by:
- issuing a verifier API key through the lab UI
- generating a QR artifact
- uploading the generated QR image back into the lab
- verifying the scanned payload and confirming replay blocking on the next action
Demo Scripts¶
From backend:
./.venv/bin/python scripts/replay_guard_poc_demo.py
./.venv/bin/python scripts/payload_revalidation_poc_demo.py
./.venv/bin/python scripts/signed_schema_poc_demo.py
./.venv/bin/python scripts/narrowed_verifier_poc_demo.py
./.venv/bin/python scripts/qr_artifact_poc_demo.py
What each demo proves:
replay_guard_poc_demo.py- reserve, release, finalize, expiry, and one-winner concurrency behavior
payload_revalidation_poc_demo.py- exact host match,
wwwnormalization, subdomain policy behavior, and issuer-state changes signed_schema_poc_demo.py- fixed claim order, deterministic serialization, signature verification, and metadata-conflict rejection
narrowed_verifier_poc_demo.py- full verifier chain across valid, replayed, expired, revoked, mismatch, release-failure, and finalize-failure cases
qr_artifact_poc_demo.py- signed envelope -> QR PNG -> decoded scan payload -> verifier result
FastAPI Reference Surface¶
The canonical reference API is:
GET /verifier/statusPOST /verifier/demo-materialsPOST /verifier/demo-sessionsPOST /verifier/verifyPOST /verifier/verify-scannedPOST /verifier/decode-image
Notes:
POST /verifier/demo-materialsreturns demo certificate data, issuer state, a ready-to-submit verifier request, and QR artifact dataPOST /verifier/demo-sessionsreturns the same demo data plus a shareable session ID and display path so a second screen can render the exact QR the native phone app will verifyGET /verifier/statusreturns the current verifier posture for auth, rate limits, Redis-backed coordination, and decode fallback support- it does not return the signing private key
POST /verifier/decode-imagerejects oversized image payloads before decode- verifier POST routes are rate-limited per client, using Redis-backed coordination when Redis is available and in-memory fallback otherwise
- DB-backed verifier-client keys protect verifier POST routes; static
VERIFIER_API_KEYSrequire explicitVERIFIER_STATIC_API_KEYS_ENABLED=truelocal opt-in - verifier client keys are managed through audited
/admin/verifier-clients/api-keys/*endpoints; retired/verifier/admin/api-keys/*routes return410 Gone - responses include
X-Request-IDfor request tracing - older
/certificates/*,/qrcodes/*, and/organizations/*routes are disabled by default
From backend:
PYTHONPATH=.. ./.venv/bin/uvicorn backend.app.main:app --reload
Then open:
http://127.0.0.1:8000/http://127.0.0.1:8000/docshttp://127.0.0.1:8000/verifier/labif you need the legacy static lab during the transition
React Verifier Workbench¶
The primary interactive client lives in frontend.
From frontend:
npm run dev
By default, Vite proxies /verifier/* to:
https://127.0.0.1:8443
If your backend is running without TLS instead, override the proxy target:
VITE_BACKEND_TARGET=http://127.0.0.1:8000 npm run dev
Use the non-TLS override only when the verifier API is actually listening on
port 8000. If another local app owns that port, Vite will proxy lab requests
to the wrong backend and errors such as Django/gunicorn 500 responses are not
verifier failures.
Then open:
http://127.0.0.1:5173/
Current React workbench scope:
- runtime status
- admin key issue / key list refresh
- scenario-based QR generation
- direct verifier pass against the current QR payload
- live camera capture with browser decode or bundled fallback decode
- QR image upload and decode
- scanned payload verification
- fullscreen QR display for second-screen testing
Optional local HTTPS for Safari or other secure-context camera testing:
FRONTEND_TLS_ENABLED=true \
FRONTEND_TLS_CERT_FILE=../local/https/verifier-lab.pem \
FRONTEND_TLS_KEY_FILE=../local/https/verifier-lab-key.pem \
VITE_BACKEND_TARGET=https://127.0.0.1:8443 \
npm run dev -- --host 0.0.0.0 --port 5173
Native iPhone Scanner¶
The native iPhone scanner app lives in ios/VerifierLabApp.
Use it when you need:
- immediate native feedback for blocked states such as
payload_revalidation - real camera behavior on iPhone without Safari/browser quirks
- an end-user scanner surface for QR decisions produced by the live verifier API
Open in Xcode:
ios/VerifierLabApp/VerifierLabApp.xcodeproj
Local developer endpoint configuration:
- simulator:
http://127.0.0.1:8000 - physical iPhone: generate the ignored Xcode provider profile with
make ios-provider-config
The physical-device endpoint is a developer-side local lab setting. It is not shown as an end-user app field, because production scanner users should receive managed verifier providers rather than pasted raw endpoints.
For a real iPhone on the same Wi-Fi, start the backend with local TLS and admin tokens:
make up-https-admin
If another local stack already owns the default Postgres or Redis host ports, keep this stack isolated by moving only the host-side DB and Redis bindings:
make up-https-admin POSTGRES_PUBLISH_PORT=55432 REDIS_PUBLISH_PORT=6385
If the React dev server is already using 5173, add
FRONTEND_PUBLISH_PORT=5174. The iPhone app still uses the API URL on 8443.
If you want to reuse an existing local Postgres/Redis stack, use:
make ensure-shared-infra-db
make check-shared-infra-network
make up-https-admin-shared-infra FRONTEND_PUBLISH_PORT=5174
This uses the existing Postgres user publisher, creates or reuses database
qr_trust_poc, and stores replay state in Redis DB 5.
Compose applies backend Alembic migrations before the API starts, including the
QR Trust reference tables used by /admin/outbox, /admin/audit, scanner
decision evidence, runtime observations, and event-outbox propagation. The
TypeScript network schema helper remains useful for shared-infra drift checks
and local smoke drills, but compose no longer depends on it as the normal source
of schema truth.
When the shared-infra or compose network database is configured through
QRTRUST_NETWORK_DATABASE_URL, GET /verifier/status also exposes
operator-visible evidence panels for:
- Postgres event-outbox propagation health
- runtime-safety observations written by provider integrations
These panels are read-only. They help operators see propagation and present-time destination-safety posture without making the FastAPI verifier own NATS workers or runtime-provider ingestion.
For production-style management inspection, use the management API through the
CLI. These commands read Postgres-owned state through /admin/*; they do not
read from NATS or mutate trust state.
For the local HTTPS stack started above, set the published API port and local
self-signed TLS mode once in the shell before running host-side qrtrustctl
commands:
export API_PUBLISH_PORT=8443
export QRTRUSTCTL_INSECURE_TLS=true
Without those variables, host-side qrtrustctl defaults to the plain local
Compose API at http://127.0.0.1:8000. You can always override discovery with
QRTRUSTCTL_BASE_URL or --base-url.
local-lab-admin is the local bootstrap credential used by this guide. The
same X-Admin-Token header can carry an active DB-backed management API key
from qr_trust.management_api_keys; /admin/* endpoints enforce the key's
scopes and write its key id into audit rows.
Issue a scoped management key when you want an operator credential that is audited and revocable without keeping the bootstrap token in regular use. The plaintext key is returned only once by the issue command; later list and audit views expose metadata only:
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
management-key-issue \
--label "demo audit operator" \
--scope audit:read \
--scope outbox:read
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
management-key-list
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
management-key-revoke \
--key-id mkey_example
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
outbox-status
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
audit-list
Use audited outbox remediation when a stale or malformed event should stop retrying or when a failed event should be retried after fixing its dependency:
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
outbox-event-remediate \
--event-id evt_mgmt_stale_policy \
--action quarantine \
--reason "stale source policy event"
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
outbox-event-remediate \
--event-id evt_mgmt_stale_policy \
--action retry
Published outbox events are immutable. If delivered trust state was wrong, publish a correcting governance event instead of using remediation to requeue the published row.
Run the production-management live drill when you want one command to exercise
the managed workflow boundary. It proves that destination-policy publication
fails before issuer/domain preconditions, management mutations are idempotent,
issuer enrollment can reach destination-policy publication, governance and
runtime NATS subscribers are authorized through managed allowlists before broad
subject grants are rejected, verifier client keys cannot authenticate to
/admin/*, and an optional failed outbox event can be requeued after a
broker-outage repair:
python backend/scripts/qrtrustctl.py \
--base-url https://127.0.0.1:8443 \
--admin-token local-lab-admin \
--insecure-tls \
management-live-drill \
--idempotency-prefix local-management-drill
python backend/scripts/qrtrustctl.py \
--base-url https://127.0.0.1:8443 \
--admin-token local-lab-admin \
--insecure-tls \
management-live-drill \
--idempotency-prefix local-management-drill \
--outbox-retry-event-id evt_mgmt_failed_after_broker_outage \
--require-outbox-retry
Use --insecure-tls only for local self-signed HTTPS endpoints. Production
operator commands should rely on normal certificate verification.
Use --require-outbox-retry for evidence runs that must prove broker-outage
recovery; the drill fails if no failed outbox event is supplied.
Use the reset-guarded live outbox retry drill when you need the lower-level
Postgres-to-NATS recovery proof. It creates pending authority outbox rows,
marks them failed under a simulated broker outage, reconnects to local NATS, and
asserts the same rows publish after recovery. Because this drill resets
qr_trust, reset Make targets reapply backend Alembic afterward so local
shared DBs return to the canonical migration head:
make check-network-live-outbox-retry
For scripted management mutations, use qrtrustctl with --idempotency-key.
A retry with the same key and same mutation reuses the completed management
result; a retry with different mutation content returns 409 and does not
write another audit or outbox row. Bootstrap root and authority records before
enrolling issuers:
The CLI can also run from the optional management-tools Compose profile when
operator scripts need the same packaged runtime as the API container. Use
make qrtrustctl-container-help to inspect the containerized command surface.
Set QRTRUSTCTL_ADMIN_TOKEN in the shell or pass --admin-token; the container
defaults QRTRUSTCTL_BASE_URL to http://api:8000 and does not add another
database writer.
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-root-demo \
root-program-upsert \
--root-program-id root:qrtrust-demo:2026 \
--name "QR Trust Demo Root" \
--program-scope "demo merchant QR trust" \
--accepted-algorithm-id ES256
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-authority-demo \
delegated-authority-upsert \
--root-program-id root:qrtrust-demo:2026 \
--delegated-authority-id authority:qrtrust-demo:merchant-web \
--name "Merchant Web Authority" \
--authority-type merchant_operator
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-issuer-demo \
issuer-enroll \
--root-program-id root:qrtrust-demo:2026 \
--delegated-authority-id authority:qrtrust-demo:merchant-web \
--issuer-id issuer:acme-demo \
--display-name "ACME Demo" \
--issuer-class business \
--assurance-tier domain_controlled
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-domain-proof-demo \
domain-proof-upsert \
--root-program-id root:qrtrust-demo:2026 \
--delegated-authority-id authority:qrtrust-demo:merchant-web \
--issuer-id issuer:acme-demo \
--domain acme.example \
--proof-method manual_review \
--verification-status verified \
--evidence-ref operator://manual-review/acme.example
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-issuer-active-demo \
issuer-status-update \
--root-program-id root:qrtrust-demo:2026 \
--delegated-authority-id authority:qrtrust-demo:merchant-web \
--issuer-id issuer:acme-demo \
--enrollment-status active
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-policy-demo \
destination-policy-upsert \
--root-program-id root:qrtrust-demo:2026 \
--delegated-authority-id authority:qrtrust-demo:merchant-web \
--issuer-id issuer:acme-demo \
--destination-policy-id policy:acme-demo:web-payments:v1 \
--approved-destinations-json '[{"destination_id":"dest:acme-demo:pay","expected_final_url":"https://acme.example/pay","allowed_hosts":["acme.example"],"allow_subdomains":false,"path_prefixes":["/pay"],"query_policy":"allow_known_payment_query"}]'
Destination-policy upsert is intentionally guarded. It only writes the policy, audit row, and outbox event when the issuer is active and every approved host has a current verified domain proof in the same root, authority, and issuer namespace.
Use the same management transaction path to suspend, revoke, expire, or reactivate an existing destination policy. This is the preferred emergency rollback path; it writes source state, audit history, and an outbox event without direct SQL:
python backend/scripts/qrtrustctl.py \
--admin-token local-lab-admin \
--idempotency-key idem-policy-revoke-demo \
destination-policy-status-update \
--root-program-id root:qrtrust-demo:2026 \
--delegated-authority-id authority:qrtrust-demo:merchant-web \
--issuer-id issuer:acme-demo \
--destination-policy-id policy:acme-demo:web-payments:v1 \
--status revoked
The /operator route also includes a read-only management evidence panel for
the same outbox and audit read paths. Use it for classroom or developer review
when you need to explain what the API and CLI are reading from Postgres.
To smoke-test only the runtime-safety observation report contract, run:
make check-network-runtime-observations
For the reference-network worker path, add local NATS JetStream without making it the source of truth. The local broker uses static PoC users; the Makefile and Compose defaults keep separate credentials for the outbox publisher, governance subscriber, and runtime subscriber.
For the full local HTTPS admin stack with NATS propagation, use
make up-https-admin-shared-infra-nats. That target starts the API, frontend,
broker, outbox publisher, governance subscriber, and runtime subscriber together.
Run qrtrustctl demo-bootstrap first when the governance and runtime subscribers
need their DB-backed allowlist rows.
make up-nats
make check-network-stack-ready
make up-network-outbox-worker
make up-network-governance-subscriber-worker
make up-network-runtime-subscriber-worker
make check-network-worker-drill
The shared-infra preflight refuses the existing publisher database as the QR
Trust database name, checks the separate Postgres database, checks Redis DB 5,
and reports whether the optional NATS monitor is already online. Host-side Redis
checks use EXTERNAL_REDIS_SETUP_HOST (default 127.0.0.1), while Compose
containers still use EXTERNAL_REDIS_HOSTNAME (default host.docker.internal).
Use make check-network-stack-ready as the non-destructive readiness gate for
the reference-network path. It checks shared Postgres, Redis DB 5, and live
NATS propagation without resetting the QR Trust schema. Use
make check-network-worker-drill only when you want the stronger local worker
proof; it runs the reset-guarded live worker smoke against the separate
qr_trust_poc database.
When overriding local NATS credentials, use the service-specific variables
QRTRUST_OUTBOX_NATS_USER, QRTRUST_GOVERNANCE_SUBSCRIBER_NATS_USER, and
QRTRUST_RUNTIME_SUBSCRIBER_NATS_USER with their matching password variables.
Do not collapse all workers onto one NATS user; the local broker permissions are
intentionally narrower per worker role.
Use make up-network-governance-subscriber-worker after authorizing the
subscriber through /admin/nats/subscribers or qrtrustctl demo-bootstrap.
The worker loads QRTRUST_GOVERNANCE_SUBSCRIBER_ID from Postgres, applies the
DB-managed subject allowlist, fetches authoritative artifacts by reference, and
materializes verifier-cache state. Tail it with
make logs-network-governance-subscriber-worker.
Use make up-network-runtime-subscriber-worker after authorizing a subscriber
for qrtrust.*.runtime.verdict.observed.v1 or a broader runtime family subject
that covers it. qrtrustctl demo-bootstrap creates the default
subscriber:runtime-observations authorization alongside the governance
subscriber. The worker loads QRTRUST_RUNTIME_SUBSCRIBER_ID from Postgres, fetches
runtime-safety-observation artifacts by reference, validates artifact hashes,
and persists normalized rows into qr_trust.runtime_observations. Tail it with
make logs-network-runtime-subscriber-worker.
For federation, stale-cache, or cross-root contradiction demos, start the optional second verifier node:
make up-secondary-verifier-node
make logs-secondary-verifier-node
The second node runs the same backend image under the verifier-federation
Compose profile, publishes on port 8001, uses verifier id
verifier:reference-http-runtime-b, and keeps Redis state separate from the
primary verifier. It still reads QR Trust source state from Postgres; the
different verifier id lets cache entries and scanner decisions be compared
without adding a second source-of-truth database.
Before capturing native evidence, run:
make ios-provider-config
make check-ios-provider-config
make iphone-evidence-preflight
This writes and validates the ignored local provider profile, confirms the HTTPS verifier is reachable, prints the primary local verifier candidate, detects a paired physical iPhone, and reports if Developer Mode is disabled.
Native app scope:
- scan QR codes with the native camera
- submit the scanned payload to
/scanner/decisions - show green, orange, or red user-facing decisions
- preserve the destination so the user can still choose whether to open it
- show the four-layer decision path for reviewers or curious users
- keep local scan history according to the selected retention setting
Deterministic cross-device rule:
- generate the demo QR from the browser lab on the laptop
- do not click the browser lab's
Check scanner decisionaction before the phone scan - scan that laptop QR from the iPhone app
For the primary native mismatch test:
- browser lab
Scenario = payload-mismatch - browser lab
Nonce label = timestamped - expected iPhone result = red
Destination changed - expected decision-path stage =
payload_revalidation
See IPHONE_TEST_PLAN.md for the full step-by-step drill.
Important Runtime Note¶
main.py attempts a Redis connection during startup when REDIS_STARTUP_ENABLED=true. If Redis is not running, startup logs an error and warning, but the narrowed PoC still works because the verifier reference flow currently uses the in-memory replay guard from verifier.py. Set REDIS_STARTUP_ENABLED=false when you want deterministic no-Redis startup behavior.
Containerized Stack¶
Preferred local shortcuts from the repository root:
make up
make up-admin
make up-lan
make up-lan-admin
make up-https-admin
make up-admin is the standard local path when you want the workbench to issue verifier API keys with:
VERIFIER_ADMIN_TOKENS='["local-lab-admin"]'
VERIFIER_BOOTSTRAP_ADMIN_TOKENS_ENABLED=true
Production should leave VERIFIER_BOOTSTRAP_ADMIN_TOKENS_ENABLED=false and use
DB-backed management keys instead of config admin tokens.
From the repository root:
docker compose up -d --build
docker compose logs -f api
This starts:
postgresredisapifrontend
The published API port defaults to 127.0.0.1:8000 for local development.
The published frontend port defaults to 127.0.0.1:5173.
The published Postgres and Redis ports default to 127.0.0.1:5432 and
127.0.0.1:6379.
For phone or tablet testing on the same Wi-Fi, override the API and frontend host bind:
API_PUBLISH_HOST=0.0.0.0 FRONTEND_PUBLISH_HOST=0.0.0.0 docker compose up -d --build
If you want the frontend workbench reachable on the LAN as well:
API_PUBLISH_HOST=0.0.0.0 FRONTEND_PUBLISH_HOST=0.0.0.0 docker compose up -d --build
You can also override the published port:
API_PUBLISH_HOST=0.0.0.0 API_PUBLISH_PORT=8010 docker compose up -d --build
When another local stack already owns the default host ports, prefer this side-by-side command:
make up-admin API_PUBLISH_PORT=8010 FRONTEND_PUBLISH_PORT=5174 POSTGRES_PUBLISH_PORT=5433 REDIS_PUBLISH_PORT=6380
The PoC still uses its own containerized postgres and redis services; these
overrides only move their published host ports away from existing projects.
After the stack is running, verify both the backend verifier contract and the React workbench routes:
make smoke-compose FRONTEND_PUBLISH_PORT=5174
For an HTTPS stack started with make up-https-admin or
make up-https-admin-shared-infra, use:
make smoke-compose-https FRONTEND_PUBLISH_PORT=5174
The smoke target checks:
/verifier/status- API key issue, QR demo-material generation, first accepted verification, and replay rejection
- the React root route
- the lab comparison route at
/?scenario=payload-mismatch&nonce=fixed&autogenerate=1&compare=valid
For the React app itself, run the route-query navigation smoke while the dev server is active:
make check-route-navigation FRONTEND_DEV_PORT=5173
That browser check pushes query-only route changes inside the same page session
and verifies that /, /about, and /operator refresh their state. It is
the regression guard for stale guided-track, scenario, or operator-handoff
state when only the query string changes.
If you run the React dev server outside Docker on 5173, point its Vite proxy
at the published compose API port:
make dev-frontend API_PUBLISH_PORT=8010
The Vite proxy derives its default backend target from API_PUBLISH_HOST and
API_PUBLISH_PORT. Use VITE_BACKEND_TARGET=http://127.0.0.1:8010 only when
you need to override that derived target explicitly.
To capture browser evidence for the primary engineering outcomes, keep the dev server running and execute:
make capture-browser-evidence FRONTEND_DEV_PORT=5173
The capture script stores:
docs/public/evidence/browser/accepted.pngdocs/public/evidence/browser/replay-guard.pngdocs/public/evidence/browser/payload-mismatch.png
The compose flow runs Alembic migrations before starting the API. To stop the stack:
docker compose down
The current compose baseline was validated by:
docker compose configdocker compose up -d --build- live verifier request cycle executed inside the API container
If your host already uses port 8000, change API_PUBLISH_PORT.
If your host already uses 5173, change FRONTEND_PUBLISH_PORT.
If your host already uses 5432 or 6379, change POSTGRES_PUBLISH_PORT or
REDIS_PUBLISH_PORT.
Phone Testing¶
To open the React workbench from your phone:
- Start the stack with LAN publishing:
export VERIFIER_ADMIN_TOKENS='["local-lab-admin"]'
export VERIFIER_BOOTSTRAP_ADMIN_TOKENS_ENABLED=true
API_PUBLISH_HOST=0.0.0.0 FRONTEND_PUBLISH_HOST=0.0.0.0 docker compose up -d --build
- Find your Mac's LAN IP. Common commands on macOS are:
ipconfig getifaddr en0
ipconfig getifaddr en1
- On the phone, while connected to the same Wi-Fi, open:
http://<mac-lan-ip>:5173/
-
Open the React verifier workbench directly.
-
If verifier auth is enabled, enter the admin token in the workbench, issue a key, and the client will auto-load that key into the API key field.
Practical testing note:
- if you want to test the phone camera scanning path, display the generated QR on a second device such as your laptop screen
- if you only want to test the phone browser, upload a QR image or use
Verify current QR without scanning - if you scan the generated QR with the native Camera app instead of the workbench, the phone will open the QR payload URL directly and bypass verifier checks
- on iPhone Safari over plain
http://<mac-lan-ip>:...,getUserMediamay be unavailable because the page is not in a secure context; use image upload, direct verify, or HTTPS if you need camera capture
Local HTTPS With mkcert¶
If you want true iPhone Safari camera capture, use local HTTPS instead of plain LAN HTTP.
Generate local certs from the repository root:
./scripts/create_local_https_certs.sh
That creates ignored local files under local/https/:
verifier-lab.pemverifier-lab-key.pemmkcert-rootCA.pem
Start the stack with TLS enabled:
FRONTEND_PUBLISH_HOST=0.0.0.0 \
FRONTEND_TLS_ENABLED=true \
VERIFIER_ADMIN_TOKENS='["local-lab-admin"]' \
VERIFIER_BOOTSTRAP_ADMIN_TOKENS_ENABLED=true \
docker compose up -d --build
Then open:
https://<mac-lan-ip>:5173/
Important iPhone trust step:
- Move
local/https/mkcert-rootCA.pemto the iPhone. - Install the profile/certificate.
- Enable trust in:
Settings > General > About > Certificate Trust Settings
Without trusting the mkcert root CA on the phone, Safari will not treat the React workbench as a secure context for camera capture.
Minimal API Flow¶
- Call
POST /verifier/demo-materials - Use either:
- the returned
verify_requestwithPOST /verifier/verify, or - the returned
qr_payloadwithPOST /verifier/verify-scanned - Re-send the same request to observe replay blocking
Browser Clients¶
Primary React workbench:
http://127.0.0.1:5173/in local developmenthttp://<mac-lan-ip>:5173/for LAN testing
It provides:
- runtime posture loading from
GET /verifier/status - scenario-based demo QR generation
- camera scanning using the browser
BarcodeDetectorAPI when available - bundled verifier-side image decode fallback for browsers without
BarcodeDetector - fallback camera decoding that pauses/resumes on
429instead of failing hard - selectable camera sources for multi-camera devices
- QR image upload decoding
- QR PNG download, payload copy, and fullscreen QR display for cross-device testing
- direct submission to
POST /verifier/verify-scanned - verifier API key issue and list refresh when admin tokens are configured
- result history across verifier stages
Legacy compatibility page:
GET /verifier/lab
Keep it only while the React workbench is being validated across real devices or while you still need the backend-served page for comparison.
The optional browser regression test for the lab lives at:
The landing page for manual phone testing is served from:
GET /
If API key auth is enabled on the server, enter the key in the lab's API key
field. The value is stored only in local browser storage and attached as
X-API-Key on verifier POST requests.
If you run a separate frontend origin instead of the built-in lab, set
CORS_ORIGINS explicitly. The default public-safe runtime is same-origin.
Compose-Backed Live HTTP Smoke¶
If you want a real HTTP verification cycle against the running server rather
than TestClient, set an admin token and run the smoke script:
export VERIFIER_ADMIN_TOKENS='["local-lab-admin"]'
export VERIFIER_BOOTSTRAP_ADMIN_TOKENS_ENABLED=true
docker compose up -d --build
cd backend
VERIFIER_SMOKE_ADMIN_TOKEN=local-lab-admin ./.venv/bin/python scripts/verifier_live_http_smoke.py
The smoke script calls the live /verifier/status,
/admin/verifier-clients/api-keys/issue, /verifier/demo-materials, and
/verifier/verify-scanned endpoints over HTTP.
Request Logging Discipline¶
The public verifier surface uses sanitized request logging:
- request logs contain request ID, method, path, status, and duration
- request and QR payload bodies are not logged
- noisy third-party request logs are reduced to keep verifier traces readable
Legacy Experimental API¶
The older broad API surface is intentionally not exposed in the default public runtime:
/certificates/*/qrcodes/*/organizations/*
Those routes perform unauthenticated write operations and do not match the
current public PoC boundary. They can be re-enabled locally with
ENABLE_LEGACY_EXPERIMENTAL_API=true if you explicitly want to inspect or
refactor them.
Public Scope Boundary¶
This run guide does not cover:
- filing artifacts
- private form generation
- patent-source documents
- local-only materials under
private/