Skip to main content

Portable contexts

Vox resolves the values of entities in a selection of contexts, eg the Swedish name in the Swedish market. A system outside the installation that owns the content, eg a delivery system that receives projections from Vox, must select and resolve exactly as Vox does, or it shows other values than Vox's own preview. This page specifies the logic and its data, so it can be implemented in any runtime.

  • The .NET implementation is CommerceMind.Vox.Contexts, which depends on nothing else in Vox. Vox itself uses it, so a .NET system that references it can't drift from Vox.
  • The conformance vectors in src/engine/Vox.Contexts/Conformance are JSON test cases with their expected results. Every implementation runs them, Vox included.

Nothing in the logic depends on culture or platform. Strings are compared ordinally and case-sensitively.

Keys​

A key is a set of contexts, at most one member per context type, eg Language=sv|Market=se. It's written as its context types and members joined by |, ordered ordinally by the names of the context types, each as Type=member. A singleton context type, whose only member is $, is written without it, eg $draft. The empty key "" is the baseline, published content without contexts.

The context types that Vox owns are $draft, the drafts, with the single member $, and $release, whose members are the ids of content releases.

The manifest​

The manifest is the context space of a model as JSON. GET api/contexts/manifest serves it.

{
"formatVersion": 1,
"revision": "5f0c…",
"contextTypes": [
{
"name": "Language",
"queryName": "language",
"existence": "inherit",
"isMergeable": false,
"isRestricted": false,
"isSingleton": false,
"selection": "explicit",
"isPortable": true,
"members": [{ "id": "sv", "fallbacks": ["en"], "priority": 2 }, { "id": "en", "fallbacks": [] }]
}
]
}
  • formatVersion is 1. An implementation rejects a manifest of a version it doesn't support.
  • revision is the lowercase hex SHA-256 of the UTF-8 JSON of { "formatVersion", "contextTypes" }, written as Vox writes them. It changes whenever anything in the manifest does, and documents built with a manifest carry it, so a system can tell when documents and manifest disagree, eg during a rebuild.
  • contextTypes are in stacking order, from the highest to the lowest.
  • existence is gate or inherit (see Existence).
  • isMergeable is true for context types whose cells are merged into their parent, eg $draft.
  • isRestricted is true for context types that need a permission to select. They're only exported when the exporter is allowed to read them.
  • selection is explicit, when members are only selected by the caller, criteria, when the criteria expressions of the members select them (see Criteria), or programmatic, when Vox selects them with application code (see Selection).
  • isPortable is false for a context type whose members Vox selects with application code. A system outside Vox can't select those, and its caller passes their members explicitly.
  • members are the members with their fallback chains, from the closest fallback. A system needs no fallback logic of its own.
  • priority is an optional number of a member, which orders several selected members of its context type (see Selection).
  • criteria is the criteria expression of a member of a criteria context type, or missing if it has none.

Selection​

Selection turns an input into the selected members of some context types. The input is:

{
"host": "shop.example.se",
"path": "/sv/mugs",
"query": { "page": ["2"] },
"headers": { "accept-language": "sv-SE" },
"cookies": { "vox-visitor": "4f1c…" },
"attributes": { "customerGroup": "b2b" },
"nowUtc": "2026-10-24T12:00:00Z",
"visitorId": "4f1c…",
"explicit": { "Language": ["sv"], "warehouse": ["north"] }
}
  • headers are by their names in lowercase. attributes are the caller's, eg the claims of its token, which the host fills so they mean the same everywhere.
  • explicit are the members the caller selects, by the names or query names of their context types.

Selection runs in this order:

  1. Explicit members. A context type named in explicit is selected with those members, whatever else would select it. An unknown context type, a context type named twice and a singleton's member other than $ are rejected. An empty list selects nothing.
  2. Programmatic context types are selected by Vox with application code before it selects the rest, and passed on as explicit members. A system outside Vox can't, so its caller passes their members explicitly, and a context type without one isn't selected.
  3. Criteria context types that aren't selected explicitly select the members whose criteria hold, see Criteria. A context type whose criteria read the members of another (context.<type>) is selected after it, and otherwise they're selected in stacking order. Criteria that read each other's members are rejected.
  4. Fallbacks. Each selected member gets its fallback chain from the manifest.

Several members of a context type can be selected, eg a customer in two customer groups, except of a mergeable or singleton context type. They're in their order of precedence: the higher priority first, members without one after those with one, and then the lower id, compared ordinally. A member selected twice is selected once.

A context type without a selected member isn't selected, so its cells resolve to the baseline. The result is the selected members by the names of their context types:

{ "Language": [{ "id": "sv", "fallbacks": ["en"] }] }

The conformance vectors are selection.json.

Criteria​

A criteria expression is a JSON tree. Each node is an object with a single operator:

{
"and": [
{ "equals": { "input": "host", "value": "shop.example.se", "ignoreCase": true } },
{ "not": { "startsWith": { "input": "path", "value": "/en/" } } },
{ "between": { "input": "now", "from": "2026-11-01T00:00:00Z", "to": "2026-12-01T00:00:00Z" } }
]
}
  • and and or take an array of at least one node, and not takes a node.
  • equals, in and startsWith compare strings: value, or values for in. They compare ordinally, or ordinally ignoring case with "ignoreCase": true.
  • exists is true if the input has a value.
  • lessThan and greaterThan compare with value, exclusively, and between from from, inclusive, to to, exclusive. A number is compared as a decimal with the input read as a decimal, and a time, an ISO 8601 string with Z or an offset, as an instant with the input read as one. An input that can't be read as the bound's kind doesn't match.

The inputs are:

InputValues
host, pathThe host and path of the request.
query.<name>The values of a query string parameter.
header.<name>A header, by its name ignoring case.
cookie.<name>, attribute.<name>A cookie, or an attribute of the caller.
nowThe current UTC time.
bucketThe visitor's bucket for the member, see below.
context.<type>The members selected of another context type, by its name or query name.

An input with several values matches if one of them does, and an input without a value matches no comparison. So not of a comparison holds for a missing input.

The bucket of a visitor for a member is a number from 1 to 100: the first four bytes of the SHA-256 of the UTF-8 bytes of {visitorId}:{contextType}:{memberId} as a big-endian unsigned integer, modulo 100, plus 1. Without a visitor id, there's no bucket. Each member buckets a visitor independently.

An expression with an unknown operator or input, or a node with other than one operator, is rejected when it's written. A member whose criteria aren't valid is never selected. The conformance vectors are criteria.json.

Candidates​

A selection selects members of some context types. The candidates of a context type are its selected members in their order of precedence, followed by their fallbacks from the manifest in the same order, without repeating a member. A member that the manifest doesn't list, eg a content release that isn't exported, has no fallbacks.

Resolution​

A key matches a selection if every context of the key is among the candidates of its context type. Of the matching keys, the most specific one wins. Keys are compared by their context types in stacking order, from the highest:

  • a key's rank for a context type is the position of its member among the candidates, and a key without the context type ranks after every candidate,
  • the first context type where two keys rank differently decides.

So a closer fallback beats a farther one, a key with a context beats a key without it, and the stacking order decides, not the number of contexts: with $release ranked above Language, $release=autumn beats Language=sv|Market=se. The baseline matches every selection and ranks last.

Documents​

A keyed document is an entity's values per key:

{
"memberOf": ["Market"],
"cells": [
{ "key": "", "values": { "name": "Mug", "color": "Red" } },
{ "key": "Language=sv", "values": { "name": "Mugg" } },
{ "key": "Market=se", "exists": true, "values": {} }
]
}
  • values are the values of a cell by the paths of their fields. A document uses the same paths in all of its cells.
  • Each path resolves to its value in the most specific matching cell that has the path, so a Swedish cell with only a name still has the color of the baseline.

Existence​

exists is optional on a cell. A document exists in a selection if both of these hold:

  • Its own existence. The most specific matching cell with exists whose key has only mergeable context types, or is the baseline, doesn't say false. A draft-only entity is "" = false and $draft = true, so it exists only when the draft is selected.
  • Its memberships. For each context type in memberOf with selected members, the entity is a member of one of them. It's a member of a selected member by the most specific matching cell with exists whose key has the member or one of its fallbacks, ranked with the member and its fallbacks as the candidates of the context type. Without one, it's a member if the context type inherits, and not one if it's a gate. So a product hidden in Swedish is hidden in Finland Swedish, which falls back to Swedish. The key can have other application context types too, eg Channel=web|Language=sv for a membership of the web channel that only applies in Swedish, and it then matches like any other key, so Channel=web|Language=sv wins over Channel=web when Swedish or a language that falls back to it is selected.

A membership never makes an entity exist that doesn't, and a cell at a key with a draft, eg $draft|Market=se, wins over the published one, Market=se, when the draft is selected.

Paths​

The paths of a document's values are the properties of its objects joined with dots, eg seo.title, down to values that aren't objects, such as strings, numbers and arrays, which a more specific cell replaces as a whole. An empty object is a value of its own. A resolved document is expanded from its paths back into objects. Property names with dots can't be told apart from nested objects, so documents published as keyed cells don't use them. VoxDocumentPaths flattens and expands them in .NET.

Published documents​

A projection publication publishes a source as documents, see Entity projections, in one of two shapes per context type it enumerates:

  • Per combination: a document per combination of members, each with its key, exists and the source's memberOf. A delivery system resolves them as the cells of a keyed document whose cells each hold a whole document at the path "", the empty path: the document at the most specific key that the selection matches, if the source exists in the selection by the rules of existence. A document without a value, null at "", says there's no document there, so a less specific one doesn't apply either.
  • Keyed cells: the cells of a document, which a delivery system resolves as a keyed document. A cell at a more specific key only has the values that differ from the cells a selection matching it could resolve to instead, so each selection resolves to the values of the projection evaluated in it.

With both, the document of the selection is chosen first, and its cells are resolved in the same selection. Every document carries the revision of the manifest it was built with.

A reference in a document, { "$ref": { "entityType", "entityId", "projection", "version", "contexts" } }, is resolved in the request's selection, with the members of its optional contexts, a key, on top of it. Its key is vox://entity/{entityType}/{entityId}?projection={projection}, with &context= and its contexts in the syntax of the context parameter, eg Market:no, ordered like the key, when it has them. The entity id, projection, context types and members are escaped like URI data.