vtex-io-session-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

VTEX IO session transform apps

VTEX IO 会话转换应用

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
    )
当你的VTEX IO应用需要集成VTEX会话系统
vtex.session
),来派生、计算或传播下游转换逻辑、店面或结账流程依赖的状态时,可以使用本指南:
  • 构建会话转换逻辑,基于上游会话状态计算自定义字段(例如从外部后端获取定价上下文、从组织数据计算区域化规则)
  • vtex.session/configuration.json
    中声明输入/输出字段
  • 确定你的应用应该拥有哪个命名空间,以及可以读取哪些其他命名空间的内容
  • 把值传播到**
    public.*
    **输入字段中,触发原生转换逻辑(用户资料、搜索、结账)重新运行
  • 调试会话字段过期竞态条件或应用间的命名空间冲突问题
  • 设计B2B会话流程,协调
    storefront-permissions
    、自定义转换逻辑和结账流程的交互
本指南不适用于以下场景:
  • 通用IO后端模式(请参考
    vtex-io-service-apps
  • 会话转换之外的性能优化模式(请参考
    vtex-io-application-performance
  • GraphQL schema或解析器设计(请参考
    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.
  • 每个会话应用仅拥有一个输出命名空间(或一个命名空间下的少量字段)。命名空间名称通常和应用功能匹配(例如
    rona
    myapp
    storefront-permissions
    )。
  • 绝对不要写入其他应用的输出命名空间。 如果
    storefront-permissions
    拥有
    storefront-permissions.organization
    字段,你的转换逻辑不能覆盖它,而是应该把它作为输入读取。
  • 绝对不要复制VTEX官方拥有的字段(组织、成本中心、邮编、国家)到你的命名空间,这些字段已经存在于
    storefront-permissions
    profile
    checkout
    store
    中。你的命名空间应该仅包含来自你的后端或计算逻辑的数据。

public
is input, private is read model

public
是输入层,private是读取模型

  • 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.
  • public.*
    字段是输入层
    :是消费者或流程设置的值,供会话转换逻辑运行使用(例如地理位置、标记位、UTM参数、用户意图)。不要在店面代码中把
    public.*
    当做权威读取模型。
  • 私有命名空间
    profile
    checkout
    store
    search
    storefront-permissions
    、你的自定义命名空间)是读取模型:是基于输入派生的计算输出。前端组件应该读取私有命名空间的字段来实现业务规则和展示逻辑。
  • 如果你的转换逻辑需要影响原生应用(例如设置基于成本中心地址派生的邮编),请更新原生应用声明为输入的
    public.*
    字段
    ,这样平台会重新运行上游转换逻辑,保证私有输出的一致性。这是输入传播,不是重复存储真值。

Transform ordering (DAG)

转换执行顺序(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.
  • VTEX会话系统会基于每个应用的
    vtex.session/configuration.json
    中声明的输入/输出依赖,按照**有向无环图(DAG)**的顺序运行转换逻辑。
  • 当任意一个声明的输入字段变更时,转换逻辑就会运行。如果你依赖
    storefront-permissions.costcenter
    字段,你的转换逻辑会在
    storefront-permissions
    输出该字段之后运行。
  • 仔细规划你的依赖顺序:如果你的转换逻辑同时需要
    storefront-permissions
    profile
    的输出,需要把两者都声明为输入,这样平台会在两者都运行完成后再调度你的转换逻辑。

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
    ).
  • 会话转换逻辑会在每次会话变更触及声明的输入字段时执行,必须保证运行速度够快。
  • 热点查询(组织数据、配置、令牌)可以使用LRU(进程内、单工作进程级别)缓存,设置较短的TTL。
  • 可以容忍短暂过期的数据(外部后端响应、计算映射关系)使用VBase stale-while-revalidate缓存:立即返回过期内容,后台异步更新。
  • 遵循所有IO服务通用的租户键规则:内存缓存键必须包含**
    account
    workspace
    **(参考
    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.
  • 店面组件应该通过
    items=
    查询参数请求特定的会话字段(例如
    items=rona.storeNumber,storefront-permissions.costcenter
    )。
  • 从对应的私有命名空间(
    rona.*
    storefront-permissions.*
    profile.*
    等)读取权威状态。
  • 仅在设置用户意图时(例如选择位置、切换标记位)才写入
    public.*
    字段。绝对不要把
    public.*
    当做私有命名空间已有值的“缓存”使用。

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"]
    }
  }
}
你的会话转换逻辑必须仅输出来自你的计算逻辑或后端的字段。复制
storefront-permissions
profile
checkout
已经拥有的身份、地址或组织字段会产生两个真值来源,在部分失败的场景下会出现数据不一致。
为什么重要 — 当两个命名空间包含同一个事实数据时(例如你的命名空间和
storefront-permissions
都有
costCenterId
),会话部分更新后消费者会读取到不一致的值,调试成本大幅提升,还会出现竞态条件。
检测方式 — 你的转换逻辑输出包含
organization
costcenter
postalCode
country
等和
storefront-permissions.*
profile.*
输出重复的字段。或者前端从两个不同的命名空间读取同一个逻辑字段。
正确写法 — 把
storefront-permissions.costcenter
作为输入读取,用它计算你的后端专属字段(例如
myapp.priceTable
myapp.storeNumber
),仅输出这些派生字段。
json
{
  "my-session-app": {
    "input": {
      "storefront-permissions": ["costcenter", "organization"]
    },
    "output": {
      "myapp": ["priceTable", "storeNumber"]
    }
  }
}
错误写法 — 输出VTEX官方拥有的字段的副本。
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"]
    }
  }
}
当你的转换逻辑派生了原生应用需要消费的值(例如从成本中心地址得到的邮编),把它设置为原生应用声明的输入字段(通常是
public.postalCode
public.country
),不要直接写入
checkout.postalCode
search.postalCode
为什么重要 — 原生转换逻辑期望输入字段变更时触发重新计算输出字段。直接写入它们的输出命名空间会跳过重新计算流程,导致派生状态过期(例如
regionId
没有更新、结账地址不一致)。
检测方式 — 你的转换逻辑在其他应用拥有的命名空间中声明输出字段(例如
output: { checkout: [...] }
output: { search: [...] }
)。或者你PATCH会话时写入了你不拥有的命名空间的值。
正确写法 — 在
public
中声明原生应用作为输入消费的字段输出,需要和每个原生应用的
vtex.session/configuration.json
核对一致。
json
{
  "my-session-app": {
    "output": {
      "myapp": ["storeNumber", "priceTable"],
      "public": ["postalCode", "country", "state"]
    }
  }
}
错误写法 — 直接写入search或checkout输出命名空间。
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

约束:前端必须读取私有命名空间获取权威业务状态,不要读取
public

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',
])
店面组件和中间件必须从权威私有命名空间读取会话数据(例如
storefront-permissions.organization
profile.email
myapp.priceTable
),不要读取
public.*
字段。
为什么重要
public.*
字段是输入,可能是过期的、用户设置的或者不完整的。私有命名空间字段是所有转换逻辑运行完成后的计算真值。读取
public.postalCode
而不是profile或checkout派生的值会导致展示过期或不一致的数据。
检测方式 — React组件或中间件读取
public.storeNumber
public.organization
public.costCenter
用于展示或业务逻辑,而不是读取对应的私有字段。
正确写法
typescript
// 从权威命名空间读取
const { data } = useSessionItems([
  'myapp.storeNumber',
  'myapp.priceTable',
  'storefront-permissions.costcenter',
  'storefront-permissions.organization',
])
错误写法
typescript
// 把public当做真值来源读取
const { data } = useSessionItems([
  'public.storeNumber',
  'public.organization',
  'public.costCenter',
])

Preferred pattern

推荐模式

vtex.session/configuration.json

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"]
    }
  }
}
声明你的转换逻辑的输入依赖和输出字段:
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,
    },
  }
}
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
}
typescript
// 两层缓存:LRU(亚毫秒级) -> VBase(持久化,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

service.json
路由

json
{
  "routes": {
    "transform": {
      "path": "/_v/my-session-app/session/transform",
      "public": true
    }
  }
}
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.
构建转换逻辑时,梳理清楚你的店铺的转换DAG:
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)
你的转换逻辑会位于它所需的依赖链的末端。正确声明输入可以保证平台在所有上游转换逻辑运行完成后再调度你的逻辑。

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.
  • 前端通过
    updateSession
    写入B2B状态
    — 前端直接PATCH B2B会话字段,而不是让
    storefront-permissions
    和你的转换逻辑计算这些字段。这会导致竞态条件、部分状态和重复的真值来源。
  • 重复VTEX官方拥有的字段 — 把已经存在于
    storefront-permissions
    profile
    中的
    costcenter
    organization
    postalCode
    复制到你的命名空间。
  • 没有缓存的慢转换逻辑 — 每次转换逻辑调用都请求外部API,没有使用LRU + VBase SWR缓存。转换逻辑会在每次会话变更触及声明的输入时运行,必须保证速度够快。
  • public.*
    当做真值来源读取
    — 前端组件读取
    public.organization
    public.storeNumber
    而不是私有命名空间字段,导致展示过期或不一致的内容。
  • 写入其他应用的输出命名空间 — 在你不拥有的
    checkout
    search
    storefront-permissions
    命名空间中声明输出字段,跳过了原生转换逻辑的重新计算。
  • LRU中缺少租户键 — 组织或定价数据的内存缓存仅使用实体ID作为键,没有加上
    account:workspace
    ,在多租户共享Pod的场景下会有安全问题。

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?
  • 转换逻辑是否仅输出来自自身计算/后端的字段,没有复制其他命名空间的字段?
  • vtex.session/configuration.json
    中是否正确声明了输入依赖?
  • 输出字段是否仅限制在你自己的命名空间(需要传播时可以加上
    public.*
    输入)?
  • public.*
    是否仅用于输入传播,没有被当做第二个读取模型使用?
  • 前端组件是否读取私有命名空间获取业务状态,而不是读取
    public.*
  • 转换逻辑中的上游API调用是否做了缓存(LRU + VBase SWR)来保证转换延迟足够低?
  • 内存缓存键是否加上了
    account:workspace
    作用域来保证多租户安全?
  • 转换顺序(DAG)是否正确——是否在所有依赖的转换逻辑运行完成后才执行?
  • 前端代码中是否移除了针对转换逻辑计算字段的
    updateSession
    调用?

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
  • vtex-io-application-performance — 适用于转换逻辑内部的缓存层和并行I/O优化
  • vtex-io-service-paths-and-cdn — 转换端点的路由前缀规则
  • vtex-io-service-apps — 服务类、客户端和中间件基础
  • vtex-io-app-structure — 清单、构建器、权限规则

Reference

参考资料