Cookbook
Events & Identity
Subscribe to real-time events pushed from the host via the framework, and enrich identity claims. Each event type has a dedicated hook (useIdentityEvent / useMessagingEvent / useActivityEvent) — never use capabilities.events.* directly. Identity enrichment supports both a login-time hook (useExtendIdentity) and an imperative post-login API (capabilities.identity.extend).
Messaging Events
Subscribe to postback button clicks from the Messaging widget. The actionName is the button's display text, not a programmatic identifier.
Permission: events:messaging
Hook usage
import { useMessagingEvent } from '@stackable-labs/sdk-extension-react'
useMessagingEvent('postback:Buy Now', (event) => {
console.log('Postback:', event.data.actionName, event.data.conversationId)
})
Full component example
import { useMessagingEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
import type { MessagingEventHandler } from '@stackable-labs/sdk-extension-contracts'
import { useState } from 'react'
export function Content(): React.ReactElement {
const [lastPostback, setLastPostback] = useState<string | null>(null)
useMessagingEvent('postback', (event) => {
setLastPostback(event.data.actionName)
})
return (
<Surface id="slot.content">
<ui.Text className="text-xs">{lastPostback ?? 'No postbacks yet'}</ui.Text>
</Surface>
)
}
Activity Events
Subscribe to host site activity events like page views, clicks, and purchases pushed from the host via the framework. Activity event names are domain-stripped — use useActivityEvent('product_view', ...) not 'activity:product_view'.
Permission: events:activity Well-known events: click, page_view, form_submit, product_view, add_to_cart, purchase, search
Hook usage
import { useActivityEvent } from '@stackable-labs/sdk-extension-react'
useActivityEvent('product_view', (event) => {
console.log('Activity:', event.eventName, event.data)
})
Full component example
import { useActivityEvent, Surface, ui } from '@stackable-labs/sdk-extension-react'
import type { ActivityEventHandler } from '@stackable-labs/sdk-extension-contracts'
import { useState } from 'react'
export function Content(): React.ReactElement {
const [lastEvent, setLastEvent] = useState<string | null>(null)
useActivityEvent('page_view', (event) => {
setLastEvent(event.data.url as string)
})
return (
<Surface id="slot.content">
<ui.Text className="text-xs">{lastEvent ?? 'No activity yet'}</ui.Text>
</Surface>
)
}
Identity Events
Subscribe to login, logout, refresh, and expired events. Useful for tracking agent authentication state and reacting to identity enrichment pushed from sibling extensions (via capabilities.identity.extend — see Extend Identity below).
Permission: events:identity Event types: login, logout, refresh, expired
Hook usage
import { useIdentityEvent } from '@stackable-labs/sdk-extension-react'
// manifest events: ["identity:login", "identity:logout", "identity:refresh"]
useIdentityEvent('login', (event) => {
// event.data.state.user.metadata is populated with any enrichment from sibling
// extensions with identity:extend (declared in their manifest.identityClaims)
console.log('User logged in:', event.data.state.user?.email, event.data.state.user?.metadata)
})
useIdentityEvent('logout', () => {
console.log('User logged out')
})
// identity:refresh fires after any extension calls capabilities.identity.extend({...}).
// Listen here to react to post-login enrichment (verification, tier upgrades, etc.).
useIdentityEvent('refresh', (event) => {
console.log('Identity refreshed — metadata:', event.data.state.user?.metadata)
})
Full component example
import { Surface, ui, useContextData, useIdentityEvent } from '@stackable-labs/sdk-extension-react'
import type { IdentityEvent } from '@stackable-labs/sdk-extension-contracts'
// useIdentityEvent — for SIDE EFFECTS on identity lifecycle events.
// If you just want to RENDER based on identity state (e.g. show the current
// user's email), use useContextData() — it re-renders reactively on every
// host-pushed identity change. Use useIdentityEvent only when you need to
// RUN imperative code on a specific event: analytics beacons, cookie writes,
// downstream system notifications, cache invalidation, etc.
//
// manifest.json:
// {
// "permissions": ["context:read", "events:identity"],
// "events": ["identity:login", "identity:logout"]
// }
export function Header(): React.ReactElement {
const ctx = useContextData()
// Side effect: persist a "last login" hint to localStorage on every login.
// Replace with your real-world side effect — analytics beacon, cookie write,
// downstream system notification, etc.
useIdentityEvent('login', (event: IdentityEvent) => {
localStorage.setItem('last-login', JSON.stringify({
userId: event.data.state.user?.id,
timestamp: new Date().toISOString(),
}))
})
// Clear the hint on logout / session expiry.
useIdentityEvent('logout', () => {
localStorage.removeItem('last-login')
})
// Rendering: read directly from ctx — DON'T mirror identity into local
// useState via the event listener; useContextData is already reactive.
const email = ctx?.identity?.user?.email ?? null
return (
<Surface id="slot.header">
<ui.Text className="text-xs">{email ?? 'Not logged in'}</ui.Text>
</Surface>
)
}
Extend Identity
Enrich identity JWT claims and user.metadata so the signed token AND any sibling extension with events:identity can react. Two complementary paths:
useExtendIdentity(handler)— synchronous hook, fires ONCE at initial login. Use for known-at-login enrichment.capabilities.identity.extend(patch)— imperative call, fires post-login (after async verification, webhook callbacks, user-triggered flows). Re-signs the JWT and broadcastsidentity:refreshto all extensions withevents:identity.
Both paths share the identity:extend permission and the manifest.identityClaims declaration gate. Standard JWT claims (external_id, email, name) are exempt; custom keys must be declared or they're dropped by the host filter with a console.warn.
Permission: identity:extend
Login-time hook (useExtendIdentity)
import { useExtendIdentity } from '@stackable-labs/sdk-extension-react'
// manifest.json:
// {
// "permissions": ["identity:extend"],
// "identityClaims": ["loyalty_tier", "verified", "verified_by", "verified_at"]
// }
// Standard JWT claims (external_id, email, name) are exempt from declaration.
// Custom claims must be in manifest.identityClaims or they're dropped with a warn.
//
// Fires ONCE at initial login — return what's known synchronously. For post-login
// async updates (e.g., after verification completes via a webhook or polling),
// use capabilities.identity.extend(patch) — see the 'identity.extend' capability.
useExtendIdentity((claims) => ({
external_id: `custom_${claims.external_id}`, // standard claim override (exempt)
loyalty_tier: 'bronze', // custom — sync, known at login
verified: false, // custom — default; updated async post-verification
}))
Imperative post-login (capabilities.identity.extend)
import { useCapabilities, useContextData, Surface, ui } from '@stackable-labs/sdk-extension-react'
import type { ContextData } from '@stackable-labs/sdk-extension-contracts'
// manifest.json:
// {
// "permissions": ["identity:extend"],
// "identityClaims": ["verified", "verified_by", "verified_at"]
// }
//
// PUSH side — capabilities.identity.extend(patch) sends new claims to the host
// AFTER initial login. The host filters the patch against manifest.identityClaims,
// merges into user.metadata, re-signs the JWT, and broadcasts identity:refresh.
//
// CONSUMER side — useContextData() already re-renders on every host-pushed
// context update (login/logout/refresh/expired). For pure rendering, read the
// enriched value directly from ctx.identity.user.metadata — no event listener
// needed. Use useIdentityEvent only when you need a SIDE EFFECT (analytics,
// cache invalidation, etc.) on a specific event.
//
// Standard JWT claims (external_id, email, name) are exempt from declaration.
// Undeclared custom keys are dropped client-side with a console.warn.
export function Content(): React.ReactElement {
const capabilities = useCapabilities()
const ctx = useContextData() as ContextData & { loading: boolean }
// Push: trigger after async verification (webhook, polling, user action)
const runVerification = async () => {
await new Promise(r => setTimeout(r, 1000)) // simulated async work
await capabilities.identity.extend({
verified: true,
verified_by: 'xyzProvider',
verified_at: new Date().toISOString(),
})
}
// Consume: read directly from ctx — re-renders automatically on identity:refresh.
const isVerified = Boolean(ctx.identity?.user?.metadata?.verified)
return (
<Surface id="slot.content">
<ui.Stack direction="column" gap="2" className="p-3">
<ui.Button onClick={runVerification}>Verify</ui.Button>
<ui.Text>Status: {isVerified ? 'verified' : 'unverified'}</ui.Text>
</ui.Stack>
</Surface>
)
}