Sandbox
A local docker-compose ScaiControl stack with deterministic test data — for integration testing without touching production billing.
The compose file, seed script, and teardown script live in this repo at tools/sandbox/. This page is the single canonical entry-point describing how to use them.
What you get#
| Container | What it runs |
|---|---|
scaicontrol-sandbox-mariadb |
MariaDB 11.4, schema scaicontrol_sandbox, port 3307 (host) |
scaicontrol-sandbox-redis |
Redis 7-alpine, port 6380 (host) |
scaicontrol-sandbox-api |
ScaiControl backend on :8001 (host), Mollie test mode |
scaicontrol-sandbox-worker |
arq worker — cron jobs, event dispatcher, idempotency-record purge |
scaicontrol-sandbox-migrate |
One-shot alembic upgrade head against the sandbox DB |
Ports are configurable via env vars (SANDBOX_API_PORT, SANDBOX_DB_PORT, SANDBOX_REDIS_PORT). Defaults are picked to not collide with the standard dev stack.
Bring it up#
1 2 3 4 5 6 7 8 | |
What the seed creates#
Idempotent — re-running converges on the same state. Use python tools/sandbox/seed.py --reset to clear sandbox rows before reseeding.
- 2 partners:
prt_sandbox_platform,prt_sandbox_reseller - 6 tenants (3 per partner):
tnt_sandbox_<partner>_a/b/c, each with a billing profile - 12 users (2 per tenant): one
tenant_admin, one no-role - 18 subscriptions (3 per tenant): one each in
active,trialing,cancelled— exercises the state machine - Uses the first active+public service+plan it finds in the catalog. If the DB is empty, falls back to creating a placeholder service/plan called
sandbox-svc/trial.
Configuration: Mollie#
The compose file pre-wires MOLLIE_ENABLED=true and reads MOLLIE_TEST_API_KEY from your environment. Get a Mollie test key from the Mollie dashboard (it prefixes with test_) and put it in .env.sandbox:
1 | |
Then source .env.sandbox before docker compose up. Never put a live_ key in the sandbox file — the sandbox can charge real cards if you do.
Stripe and crypto plugins are forced off in the sandbox to keep the surface area minimal.
Scenarios#
Each recipe assumes the stack is up, the seed has run, and you have a sandbox JWT (mint one from the ScaiKey sandbox realm or use the seeded tenant_admin users with the dev-token flow).
Apply a plan change, observe the webhook#
1 2 3 4 5 6 | |
Then watch the worker logs for the subscription.changed.v1 outbox row being fanned out to whichever webhook subscriber you registered.
Retry an Idempotency-Key#
1 2 3 4 5 6 7 8 | |
Calls 2 and 3 return the cached response from call 1 with X-Idempotency-Replayed: true. The worker only emits one subscription.cancelled.v1 event because the outbound idempotency_key derives from the inbound Idempotency-Key.
Trigger trial-ending notification#
The trial monitor runs daily at 09:13 UTC. To trigger it on demand:
1 2 3 | |
It scans for trialing subs with trial_ends_at at 7/3/1 days remaining and emits subscription.trial_ending.v1.
Suspend → resume cycle#
1 2 3 4 5 6 7 8 9 | |
Force-deliver a stuck webhook#
The dispatcher ticks every 30s. To force immediate delivery during a test:
1 2 3 | |
Tear down#
1 | |
Stops every container and wipes the volumes — next bring-up starts deterministic.
Pointing CRM at the sandbox#
Configure your CRM's outbound webhook subscriber and OIDC issuer to:
| Setting | Value |
|---|---|
| API base | http://localhost:8001/api/v1 |
| OpenAPI | http://localhost:8001/openapi.json |
| ScaiKey realm | https://auth.scailabs.ai/realms/sandbox |
| Webhook subscriber target | your CRM's test webhook endpoint |
Register the webhook via the seeded tenant_admin user against POST /admin/webhook-subscriptions and the dispatcher will start sending events on the next tick.
See also#
- API reference — every endpoint with request/response schemas
- Events catalog — webhook payload shapes
- Webhooks operational reference — dispatcher behaviour
- State machines — full subscription / invoice / provisioning transitions