Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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#

bash
1
2
3
4
5
6
7
8
# 1. From the project root, start the stack
docker compose -f tools/sandbox/docker-compose.sandbox.yml up -d

# 2. Run migrations (idempotent)
docker compose -f tools/sandbox/docker-compose.sandbox.yml run --rm migrate

# 3. Seed deterministic test data
python tools/sandbox/seed.py

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:

scdoc
1
MOLLIE_TEST_API_KEY=test_xxxxxxxxxxxxxxxx

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#

bash
1
2
3
4
5
6
SUB=sub_sandbox_platform_a_active
curl -X POST http://localhost:8001/api/v1/admin/subscriptions/$SUB/override \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: idmp_$(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"plan_id": "<another-plan-uuid>"}'

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#

bash
1
2
3
4
5
6
7
8
KEY=idmp_$(uuidgen)
for i in 1 2 3; do
  curl -i -X POST http://localhost:8001/api/v1/admin/subscriptions/$SUB/cancel \
    -H "Authorization: Bearer $TOKEN" \
    -H "Idempotency-Key: $KEY" \
    -H "Content-Type: application/json" \
    -d '{"immediate": true, "reason": "sandbox test"}'
done

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:

bash
1
2
3
docker exec scaicontrol-sandbox-worker \
  python -c "from scaicontrol.workers.trial_monitor import emit_trial_endings; \
             import asyncio; asyncio.run(emit_trial_endings({}))"

It scans for trialing subs with trial_ends_at at 7/3/1 days remaining and emits subscription.trial_ending.v1.

Suspend → resume cycle#

bash
1
2
3
4
5
6
7
8
9
SUB=sub_sandbox_platform_a_active

curl -X POST http://localhost:8001/api/v1/admin/subscriptions/$SUB/suspend \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"
# emits subscription.suspended.v1, status → 'suspended'

curl -X POST http://localhost:8001/api/v1/admin/subscriptions/$SUB/resume \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json"
# emits subscription.resumed.v1, status → 'active'

Force-deliver a stuck webhook#

The dispatcher ticks every 30s. To force immediate delivery during a test:

bash
1
2
3
docker exec scaicontrol-sandbox-worker \
  python -c "from scaicontrol.workers.event_dispatcher import dispatch_events; \
             import asyncio; asyncio.run(dispatch_events({}))"

Tear down#

bash
1
tools/sandbox/teardown.sh

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#

Updated 2026-06-29 00:34:15 View source (.md) rev 6