Session Stores
Checkout state lives in a SessionStore. The SDK ships an InMemorySessionStore suitable for development and single-process deployments. For horizontally-scaled production workloads, implement the three-method SessionStore interface against Redis, Postgres, or whatever durable store you already operate.
The SessionStore interface
export interface SessionStore {
get(id: string): Promise<CheckoutSession | undefined>
set(id: string, session: CheckoutSession): Promise<void>
delete(id: string): Promise<void>
}That’s the whole contract. CheckoutSessionManager layers TTL enforcement, status transitions, and atomic updates on top.
| Name | Type | Description |
|---|---|---|
get* | (id) => Promise<CheckoutSession | undefined> | Fetch by id. Return undefined if missing. |
set* | (id, session) => Promise<void> | Upsert by id. Overwrite on conflict. |
delete* | (id) => Promise<void> | Remove by id. No-op if missing. |
Default: InMemorySessionStore
import { AIOTShopifyClient, InMemorySessionStore } from '@aiot/shopify-x402'
const client = new AIOTShopifyClient({
// ...,
sessionStore: new InMemorySessionStore()
})A simple Map<string, CheckoutSession>. Zero dependencies, zero setup. Do not use this in a horizontally scaled fleet — a session created on pod A can’t be completed by pod B.
Redis example
import { createClient } from 'redis'
import type { SessionStore, CheckoutSession } from '@aiot/shopify-x402'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
const SESSION_TTL_SECONDS = 30 * 60 // matches SDK's 30-minute TTL
export const redisStore: SessionStore = {
async get(id) {
const raw = await redis.get(`aiot:session:${id}`)
return raw ? (JSON.parse(raw) as CheckoutSession) : undefined
},
async set(id, session) {
await redis.set(
`aiot:session:${id}`,
JSON.stringify(session),
{ EX: SESSION_TTL_SECONDS }
)
},
async delete(id) {
await redis.del(`aiot:session:${id}`)
}
}
const client = new AIOTShopifyClient({
// ...,
sessionStore: redisStore
})Let Redis handle TTL via the EX option. The SDK still transitions
sessions to expired in-memory when get is called on an old session,
but letting Redis evict the row keeps your key-count bounded.
Sessions are mutated in place
CheckoutSessionManager reads a session, mutates a field, and writes it back. Between read and write there is no lock — the SDK assumes the happy path of a single user completing a single checkout is not racy. If you’re building a multi-writer system (e.g. a background worker that settles in parallel with a polling UI), consider wrapping set in a version check or using WATCH/MULTI in Redis.
What’s in a session?
CheckoutSession is defined in src/checkout/session.ts. The key fields:
| Name | Type | Description |
|---|---|---|
id | string | 128-bit hex id generated at create time. |
status | 'pending' | 'signing' | 'settling' | 'completing' | 'completed' | 'failed' | 'expired' | Current lifecycle state. |
cartItems | CartItem[] | Line items from createCheckout. |
fiatTotal | string | Fiat total in the request currency. |
usdcAmount | string | USDC base-unit amount (6 decimals). |
shopifyDraftOrderId | string | Shopify draft order GID. |
x402OrderId | string | Order id returned by the facilitator. |
expiresAt | number | Unix millisecond expiry. |
paymentResult | PaymentResult | undefined | Set on completion. |
Related
- SDK client — consumes the store
- Error handling —
CheckoutExpiredErroris thrown fromCheckoutSessionManager.getindirectly - API Reference → SessionStore