vtex-io-session-apps

Original🇺🇸 English
Not Translated

Apply when building or debugging a VTEX IO session transform app (vtex.session integration). Covers namespace ownership, input-vs-output fields, transform ordering (DAG), public-as-input vs private-as-read model, cross-namespace propagation, configuration.json contracts, caching inside transforms, and frontend session consumption. Use when designing session-derived state for B2B, pricing, regionalization, or custom storefront context.

2installs
Added on

NPX Install

npx skill4agent add vtex/skills vtex-io-session-apps

SKILL.md Content

VTEX IO session transform apps

When this skill applies

Use this skill when your VTEX IO app integrates with the VTEX session system (
vtex.session
) to derive, compute, or propagate state that downstream transforms, the storefront, or checkout depend on.
  • Building a session transform that computes custom fields from upstream session state (e.g. pricing context from an external backend, regionalization from org data)
  • Declaring input/output fields in
    vtex.session/configuration.json
  • Deciding which namespace your app should own and which it should read from
  • Propagating values into
    public.*
    inputs so native transforms (profile, search, checkout) re-run
  • Debugging stale session fields, race conditions, or namespace collisions between apps
  • Designing B2B session flows where
    storefront-permissions
    , custom transforms, and checkout interact
Do not use this skill for:
  • General IO backend patterns (use
    vtex-io-service-apps
    )
  • Performance patterns outside session transforms (use
    vtex-io-application-performance
    )
  • GraphQL schema or resolver design (use
    vtex-io-graphql-api
    )

Decision rules

Namespace ownership

  • Every session app owns exactly one output namespace (or a small set of fields within one). The namespace name typically matches the app concept (e.g.
    rona
    ,
    myapp
    ,
    storefront-permissions
    ).
  • Never write to another app's output namespace. If
    storefront-permissions
    owns
    storefront-permissions.organization
    , your transform must not overwrite it—read it as an input instead.
  • Never duplicate VTEX-owned fields (org, cost center, postal, country) into your namespace when they already exist in
    storefront-permissions
    ,
    profile
    ,
    checkout
    , or
    store
    . Your namespace should contain only data that comes from your backend or computation.

public
is input, private is read model

  • public.*
    fields are an input surface: values the shopper or a flow sets so session transforms can run (e.g. geolocation, flags, UTMs, user intent). Do not treat
    public.*
    as the canonical read model in storefront code.
  • Private namespaces (
    profile
    ,
    checkout
    ,
    store
    ,
    search
    ,
    storefront-permissions
    , your custom namespace) are the read model: computed outputs derived from inputs. Frontend components should read private namespace fields for business rules and display.
  • If your transform must influence native apps (e.g. set a postal code derived from a cost center address), update
    public.*
    input fields
    that native apps declare as inputs—so the platform re-runs those upstream transforms and private outputs stay consistent. This is input propagation, not duplicating truth.

Transform ordering (DAG)

  • VTEX session runs transforms in a directed acyclic graph (DAG) based on declared input/output dependencies in each app's
    vtex.session/configuration.json
    .
  • A transform runs when any of its declared input fields change. If you depend on
    storefront-permissions.costcenter
    , your transform runs after
    storefront-permissions
    outputs that field.
  • Order your dependencies carefully: if your transform needs both
    storefront-permissions
    outputs and
    profile
    outputs, declare both as inputs so the platform schedules you after both.

Caching inside transforms

  • Session transforms execute on every session change that touches a declared input. They must be fast.
  • Use LRU (in-process, per-worker) for hot lookups (org data, configuration, tokens) with short TTLs.
  • Use VBase stale-while-revalidate for data that can tolerate brief staleness (external backend responses, computed mappings). Return stale immediately; revalidate in the background.
  • Follow the same tenant-keying rules as any IO service: in-memory cache keys must include
    account
    and
    workspace
    (see
    vtex-io-application-performance
    ).

Frontend session consumption

  • Storefront components should request specific session items via the
    items=
    query parameter (e.g.
    items=rona.storeNumber,storefront-permissions.costcenter
    ).
  • Read from the relevant private namespaces (
    rona.*
    ,
    storefront-permissions.*
    ,
    profile.*
    , etc.) for canonical state.
  • Write to
    public.*
    only when setting user intent (e.g. selecting a location, switching a flag). Never write to
    public.*
    as a "cache" for values that private namespaces already provide.

Hard constraints

Constraint: Do not duplicate another app's output namespace fields into your namespace

Your session transform must output only fields that come from your computation or backend. Copying identity, address, or org fields that
storefront-permissions
,
profile
, or
checkout
already own creates two sources of truth that diverge on partial failures.
Why this matters — When two namespaces contain the same fact (e.g.
costCenterId
in both your namespace and
storefront-permissions
), consumers read inconsistent values after a session that partially updated. Debug time skyrockets and race conditions appear.
Detection — Your transform's output includes fields like
organization
,
costcenter
,
postalCode
,
country
that mirror
storefront-permissions.*
or
profile.*
outputs. Or frontend reads the same logical field from two different namespaces.
Correct — Read
storefront-permissions.costcenter
as an input; use it to compute your backend-specific fields (e.g.
myapp.priceTable
,
myapp.storeNumber
); output only those derived fields.
json
{
  "my-session-app": {
    "input": {
      "storefront-permissions": ["costcenter", "organization"]
    },
    "output": {
      "myapp": ["priceTable", "storeNumber"]
    }
  }
}
Wrong — Output duplicates of VTEX-owned fields.
json
{
  "my-session-app": {
    "output": {
      "myapp": ["costcenter", "organization", "postalCode", "priceTable", "storeNumber"]
    }
  }
}

Constraint: Use input propagation to influence native transforms, not direct overwrites

When your transform derives a value (e.g. postal code from a cost center address) that native apps consume, set it as an input field those apps declare (typically
public.postalCode
,
public.country
)—not by writing directly to
checkout.postalCode
or
search.postalCode
.
Why this matters — Native transforms expect their input fields to change so they can recompute their output fields. Writing directly to their output namespaces bypasses recomputation and leaves stale derived state (e.g.
regionId
not updated, checkout address inconsistent).
Detection — Your transform declares output fields in namespaces owned by other apps (e.g.
output: { checkout: [...] }
or
output: { search: [...] }
). Or you PATCH session with values in a namespace you don't own.
Correct — Declare output in
public
for fields that native apps consume as inputs, verified against each native app's
vtex.session/configuration.json
.
json
{
  "my-session-app": {
    "output": {
      "myapp": ["storeNumber", "priceTable"],
      "public": ["postalCode", "country", "state"]
    }
  }
}
Wrong — Writing to search or checkout output namespaces directly.
json
{
  "my-session-app": {
    "output": {
      "myapp": ["storeNumber", "priceTable"],
      "checkout": ["postalCode", "country"],
      "search": ["facets"]
    }
  }
}

Constraint: Frontend must read private namespaces, not
public
, for canonical business state

Storefront components and middleware must read session data from the authoritative private namespace (e.g.
storefront-permissions.organization
,
profile.email
,
myapp.priceTable
), not from
public.*
fields.
Why this matters
public.*
fields are inputs that may be stale, user-set, or partial. Private namespace fields are the computed truth after all transforms have run. Reading
public.postalCode
instead of the profile- or checkout-derived value leads to displaying stale or inconsistent data.
Detection — React components or middleware that read
public.storeNumber
,
public.organization
, or
public.costCenter
for display or business logic instead of the corresponding private field.
Correct
typescript
// Read from the authoritative namespace
const { data } = useSessionItems([
  'myapp.storeNumber',
  'myapp.priceTable',
  'storefront-permissions.costcenter',
  'storefront-permissions.organization',
])
Wrong
typescript
// Reading from public as if it were the source of truth
const { data } = useSessionItems([
  'public.storeNumber',
  'public.organization',
  'public.costCenter',
])

Preferred pattern

vtex.session/configuration.json

Declare your transform's input dependencies and output fields:
json
{
  "my-session-app": {
    "input": {
      "storefront-permissions": ["costcenter", "organization", "costCenterAddressId"]
    },
    "output": {
      "myapp": ["storeNumber", "priceTable"]
    }
  }
}

Transform handler

typescript
// node/handlers/transform.ts
export async function transform(ctx: Context) {
  const { costcenter, organization } = parseSfpInputs(ctx.request.body)

  if (!costcenter) {
    ctx.body = { myapp: {} }
    return
  }

  const costCenterData = await getCostCenterCached(ctx, costcenter)
  const pricing = await resolvePricing(ctx, costCenterData)

  ctx.body = {
    myapp: {
      storeNumber: pricing.storeNumber,
      priceTable: pricing.priceTable,
    },
  }
}

Caching inside the transform

typescript
// Two-layer cache: LRU (sub-ms) -> VBase (persistent, SWR) -> API
const costCenterLRU = new LRU<string, CostCenterData>({ max: 1000, ttl: 600_000 })

async function getCostCenterCached(ctx: Context, costCenterId: string) {
  const { account, workspace } = ctx.vtex
  const key = `${account}:${workspace}:${costCenterId}`

  const lruHit = costCenterLRU.get(key)
  if (lruHit) return lruHit

  const result = await staleFromVBaseWhileRevalidate(
    ctx.clients.vbase,
    'cost-centers',
    costCenterId,
    () => fetchCostCenterFromAPI(ctx, costCenterId),
    { ttlMs: 1_800_000 }
  )

  costCenterLRU.set(key, result)
  return result
}

service.json
route

json
{
  "routes": {
    "transform": {
      "path": "/_v/my-session-app/session/transform",
      "public": true
    }
  }
}

Session ecosystem awareness

When building a transform, map out the transform DAG for your store:
text
authentication-session → impersonate-session → profile-session
profile-session → store-session → checkout-session
profile-session → search-session
authentication-session + checkout-session + impersonate-session → storefront-permissions
storefront-permissions → YOUR-TRANSFORM (reads SFP outputs)
Your transform sits at the end of whatever dependency chain it requires. Declaring inputs correctly ensures the platform schedules you after all upstream transforms.

Common failure modes

  • Frontend writes B2B state via
    updateSession
    — Instead of letting
    storefront-permissions
    + your transform compute B2B session fields, the frontend PATCHes them directly. This creates race conditions, partial state, and duplicated sources of truth.
  • Duplicating VTEX-owned fields — Copying
    costcenter
    ,
    organization
    , or
    postalCode
    into your namespace when they already live in
    storefront-permissions
    or
    profile
    .
  • Slow transforms without caching — Calling external APIs on every transform invocation without LRU + VBase SWR. Transforms run on every session change that touches a declared input; they must be fast.
  • Reading
    public.*
    as source of truth
    — Frontend components reading
    public.organization
    or
    public.storeNumber
    instead of the private namespace field, leading to stale or inconsistent display.
  • Writing to other apps' output namespaces — Declaring output fields in
    checkout
    ,
    search
    , or
    storefront-permissions
    namespaces you don't own, bypassing native transform recomputation.
  • Missing tenant keys in LRU — In-memory cache for org or pricing data keyed only by entity ID without
    account:workspace
    , unsafe on multi-tenant shared pods.

Review checklist

  • Does the transform output only fields from its own computation/backend, not duplicates of other namespaces?
  • Are input dependencies declared correctly in
    vtex.session/configuration.json
    ?
  • Are output fields limited to your own namespace (plus
    public.*
    inputs when propagation is needed)?
  • Is
    public.*
    used only for input propagation, not as a second read model?
  • Do frontend components read from private namespaces, not
    public.*
    , for business state?
  • Are upstream API calls in the transform cached (LRU + VBase SWR) to keep transform latency low?
  • Are in-memory cache keys scoped with
    account:workspace
    for multi-tenant safety?
  • Is the transform order (DAG) correct—does it run after all its dependency transforms?
  • Has
    updateSession
    been removed from frontend code for fields the transform computes?

Related skills

  • vtex-io-application-performance — Caching layers and parallel I/O applicable inside transforms
  • vtex-io-service-paths-and-cdn — Route prefix for the transform endpoint
  • vtex-io-service-apps — Service class, clients, and middleware basics
  • vtex-io-app-structure — Manifest, builders, policies

Reference