Sandbox & compliance
Business view
A safe place to build
Partner developers don't test against a real clinic's data. Xolize runs a dedicated, shared plugin-dev sandbox — a full Dentolize deployment with synthetic, deterministic demo data that gets wiped and reseeded weekly. A new partner gets a demo-clinic login (so they can see what clinic staff see), a pre-created draft listing they can install without waiting for review, and a dtz_test_ token. Test tokens and live tokens are hard-isolated — a test credential is rejected outright in production, and a live one won't work in the sandbox.
Why the platform can expose clinic data to outsiders at all
This is the part that matters most to anyone answering a security or compliance question about this feature. Dentolize is already the data controller's processor for a clinic's patient data (the clinic is the controller, under Saudi PDPL and comparable regimes). Installing a plugin makes that plugin an independent processor engaged by the clinic, bound by a platform-standard data processing agreement the clinic explicitly accepts at install time — not a contract Xolize signs on the clinic's behalf, and not something the plugin's own listing copy can water down (the DPA and consent language are platform-owned, never authored by the plugin).
Two obligations follow a clinic through the DPA regardless of what the plugin's own privacy policy says: if a patient is deleted, or the plugin is uninstalled, the plugin is contractually obliged to purge its copies of that clinic's data within 30 days — Dentolize enforces this by firing patient.deleted / installation.uninstalled events, but the actual deletion happens on the partner's infrastructure, which Dentolize cannot verify.
Technical view
Sandbox mechanics
- Webhook endpoints in the sandbox may use plain HTTP and tunnel/local addresses — the SSRF guard described in Webhooks is specifically relaxed there.
- Test-token rate limits are intentionally lower (60 weighted points/min vs. 300 live) so a partner hits throttling behavior early in development rather than discovering it in production.
- The seeded reference plugin is
demo-appointment-reminders, upserted idempotently bypackages/server/src/generateServerData/seedDemoPlugin.js(standalone from the byte-identity-lockeddynamicSeed/set, per its own header comment — it's safe to re-run and re-edit). This is the exact plugin captured throughout Walkthrough. - Operationally (Xolize-internal): the sandbox rides the existing per-branch deploy infrastructure, reseeded weekly via a scheduled workflow;
seedDemoPlugin.jsis re-run manually after each reseed to restore the demo listing.
Data minimization mechanics
- Webhook payloads are thin by construction (ids + minimal state) with a PII-key denylist strip as a second net — PII only ever flows through the pull API, under scope control, where it's auditable.
- Serializers are whitelist-only (
compliance/fieldTiers.js): fields the platform never exposes to any plugin (signature images, geolocation, raw internal notes, other integrations' credentials, cross-tenant data) are excluded from the Prismaselectitself — they're not fetched and then filtered, they're never fetched.
Audit trail
ApiRequestLog is an append-only, async-buffered log of every plugin API call: token, installation, company, templated route, status, latency, IP, the specific resourceIds returned, a piiAccessed flag, and a requestId. It's clinic-visible via the pluginApiRequestLogs query (permission MANAGE_PLUGINS) — though note from Plugin Hub that the shipped Activity tab UI doesn't currently surface this query, only webhook deliveries. Token and installation lifecycle events (mint/rotate/revoke, install/uninstall, DPA acceptance) all carry attribution fields (createdBy/revokedBy/installedBy/dpaAcceptedBy).
Deletion & retention
Deletion propagation is signaled via patient.deleted and installation.uninstalled webhooks; the actual purge is a partner obligation under the DPA, not something Dentolize can enforce technically. Uninstalling records dataDeletionRequested when the clinic explicitly asks for it via the Uninstall modal checkbox. Platform-side retention is cron-driven and env-tunable (pluginRetentionCron, packages/server/src/cronJobs/plugins/pluginMaintenanceCron.js): ApiRequestLog rows ~13 months, webhook events/deliveries 90 days, delivery response-body snippets nulled after 30 days (so a stale delivery record can't leak a captured response body indefinitely).
Isolation and suspension
Test and live tokens never share a database; production rejects test tokens outright when PLUGIN_TEST_MODE_ENABLED isn't set. Suspending a listing (from the admin tool — see Plugin registry) invalidates every token issued under it, platform-wide, at the auth layer — not just new installs.
Known, documented gaps
docs/plugin-platform/compliance.md is explicit about what's not solved by this PR, and it's worth repeating rather than glossing over: field-level encryption at rest is a pre-existing platform posture, not something this PR changes; patient-facing PDPL export/erasure tooling is a platform-level backlog item independent of plugins; and a few alternate money paths (e.g. editInvoiceWithPayments's paid-transitions) don't yet emit invoice.paid — tracked as a hardening-pass item, not fixed here. See For Quality for how this should shape test coverage.