Skip to content

Context

Context is a small set of notes your app keeps about a visitor. Your event handlers receive the notes with each event and run no query, and other apps on the site can use them if you allow it. Context lives with each visitor, so it grows with your traffic and you have no database to size. For what context is and why it exists, read Context first.

Private Shared
Who can read it Only the app that wrote it Every app on the site with sharedContext
Who can write it Each app, its own entry Each app with sharedContext, its own entry
Capability None sharedContext
Reaches the browser Never Yes, and browser code can only read it
Size of one entry 32 KiB 4 KiB

Your app picks the level each time it writes, and nothing moves from one level to the other on its own.

An entry belongs to the app, not to an install. If a site installs your app twice, both installs read and write the same entries for a visitor. Put the install id in your keys if the installs must keep separate values.

An entry has two independent parts and a timestamp.

  • kv is a map of keys to values. A value is a string, a number or a boolean, and it comes back as the same type.
  • text is one free-form string, or null. Use it for something a person or a language model reads, such as a summary.
  • updatedAt is when the entry last changed, in milliseconds since the epoch.

An entry keeps only its current value. It has no history, so read updatedAt if you need to know how fresh a value is.

Private context needs no declaration. To use shared context, add the capability to your manifest.

{
"capabilities": {
"data": ["sharedContext"]
}
}

An event handler receives both levels as plain values and writes with set.

import { defineApp } from '@blotout/qinto-sdk-app/server'
export default defineApp({
onEvent: {
track: async (action, ctx) => {
ctx.context.private.set({ kv: { last_event: action.payload.event } })
if (action.payload.event === 'Purchase') {
ctx.context.shared.set({ kv: { has_purchased: true } })
}
// Read what another app shared. The key is that app's slug.
const tier = ctx.context.shared.byApp['loyalty']?.kv['tier']
if (tier !== undefined) {
ctx.observability.log('visitor tier', { tier })
}
}
},
onDeleteRequest: async () => {}
})

set merges a patch into your entry. It never replaces the whole entry.

In the patch What happens
A key with a value The key is written
A key set to null The key is deleted
A key you leave out The stored value stays
text with a string The text is replaced
text: null The text is cleared
No text The stored text stays

Several set calls on one level in one handler combine into one write. For the same key, the last call wins.

set throws at once on a patch it cannot store: an empty key, a number that is not finite such as NaN, or an empty text. Pass null to clear the text.

How your code reaches a visitor depends on where it runs.

  • An event handler always runs for a visitor, and it already has their context.
  • An rpc function or a workflow takes its visitor from whatever called or started it. It cannot name a different one. When there is no visitor, for example a workflow started from a schedule, reading or writing either level throws. Check ctx.user in an rpc function and ctx.userId in a workflow first. Each is null when there is no visitor.
  • An endpoint, a schedule or a queue consumer names a visitor, using an anon_… id that you stored earlier or looked up.
Where your code runs Read Write
onEvent, onConnect, transform ctx.context.private.entry, ctx.context.shared.byApp ctx.context.private.set(…), shared.set(…)
Rpc function, called for a visitor await ctx.context.private.load(), shared.load() set(…), as in an event handler
Workflow, started for a visitor await ctx.context.private.load(), shared.load() await ctx.context.private.set(…)
Endpoint, schedule, queue consumer await ctx.context(anonId).private.load() await ctx.context(anonId).private.set(…)
Browser code app.onSharedContext(…), shared level only Not available
Agent tool, settings panel Not available Not available

In browser code, register a handler. It runs when the page connects and again each time any app changes shared context.

import { defineBrowserApp } from '@blotout/qinto-sdk-app/browser'
export default defineBrowserApp((app) => {
app.onSharedContext((context) => {
if (context['loyalty']?.kv['tier'] === 'gold') {
document.body.classList.add('gold-member')
}
})
})

The handler receives an empty object when no app has shared anything yet, so you can tell “nothing shared” from “not loaded”.

The limit applies to the whole entry after the merge. A small patch can be refused because the stored entry is already close to the limit. When that happens, the whole patch is refused and the stored entry does not change.

The same refusal applies when your app lacks the visitor’s consent, or writes to the shared level without the sharedContext capability. What you see depends on where you called set.

  • In an event handler, transform or rpc function, nothing throws, because the write happens after your handler returns. The platform drops it and records a warning on the trace.
  • In a workflow, endpoint, schedule or queue consumer, the awaited set throws. Catch the error if your code can continue without the write.

Keep shared entries short. Every app with the capability receives every shared entry, so a large entry costs all of them.

  • Your app needs the visitor’s consent to read or write either level. Without it, your handler sees no context and its writes are dropped.
  • A shared entry stops reaching other apps when the app that wrote it leaves the site, drops the sharedContext capability, or loses the visitor’s consent. The entry is not deleted.
  • If your app is in the marketing or salesOfData category, it receives no context for events from a browser that sends Global Privacy Control, and that browser does not receive your shared entries.

Context is stored per device. When one person uses a phone and a laptop, your app sees different context on each one, and linking a device to a person copies no context to it. Your app can sync what it needs between devices with the approaches below.

If you know the person, look up their devices and write to each one. This needs the identity capability and the person’s consent for your app. It runs in an endpoint, a schedule or a queue consumer, where ctx.context(anonId) exists.

let cursor: string | undefined
do {
const person = await ctx.identity.resolve('ada@example.com', { cursor })
for (const anonId of person.anonIds) {
await ctx.context(anonId).shared.set({ kv: { tier: 'gold' } })
}
cursor = person.nextCursor ?? undefined
} while (cursor !== undefined)

This reaches the devices linked at that moment. A device that links later does not receive the earlier write.

If the value describes the person, store it as a trait. Traits and custom properties converge across devices.

Your app can write the standard fields itself with ctx.identity.upsert: a first name, last name, city, state, zip or country, together with at least one of an email, phone or custom id. This needs the identity capability.

await ctx.identity.upsert({ email: 'ada@example.com', city: 'Ljubljana' })

Your app cannot write a custom property. The fields above are the only ones upsert accepts, and the events an app emits are always track events. Custom properties come from your site or backend, through identify. See User data.

The value Put it in
Is small, for your app only, and about one browser Private context
Is small and useful to other apps on the site Shared context
Must drive the page in the browser Shared context, read with onSharedContext
Must be the same on all of a person’s devices Context, written to each device
Is a standard trait, such as a name or a city A trait, written with ctx.identity.upsert
Describes the person, and only your site knows it A custom property, sent with identify
Is large, or you need its history Your own resource

Under a storage link, sites of one team share each visitor profile. If your app runs on both sites, it reads one private entry for that visitor, because an entry belongs to the app. Each site’s apps read shared entries only from apps installed on that site.