Patterna Docs

Consent modes

Personalized, cohort, non-profiled: what each mode stores and what it never stores.

Everyone 6 min read View as Markdown

Every request to Patterna carries a mode. The mode decides what Patterna may store about the person and which signals it may learn from. Your backend picks the mode from the consent the user gave you. Patterna does not ask the user anything itself.

The three modes at a glance#

personalizedcohortnon_profiled
Identifier sentyour random per-tenant user_idyour random per-tenant user_ida rotating session_id only
User row in Patternayesyesno
Ranking usesthe user's own behaviour, declared attributes, contextdeclared goals and contextcontext only
Seen filterfrom the user's historyfrom the impression log onlyno user history
Events used for learningyesno: learning jobs exclude cohort users' eventsstored with a session id only, for the global policy
Holdout armsticky per usersticky per userfrom a hash of the session id
Shown in the ledgeryesyesyes

personalized#

The user agreed to recommendations based on what they read and do. Patterna keeps a user row with the random id, the mode, a persona and declared attributes (goals, activity level, stress level, wearable, supplement use, premium flag, locale). It learns a per-user model on top of the per-persona one.

What it never stores, in any mode: email, name, IP, device id, free text, coordinates, or any health value. See Data we never store.

cohort#

The user agreed to recommendations based on what they told you, not on what they do. Patterna ranks from declared goals only. There is no behavioural affinity and no seen-filter history beyond the impression log. The learning jobs leave cohort users' events out.

Cohort is a choice you offer. If you do not offer it, nothing maps to cohort and a missing consent means non_profiled. Cohort ranking reads the goals you send with PUT /users/{user_id}/profile. Send the mode with the profile for a new user: a profile without mode keeps an existing user's mode, but for an id Patterna does not know it is refused with 422 mode_required (and for a user who opted out with 422 mode_conflict), so a profile write can never create consent by accident.

non_profiled#

The user said no, or never said yes. The request must carry a session_id and no user_id. Patterna never touches the users table for it. It still serves a good list: a global policy learned from context only (locale, app_version, platform, hour_bucket).

The holdout still covers these users. The arm comes from a hash of the session id, so the ledger includes people who opted out.

Rotate the session id. Mint 128 random bits and rotate them, for example when the app returns to the foreground after 30 minutes. It is not a device id and is never persisted.

Because the session id rotates, one person can land in both holdout arms over a day. Send an assignment_unit on non_profiled requests (a 64-hex hash you compute from your own user id) used only to keep that person in one arm; it is never stored raw and never used for ranking. See Identity across connectors.

Patterna has three modes; you decide which of your consent records maps to which. Write the mapping down once and use it in every place that calls Patterna: the request path, the consent-change handler and any import.

A worked example, from a health app that already asks two consent questions:

Your consent recordMode
personalized_offers granted ("Allow personalized offers and recommendations")personalized
analytics granted, personalized_offers not grantedcohort
neither granted, or no consent row at allnon_profiled

Rules that keep it honest:

  • Missing means no. A user without a consent row is non_profiled. Never treat silence as a yes.
  • Personalization needs a consent that names it. Analytics consent alone is not a basis for profiling on behaviour; that is why it maps to cohort (declared goals only), not to personalized.
  • One mapping everywhere. The request path, the consent-change handler and any one-off import from your database use the same table.

You can also add a new consent type that asks only about recommendations. Compared with reusing an existing offers consent, it gives:

  • a purpose that names recommendations only, separate from marketing offers, which is a cleaner basis for profiling;
  • a toggle you can explain in one sentence next to the lists it controls.

The cost: nobody has given it yet. Everyone starts non_profiled until they answer, so you need a prompt after onboarding and an opt-in rate to watch. The choice belongs to your privacy owner; Patterna works the same either way.

Interaction reporting (seen, tap, dismiss) follows the user's analytics consent: if a user did not allow usage analytics, your app does not report what they saw or tapped. That is independent of the mode, which governs what Patterna may store and rank on.

If you stop reporting for those users without telling Patterna, their slates still count as served with zero taps in both arms. The difference between arms stays unbiased but is diluted, and the tap rate is meaningless as an absolute number.

Send context.measured: false on the recommendation request for a user who did not allow analytics. Serving is unchanged. Unmeasured slates are left out of the ledger, the power curve and learning, in both arms. Consent is given before the arm is drawn, so leaving them out is valid. measured defaults to true.

Changing the mode#

Tell Patterna when consent changes. The call takes the ingest key:

curl
curl -sS -X PUT "https://api.patterna.nl/v1/users/u_mfrggzdfmztwq2lknnwg23tpobyxe/mode" \
  -H "Authorization: Bearer $PATTERNA_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "non_profiled"}'

Switching to non_profiled is an opt-out, with the same semantics as a deletion of the profile. In the request the user row (profile and affinities) is deleted, Patterna stores a salted hash of the id as the opt-out marker, and it files a deletion request that holds only that salted hash. The hourly deletions job then unlinks the user's impressions (the id and the logged candidates are cleared; the salted unit_hash stays, so ledger rows do not change after the fact), keeps their interactions (they belong to the slate), and deletes the user's events, holdout assignments and precomputed shortlist. Serve honours the marker at once: a personalized or cohort request for that id is served as non_profiled and logged without the id, keeping the unit it had and, unless the holdout is stratified by persona (the user has no persona any more), the arm; the audit log records serve.optout_downgrade once per day, without the id. Setting the mode to personalized or cohort again (with this call, or a profile that carries mode) clears the marker. The nightly retrain clears the models.

After the call, forget your mapping for this user. A later opt-in then mints a fresh id that cannot be joined to the old history.

Opt-out versus deletion. Both leave impressions in the ledger, unlinked, and neither deletes them. An opt-out keeps a hashed marker so serve stays non-profiled for that id. A deletion (DELETE /users/{user_id}) also removes that marker and keeps only a hashed record that a deletion happened. See Handle a deletion request.

The endpoint accepts the ingest key (and the admin key). Both SDKs send the admin key when you configure one and the ingest key otherwise.

The SDKs enforce the mode#

Both SDKs refuse a request before anything is sent when:

  • the mode is missing or unknown (consent_required);
  • a non_profiled request carries a user_id or lacks a session_id (consent_violation);
  • a user id looks like an email or an identity-provider subject such as auth0|....

These are bugs in the caller. withFallback and RecommendWithFallback rethrow them instead of hiding them behind the fallback list, because a consent bug is a privacy bug.

The engine checks too: a non_profiled request with a user_id gets 422 mode_conflict, and a personalized or cohort request without one gets 400.

Choosing the default#

Set the default mode in Settings → Consent. A safe default is non_profiled: everyone starts there and personalization begins per user at opt-in. That makes the opt-in rate a number to watch in Audiences.

esc
  • Type to search every page, heading and code sample.