Sandbox mode lets developers run the full schema -> validate -> ingest ->
retrieve warehouse workflow entirely locally, with no network access and no
authentication. Sandbox mode is enabled by default, so that development
work never reaches the production warehouse by accident: the same
make_request() calls are routed to a local, in-memory warehouse instead of
the REST API. It is disabled for deployment with
options(csiapps.sandbox = FALSE) (or by setting CSIAPPS_ENV=production),
and can be overridden per call with the sandbox argument of
make_request(). Because the production and sandbox workflows use the same
calls, no code changes are needed when moving from development to deployment.
The sandbox emulates three endpoints:
GET api/warehouse/data-sources/{uuid}– returns the schema previously registered withregister_sandbox_schema(), nested underhead_primary_definition$schemaexactly like the real API.POST api/warehouse/ingestion/primary/– validatesbody$recordsagainst the registered schema forbody$source(usingjsonvalidate) and, on success, stores them in memory and writes the JSON payload to a per-source folder on disk for inspection (seebrowse_sandbox()).GET api/warehouse/data-records– returns previously ingested records forquery$source_uuid, each wrapped in the same envelope (data,subject,created_at, ...) the real API returns.
Any other endpoint raises an error. Sandbox state lasts for the R session;
use clear_sandbox() to reset it between tests.
Shiny app wrappers
Sandbox mode also lets a wrapped Shiny app (see ui_wrapper(),
server_wrapper(), check_secrets()) run locally without the OAuth2
redirect. The redirect exists only to obtain an access token, and requires
client credentials that cannot be safely distributed, so in sandbox mode
server_wrapper() simulates the login instead: it seeds the session from
the developer's existing CSIAPPS_ACCESS_TOKEN and hands it to the same code
path a production login would. The token is used only to emulate the
login: if it is present, /me is loaded from the real API so the header
shows the developer's real identity. All data – sport organizations,
athletes/profiles, and warehouse records – is served from the local sandbox
(the dummy registry and the emulated warehouse), never the live API. If no
token is set, the app shell still renders but shows an unauthenticated notice
prompting the developer to set a read-only CSIAPPS_ACCESS_TOKEN. The same
wrapped-app code runs in both modes; only the csiapps.sandbox option differs.
Limitations
The access token is used only to emulate login. A wrapped app's
/meidentity is fetched from the real API with your token, so set the institute withset_institute()to match the institute that issued it or/meis rejected. Everything else is dummy: sport organizations and athletes come from the local registry (create_sport_org(),create_profile()) and warehouse reads/writes are emulated in memory. No real client data is read.Only warehouse endpoints are routed through
make_request(). Callingmake_request("api/registration/...")in sandbox raises a 501; use thefetch_org_options()/fetch_profiles()helpers, which read the dummy registry instead. In sandbox,fetch_profiles()honours only thesport_org_idfilter; other filters are ignored. The sandbox faithfully simulates the schema contract, not the warehouse. Anything that depends on server-side state will differ from production:Validation parity is approximate. Sandbox ingestion validates records against the JSON Schema with Ajv (via
jsonvalidate), which catches the most common failure modes (missing required fields, wrong types, length and pattern violations). The real server additionally enforces things the schema cannot express: resolution ofsubject_fieldvalues against registered profiles, duplicate/dataset handling against existing data, and token permissions. Validator implementations may also differ on edge cases (e.g.formatkeyword enforcement). Passing sandbox validation is therefore a necessary but not sufficient condition for production acceptance – do not treat a green sandbox run as a guarantee.subjectlinkage is emulated only for registered athletes. On retrieval the sandbox resolves each record'ssubject_fieldvalue against athletes registered withcreate_profile()and returns the matched athlete (id, name, sport) in the record envelope, mirroring production. Resolution happens at read time, so athletes registered after ingestion are linked on the next read. If no athlete matches – because none was registered or thesubject_fieldvalue is mistyped –subjectisNULLrather than fabricated. Production would reject an unresolved subject; the sandbox accepts it silently, so a green sandbox ingest does not guarantee the server will link every record.
