Security
Signing, access control, the browser events endpoint, privacy, and the limits you must enforce at your platform.
Signing
The visitor cookie, the x-xp-assignments and x-xp-request headers and preview tokens are signed with HMAC-SHA256, with a separate purpose in each signed input, and verified in constant time. A browser cannot choose an identity, a version, a request context or a preview group by editing a value. The manifest is private: it is read from the database by the proxy, or from an endpoint that needs a key derived from the secret.
Rotate the secret by passing secret: [newSecret, previousSecret]. The first entry signs; every entry verifies.
Access control
- Every plugin collection and global states each access operation explicitly: create, read, update, delete, readVersions and unlock.
- Managers operate tests, layers and goals. Reading results and reading identities have separate permissions (
access.readResults,access.readIdentities). - Writes to events, identities, rounds, snapshots and state through ordinary collection access are denied. Only the plugin's own privileged operations write them, each after its authorised action and in the same transaction.
- Request handlers and admin views pass
reqandoverrideAccess: false. Content writes when making a winner permanent use the manager's own access. linkIdentitydeliberately uses its own insert-ignore connection, so a duplicate link cannot abort a sign-in.
Browser events
POST /xp/events receives viewport, pageview and manual exposures, client conversions and clicks.
| Rule | Value |
|---|---|
| Authentication | A valid signed visitor cookie |
| Body limit | 8192 bytes by default (events.maxBodyBytes), read as a stream |
| Events per body | 20 |
| Content type | JSON is parsed regardless of content type, for sendBeacon compatibility |
| Rate limit | Your events.rateLimit adapter, when supplied |
| Response | 202 {} for both accepted and dropped events; malformed requests can return errors |
Exposures must name the current round, policy and the version actually rendered; the server recomputes the assignment and drops mismatches, counting them separately. Only manual, viewport and pageview exposures are accepted. Conversions need a client-writable source and are never revenue; client values are stored as 1 and attributes are allow-listed. Click conversions are accepted only for defined click-goal keys; undefined keys are dropped with 202, and defined keys are cached for up to 30 seconds.
Enforce timeouts at your platform
The real bound on slow request bodies is your firewall and platform request timeout. A stream under the byte limit can stay open, and Payload can read an X-HTTP-Method-Override body before the plugin's streamed limit runs. Add firewall rules and request timeouts for /api/xp/events.
There is no built-in in-memory rate limiter. Supply one that is shared across your instances.
Privacy
- Visitor ids are random and opaque. Store no personal data beyond opaque unit ids: keep names, emails and other identifying values out of attributes, flag point settings and event data.
events.dimensionsrefuses identifying names such asuserId,emailandipat startup.cookie.consentlets you enrol only after consent.eraseUser(payload, { userId })deletes a user's identity links and events.- Events are deleted 180 days after a test's final round ends by default.
- Plugin logs carry error names, codes and stack frames, not error messages or database details. Results and make-content reports store reason codes, not raw error text.
Bots and staff
Bots matched by the built-in user-agent list, staff identified by your attributesOf, preview renders and HEAD requests never record exposures.

