01.software Docs

SDK

Use the TypeScript SDK for the default implementation path.

SDK

Use the SDK when you are building a frontend, server route, or workflow that should stay aligned with 01.software conventions.

Setup Flow

Install the package

Add @01.software/sdk to the project package manager already used by the repository.

Create the browser client

Use the Publishable Key only for browser-safe reads and only from approved origins.

Create the server client

Use the Secret Key only in trusted server code for writes and privileged workflows.

Verify the slice

Run the target project's typecheck, lint, tests, and build before handoff.

Client Choice

ContextClientNotes
browser UIbrowser clientreads only, origin-limited
server routeserver clientimport from @01.software/sdk/server for writes and privileged operations
React data fetchingquery helperskeep cache and loading states explicit
AI-generated codeexisting project patternsavoid adding framework layers

Import Boundaries

Use @01.software/sdk for the browser-safe client, commerce helpers, and core types. Use @01.software/sdk/server for createServerClient, and use @01.software/sdk/query only when your app needs React Query helpers.

Rows marked none do not need any extra packages beyond @01.software/sdk.

ImportFeature(s)Install when used
@01.software/sdkbrowser-safe createClient, commerce helpers, collection helpers, typesnone
@01.software/sdk/clientbrowser-safe createClient entrynone
@01.software/sdk/servercreateServerClient, server-only collection, commerce, and preview APIsnone; keep secretKey code on the server
@01.software/sdk/errorsSDK error classes and guardsnone
@01.software/sdk/analyticsbrowser analytics client and typed event helpersnone
@01.software/sdk/metadataSEO metadata extraction and generation helpersnone
@01.software/sdk/webhookwebhook handlers, event guards, and webhook typesnone
@01.software/sdk/queryReact Query hooks, cache helpers, getQueryClient@tanstack/react-query, react, react-dom
@01.software/sdk/realtimeRealtimeConnection, useRealtimeQuery@tanstack/react-query, react, react-dom
@01.software/sdk/storefront-cacheproduct storefront cache resource helpersnone
@01.software/sdk/analytics/react<Analytics />react, react-dom
@01.software/sdk/ui/rich-textRichTextContent, StyledRichTextContentreact, react-dom, @payloadcms/richtext-lexical
@01.software/sdk/ui/formFormRendererreact, react-dom
@01.software/sdk/ui/code-blockCodeBlock, highlightreact, react-dom, shiki, hast-util-to-jsx-runtime
@01.software/sdk/ui/canvasCanvasRenderer, CanvasFrame, useCanvas, prefetchCanvasreact, react-dom, @tanstack/react-query, @xyflow/react, quickjs-emscripten, postcss, sucrase
@01.software/sdk/ui/canvas/servercanvas server helpersnone
@01.software/sdk/ui/videoVideoPlayerreact, react-dom, @mux/mux-player-react
@01.software/sdk/ui/imageImagereact, react-dom

If a feature is not listed here, it does not need a separate peer install. For the full component-to-peer mapping, see the SDK package README.

Tenant Context Introspection

Trusted server workflows can read the resolved tenant features, active and inactive collections, and field configuration through the server-only client:

import { createServerClient } from '@01.software/sdk/server'

const server = createServerClient({
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
  secretKey: process.env.SOFTWARE_SECRET_KEY!,
})

const context = await server.tenant.context()

Use server.tenant.context({ includeCounts: true }) only when collection counts and webhook configuration are required. It performs additional reads and is slower than the default call. Keep this API and both credentials in trusted server code.

Customer Auth

Hosted customer OAuth is the default path for new storefronts. Configure the browser-safe client with the workspace publishable key plus customer auth issuer and client id:

const client = createClient({
  publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
  customer: {
    persist: false,
    oauth: {
      issuer: process.env.NEXT_PUBLIC_SOFTWARE_CUSTOMER_AUTH_ISSUER!,
      clientId: process.env.NEXT_PUBLIC_SOFTWARE_CUSTOMER_CLIENT_ID!,
    },
  },
})

In a trusted route handler, first guard that client.customer.oauth is configured, then call createAuthorizationUrl() to start PKCE. Exchange the callback code server-side and store access, refresh, and transient OAuth state in HttpOnly cookies or another server-owned session store. Do not place hosted customer OAuth tokens in browser JavaScript, localStorage, or client-readable cookies.

client.customer.auth.login() and register() remain available for legacy direct email/password compatibility, but new ecommerce templates should expose that path only behind explicit legacy routes.

Events Admission

Use client.events.getAdmissionAvailability({ occurrence }) to render public paid-admission choices for one event occurrence.

import { createClient } from '@01.software/sdk'

const client = createClient({ publishableKey: 'pk_xxx' })
const admission = await client.events.getAdmissionAvailability({
  occurrence: 'occ_123',
})

const general = admission.admissionTypes[0]

The response includes event/occurrence identity, admission type identity, productId / variantId for the existing cart flow, public price presentment, and a remaining-capacity hint. Each admission type row includes the requested occurrenceId, including all-event admission types. It does not expose raw priceReference, product inventory, holds, checkout/order records, payment sessions, provider event ids, or order-money internals.

Checkout still uses ecommerce helpers: add the returned product/variant to a cart with admission: { eventId, occurrenceId, admissionTypeId }, then use the normal checkout and payment-session flow. This helper is separate from the event-commerce events.products projection.

Market context

Storefronts pass a market to price-and-currency-aware calls. Discover markets and resolve the default with the markets namespace:

const markets = await client.markets.list()
const primary = await client.markets.default()

const cart = await client.commerce.cart.create({ market: primary?.handle })

Only client-safe fields are returned (id, handle, name, countryCode, targetCountries, currency, isPrimary, isActive); FX rates and pricing adjustments stay server-side.

Analytics

@01.software/sdk/analytics tracks pageviews automatically and lets you fire custom events with a single track() call.

import { createAnalytics } from '@01.software/sdk/analytics'

const analytics = createAnalytics({ publishableKey: 'pk_xxx' })
// pageviews are tracked automatically
analytics.track('signup', { plan: 'pro', trial: false })

Register events first. Custom event names, dimensions, and allowed values are defined per workspace in Console → Analytics. An unregistered or mistyped event is accepted by the browser but silently dropped server-side.

Typed events. Declare a plain type (do not extends AnalyticsEventMap — an index signature defeats typo detection) and pass it as the generic. Mistyped names and out-of-enum values fail to compile:

import {
  createAnalytics,
  defineAnalyticsEvents,
} from '@01.software/sdk/analytics'

type ShopEvents = {
  signup: { plan: 'free' | 'pro'; trial: boolean }
  add_to_cart: { productId: string; price: number }
  checkout_start: undefined // no props
}

const analytics = createAnalytics<ShopEvents>({ publishableKey: 'pk_xxx' })

// Bind the map once to avoid repeating the generic:
const create = defineAnalyticsEvents<ShopEvents>()
const a2 = create({ publishableKey: 'pk_xxx' })

React. AnalyticsProvider + useAnalytics() fire events from components. The provider auto-tracks pageviews and owns one instance for its subtree:

Reusing the ShopEvents type from above:

import {
  AnalyticsProvider,
  useAnalytics,
} from '@01.software/sdk/analytics/react'

function Root() {
  return (
    <AnalyticsProvider>
      <App />
    </AnalyticsProvider>
  )
}

function SignupButton() {
  const { track, pageview } = useAnalytics<ShopEvents>()
  // pageview(path?) is available for manual SPA pageviews; the provider already auto-tracks pageviews
  return (
    <button onClick={() => track('signup', { plan: 'pro', trial: false })}>
      Sign up
    </button>
  )
}

<Analytics /> is the pageview-only mount helper for apps that do not fire custom events from components.

Send mode. mode: 'auto' (default) suppresses sends on local hosts (localhost / 127.0.0.1 / *.local). Use 'production' to always send (useful for local smoke tests) or 'development' to never send. The hosted <script> snippet reads mode from window.__01_analytics__.mode or data-mode; the legacy captureOnLocalhost: true flag is equivalent to mode: 'production' and is honored only when mode is unset.

Storefront Cache Resources

Shaped storefront reads use framework-neutral resource names so adapters can map the same invalidation contract to Next.js cache tags, CDN tags, or another cache layer without baking framework APIs into the core SDK. In this section, storefront means public tenant-facing reads for a website, app, docs site, or commerce frontend; it is not limited to ecommerce product pages.

import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'

const cacheScope = {
  tenantId: tenant.id,
  publishableKeyScope: 'default', // app-defined, short, non-secret scope
}

const listingTags = [storefrontCacheResources.productListing(cacheScope)]
const detailTags = [
  storefrontCacheResources.productDetail(cacheScope, { slug: productSlug }),
]

Resource names use this shape:

storefront:v1:tenant:<tenantId>:key:<publishableKeyScope>:resource:<collection>:list
storefront:v1:tenant:<tenantId>:key:<publishableKeyScope>:resource:<collection>:detail:<id|slug>:<identity>

The SDK intentionally exposes public helpers only for product shaped reads today. The other rows reserve the same resource-family grammar for first-party adapters that own those shaped reads; do not add public helpers until the shaped adapter surface exists.

Read surfaceResource familyPublic SDK helper
product listing / product detailproductsproductListing(scope), productDetail(scope, { id }) or { slug }
linkslinksreserved family; no public helper yet
documentsdocumentsreserved family; no public helper yet
gallery itemsgallery-itemsreserved family; no public helper yet
playlistsplaylistsreserved family; no public helper yet
trackstracksreserved family; no public helper yet
media/imagesimagesreserved family; no public helper yet
shipping policiesshipping-policiesreserved family; no public helper yet

Use list resources for list pages, search pages, and shaped reads that aggregate multiple documents. Use detail resources only when the adapter already has the same public identity it used for the cache tag. Slug-based product pages can use { slug } before fetching the shaped detail response, so they do not need a raw collection pre-read just to discover the product ID.

publishableKeyScope should be a stable, short, non-secret key id or fingerprint. Use the default scope only when every publishable key for the tenant sees the same public cache view. If a cache entry is tagged with a key-specific scope, webhook revalidation must revalidate that same scope; for tenant-wide mutations, revalidate every affected public key scope.

Preview, draft, customer-token, and server-credential reads must stay outside the shared storefront cache. Error responses, permission failures, and { found: false } reads should be treated as no-store unless the adapter owns a separate negative-cache policy with its own short TTL.

Next.js Mapping

In a Next.js adapter, pass these names to cached fetch calls with next.tags. The current Next.js revalidateTag API requires a second argument; use "max" for stale-while-revalidate behavior. Tags only participate in revalidation when the response is stored in the Data Cache, so opt into caching for SSG/ISR reads and keep preview, draft, and other dynamic reads as no-store. Next.js cache tags are case-sensitive, must be 256 characters or shorter, and each fetch can carry at most 128 tags; keep tenant/key scopes short and non-secret.

import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'

export async function fetchProductDetail(productSlug: string) {
  const scope = {
    tenantId: process.env.APP_TENANT_ID!,
    publishableKeyScope: process.env.STOREFRONT_CACHE_KEY_SCOPE ?? 'default',
  }
  const tags = [
    storefrontCacheResources.productDetail(scope, { slug: productSlug }),
  ]
  const params = new URLSearchParams({ slug: productSlug })

  return fetch(
    `${process.env.SOFTWARE_API_URL}/api/products/detail?${params}`,
    {
      cache: 'force-cache',
      headers: {
        'X-Publishable-Key': process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
      },
      next: { tags },
    },
  )
}

Webhook route handlers should revalidate the same resource names from an app-owned invalidation payload, not by issuing raw collection reads. This example assumes your route has normalized the platform webhook data into the product identity and public cache scopes your app uses:

import { revalidateTag } from 'next/cache'
import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'

type ProductInvalidation = {
  collection: 'products'
  tenantId: string
  publishableKeyScopes?: string[]
  productId?: string
  currentSlug?: string
  previousSlug?: string
  invalidateListing?: boolean
}

export async function POST(request: Request) {
  const invalidation = (await request.json()) as ProductInvalidation
  const scopes = (invalidation.publishableKeyScopes ?? ['default']).map(
    (publishableKeyScope: string) => ({
      tenantId: invalidation.tenantId,
      publishableKeyScope,
    }),
  )

  if (invalidation.collection === 'products') {
    for (const scope of scopes) {
      if (invalidation.invalidateListing) {
        revalidateTag(storefrontCacheResources.productListing(scope), 'max')
      }
      if (invalidation.productId) {
        revalidateTag(
          storefrontCacheResources.productDetail(scope, {
            id: invalidation.productId,
          }),
          'max',
        )
      }
      for (const slug of [
        invalidation.currentSlug,
        invalidation.previousSlug,
      ]) {
        if (!slug) continue
        revalidateTag(
          storefrontCacheResources.productDetail(scope, { slug }),
          'max',
        )
      }
    }
  }

  return Response.json({ ok: true })
}

The invalidation payload does not have to perform raw collection reads in the adapter, but it must carry the public identity needed for the tags it wants to invalidate. For slug changes, include the previous slug too. If your app cannot track previous slugs yet, use productListing(scope) as a coarse fallback and only tag product detail pages with the listing resource when you deliberately accept that broader invalidation.

Before Production

  • Confirm collection and feature availability for the workspace plan.
  • Confirm key storage and rotation ownership.
  • Confirm failure handling for customer-facing flows.

Do not put Secret Key values in browser bundles, public env vars, analytics events, or generated examples.

Next Actions

  • Need exact HTTP contracts: open API.
  • Need agent setup: open MCP.
  • Need event delivery: open Webhooks.
  • Need ready-made commerce flows? See Commerce helpers.

On this page