MainSDK
Search⌘K
Interfaces

Identity and access

Context and shared context

context

One request context every interface reads, and consent-gated, first-party shared context (settings and progress) between peer apps of one owner on one machine.

Building for 0.4ships as a beta APIPhase 1 · Core, public betaSpec contextFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

context is the one request context every interface reads, plus consent-gated first-party context shared between peer apps of one owner on one machine.

At a glance

context:
  group: acme-learning
  identity: auth
  purposes:
    personalise:
      description: "Use the same language and theme in every Acme app"
      attributes: [locale, theme]
      retention: 30d
      share-with: [prj_01JD8X]
  fields:
    attributes:
      favourite-subject: { type: string, enum: [maths, science] }
    events: [lesson.completed]
  sinks: []

Calls

  • context.current()
  • context.run(patch, fn)
  • context.capture()
  • context.restore(snap, fn)
  • context.shared.put({ purpose, attributes?, events? })
  • context.shared.get(purpose)
  • context.shared.list({ purpose?, cursor?, limit? })
  • context.shared.erase({ user?, purpose? })
  • context.identity.reset()
  • parseContext(raw, opts?)
  • contextJsonSchema(extras?)
  • deriveContextId(groupKey, { issuer, tenant, subject, epoch })

Guarantees

Whatever the driver.

  • Nothing flows without a recorded purpose and consent on both sides
  • Never an advertising or device id; the person can reset the id
  • Stays on the machine unless a sink inside the app is declared

Drivers

1 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (the app link) (default)
context:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-CONTEXT-0001Checkmainsdk check, before deploycontext is invalid: no group, a purpose without a description, a field outside the built-in schema or the app's declared extras, retention over 90d, a share-with app that isn't a mutual peer, or a sink other than db or events
MAIN-CONTEXT-0006Checkmainsdk check, before deployShared context in an app with users.audience: children, or a purpose touching a children's audience, without the purpose marked children: guardian-consent and spec privacy-lifecycle's age gate in place
MAIN-CONTEXT-0002RuntimeError responses and logsSee the spec
MAIN-CONTEXT-0003RuntimeError responses and logsSee the spec
MAIN-CONTEXT-0004RuntimeError responses and logsSee the spec
MAIN-CONTEXT-0005RuntimeError responses and logsSee the spec
MAIN-CONTEXT-0007RuntimeError responses and logsSee the spec

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
The mainsdk-context@1 schema and purposesPhase 10.4context#schema
Consent gatingPhase 10.4context#consent
Shared identity from auth or a first-party idPhase 10.4context#identity
Context over the app linkPhase 10.4context#bus
Minimisation, retention and tenant isolationPhase 10.4context#minimise
Export and erasure with the personPhase 10.4context#lifecycle

Quick start

main.yaml
context:
  group: acme-learning
  identity: auth
  purposes:
    personalise:
      description: "Use the same language and theme in every Acme app"
      attributes: [locale, theme]
      retention: 30d
      share-with: [prj_01JD8X]
app/personalise.ts
import { app } from "mainsdk";

const ctx = app().sharedContext;
const user = { id: "u_123" };

// 1. Ask: show the description and the apps that receive it, then record the person's answer.
const ask = ctx.consent.ask("personalise");
await ctx.consent.set(user, "personalise", true);

// 2. Share: sent only to peers named under the purpose, and only when both apps hold a yes.
const { sent, held, refused } = await ctx.shared.put(
  { purpose: "personalise", attributes: { locale: "hi-IN", theme: "dark" } },
  { user },
);

// 3. Read what peers shared with this app.
const { attributes } = await ctx.shared.get("personalise", { user });

Configuration

main.yaml
context:
  group: acme-learning          # required: lower-case letters, digits and dashes
  identity: auth                # auth | first-party | both
  purposes: {}                  # one entry per declared purpose, see below
  fields: {}                    # up to 16 extra attributes and event types
  sinks: [db]                   # optional onward copies: db or events
KeyTypeWhat it does
groupstringThe apps that share context. Required; peers must declare the same group
identitystringauth derives the id from the signed-in subject, first-party makes a random one, both allows either
purposes.<name>mapdescription, attributes, events, retention (default 30d, at most 90d), share-with and, for a children's app, children: guardian-consent
fieldsmapExtra attributes and events beyond the seven built-in keys
sinkslistWhere received context may be copied on this host: db or events; anything else fails MAIN-CONTEXT-0001

Purposes

Every flow is for a declared purpose. A purpose has a description shown to the person, the attributes and events it may carry, a retention (default 30d, at most 90d) and share-with, the peer apps that receive it. Consent is per purpose; there is no "all purposes" switch. Built-in attribute keys are locale, timezone, theme, reduced-motion, text-size, high-contrast and units. Declare up to 16 extra attributes and event types under context.fields:

main.yaml
context:
  fields:
    attributes:
      favourite-subject: { type: string, enum: [maths, science] }
      streak: { type: number }
    events: [lesson.completed]

A string field needs an enum or a max-length of at most 64.

Identity

identity: auth derives the id from the signed-in subject, so peers that sign people in through one issuer get the same cx_ id without exchanging the subject. identity: first-party makes a random id when the person first grants a purpose, stored in the group's store. The id differs per tenant and per group, and is never derived from a device, hardware, advertising id, IP address, fingerprint or email. The group key is 32 random bytes in <run>/context/<group>.key (mode 0600) and never leaves the machine.

What you get

context has two halves. The request context (context.current(), context.run()) is one place every interface reads who is calling and on which trace; it belongs to spec cohesion and is built: one AsyncLocalStorage in core/context.ts holds tenant, user, agent, region, trace, deadline and signal; adapters set it once per request; jobs, workflow runs and outbox rows save context.capture() and run inside context.restore(), so a log record carries the request's trace_id and agent in the job and every workflow step. Shared context (context.shared.*, context.identity.reset()) lets several MainSDK apps from one publisher on one machine share a small typed picture of a person (language, theme, accessibility settings, a finished lesson) so each app serves them better, and only because that person said yes to a stated purpose. It is not an ad-tracking SDK: no advertising or device id is ever involved, nothing leaves the machine unless you declare where, and the person can see it, export it, reset the id that links it and erase it everywhere.

Single apps and single serverless apps get nothing from shared context. For shared sign-in across separate deployments, use auth.

Calls

CallWhat it does
context.shared.put({ purpose, attributes?, events? })Share fields with the peers the purpose names. Returns sent, held (peers without consent) and refused (fields or peers refused, with the code).
context.shared.get(purpose)Read the current person's received attributes and events for a purpose, never past its retention.
context.shared.list({ purpose?, cursor?, limit? })The current person's flows by purpose and peer, for their own data page. It never lists other people.
context.shared.erase({ user?, purpose? })Erase the person's context here and in every peer that received it. Returns a receipt, the peers reached and those still pending.
context.identity.reset()The person's own action: erase their context in every peer, then change the id, so old data no longer links to them.
parseContext(raw, opts?)Read the context section of main.yaml into settings and a list of issues, without throwing.
contextJsonSchema(extras?)The JSON Schema of a shared context (mainsdk-context@1), with any extra fields the app declares.
deriveContextId(groupKey, { issuer, tenant, subject, epoch })The same pseudonymous id for the same person in every app of a group, derived with an HMAC of the group key.
context.current(), context.run(patch, fn), context.capture(), context.restore(snap, fn)The request context of spec cohesion. current() reads it, run changes user, agent, region, env, trace, deadline, signal or debug for one call (the tenant changes only through tenant.run and tenant.system), capture() returns it as JSON and restore runs a function inside a saved one with a new span in the same trace.

What it always does

  • Nothing flows without a recorded purpose and consent in both apps. Without a yes here nothing is sent; a peer without its own yes drops the message and answers held.
  • Only fields the purpose lists are sent or stored. Strings are at most 64 characters; fields labelled personal, sensitive, child or phi, and values that look like a secret, email, phone number or card, are refused (MAIN-CONTEXT-0004).
  • Received context is stored per tenant, expires at the purpose's retention and is read only for the same tenant and person (MAIN-CONTEXT-0005 for a cross-tenant message).
  • Withdrawing consent stops the flow at once and sends an erase to every peer that received it. A peer that is down gets the erasure queued and is listed as pending (MAIN-CONTEXT-0007).
  • Every flow is audited with the purpose, the peer, field names and counts, never a value and never a raw user id.
  • It stays on the host. The only onward copies are the sinks you declare under context.sinks (db or events); anything else is refused at check.
  • A children's app (users.audience: children) must mark each purpose children: guardian-consent (MAIN-CONTEXT-0006).

See what is shared

mainsdk context (and mainsdk info) show the group, purposes, share-with, sinks and, per purpose, how many people consented and how many flows went out and came in. They hold no context id, user id or value.

Errors and checks

CodeWhenWhat to do
MAIN-CONTEXT-0001check: context is invalid (no group, a purpose without a description, an unknown field, retention over 90d, a share-with app that is not a mutual peer, a sink other than db or events)Fix the key the finding names
MAIN-CONTEXT-0002A share or receive was held: no recorded consent on this side or the peer'sRecord the person's yes for the purpose
MAIN-CONTEXT-0003A peer is not first-party: another owner, or another groupUse peers of one owner and one group
MAIN-CONTEXT-0004A field or value was refused: unlisted, over 64 characters, labelled, or looks like a secret, email, phone or cardSend only fields the purpose lists
MAIN-CONTEXT-0005A cross-tenant flow was refusedShare only inside one tenant
MAIN-CONTEXT-0006check: a children's app without children: guardian-consent on the purposeMark the purpose
MAIN-CONTEXT-0007An erasure could not reach a peer; it is queued and listed as pendingStart the peer; it erases on its next start

Each code's cause and fix is in check codes.

Swapping drivers

DriverProvider
builtinBuilt in (the app link)default

builtin is the only driver, so there is nothing to swap yet. Your calls do not depend on how peers are reached.

Testing

createTestApp builds the app from main.yaml, so app.sharedContext runs the same consent and minimisation rules in memory. The unit tests for the schema, consent, tenant isolation and erasure live in packages/sdk/test/context-shared.test.ts and packages/testing/test/context-shared.test.ts.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet