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 broadcasts identity:refresh to all extensions with events: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>
  )
}

Auto-generated from Stackable Extension SDK. Questions/Issues? developers@stackablelabs.com

Previous
Messaging Examples
Events & Identity | Stackable Labs :. Dev Documentation