
Research
/Security News
Miasma Mini Shai-Hulud Hits ImmobiliareLabs npm Packages
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.
@01.software/sdk
Advanced tools
Official TypeScript SDK for the 01.software platform.
npm install @01.software/sdk
# or
pnpm add @01.software/sdk
./server, ./webhook, ./realtime, ./storefront-cache, ./ui/*) for tree-shakingcollections.from() for Client (compile-time write prevention)// Analytics — browser pageview tracking + custom events
import { createAnalytics } from '@01.software/sdk/analytics'
const analytics = createAnalytics({ publishableKey: 'pk_xxx' })
// auto-tracks pageviews; call analytics.pageview('/custom-path') manually if needed
analytics.track('signup', { plan: 'pro', trial: false }) // custom event with optional props
// Analytics React component — Vercel Analytics-style mount helper
import { Analytics } from '@01.software/sdk/analytics/react'
export function App() {
return <Analytics />
}
// Main entry - browser client, query builder, commerce helpers, utilities
import { createClient } from '@01.software/sdk'
// Server-only entry - keep Secret Key code out of browser-facing imports
import { createServerClient } from '@01.software/sdk/server'
// Webhook only - webhook handlers
import {
handleWebhook,
createTypedWebhookHandler,
} from '@01.software/sdk/webhook'
// Realtime only
import { createRealtimeClient } from '@01.software/sdk/realtime'
// Storefront cache resource names for SSG/ISR adapters
import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'
// Components - sub-path imports per domain
import { Analytics } from '@01.software/sdk/analytics/react'
import { RichTextContent } from '@01.software/sdk/ui/rich-text'
import { Image } from '@01.software/sdk/ui/image'
import { FormRenderer } from '@01.software/sdk/ui/form'
import { CodeBlock } from '@01.software/sdk/ui/code-block'
import { CanvasRenderer } from '@01.software/sdk/ui/canvas'
import { VideoPlayer } from '@01.software/sdk/ui/video'
The root entry keeps createClient, commerce helpers, collection helpers, and
types lightweight. Server, React Query, and UI features live behind explicit
sub-paths so consumers install feature peers only when they import the matching
entry.
| Import | Feature(s) | Install when used |
|---|---|---|
@01.software/sdk | browser-safe createClient, commerce helpers, collection helpers, types | none |
@01.software/sdk/client | browser-safe createClient entry | none |
@01.software/sdk/server | createServerClient, server-only collection, commerce, and preview APIs | none; keep secretKey code on the server |
@01.software/sdk/query | React Query hooks, cache helpers, getQueryClient | @tanstack/react-query, react, react-dom |
@01.software/sdk/realtime | RealtimeConnection, useRealtimeQuery | @tanstack/react-query, react, react-dom |
@01.software/sdk/storefront-cache | product storefront cache resource name helpers | none |
@01.software/sdk/analytics/react | <Analytics /> | react, react-dom |
@01.software/sdk/ui/rich-text | RichTextContent, StyledRichTextContent | react, react-dom, @payloadcms/richtext-lexical |
@01.software/sdk/ui/form | FormRenderer | react, react-dom |
@01.software/sdk/ui/code-block | CodeBlock, highlight | react, react-dom, shiki, hast-util-to-jsx-runtime |
@01.software/sdk/ui/canvas | CanvasRenderer, CanvasFrame, useCanvas, prefetchCanvas | react, react-dom, @tanstack/react-query, @xyflow/react, quickjs-emscripten, postcss, sucrase |
@01.software/sdk/ui/canvas/server | canvas server helpers | none |
@01.software/sdk/ui/video | VideoPlayer | react, react-dom, @mux/mux-player-react |
@01.software/sdk/ui/image | Image | react, 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
packages/sdk/.claude/rules/components-reference.md.
Migration quick reference:
createClient remains available from @01.software/sdk and
@01.software/sdk/client.createServerClient must be imported from @01.software/sdk/server.@01.software/sdk/query.@01.software/sdk/storefront-cache.@01.software/sdk/ui/*
sub-path and require only that row's peers.@01.software/contracts. The public SDK keeps customer-facing helpers
self-contained and must not import private contracts; Console code should
import shared helpers from contracts directly.import { createClient } from '@01.software/sdk'
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
})
// Query data (returns Payload native response)
const { docs } = await client.collections.from('products').find({
limit: 10,
select: { title: true, slug: true },
})
import { createServerClient } from '@01.software/sdk/server'
import { createServerQueryHooks } from '@01.software/sdk/query'
const server = createServerClient({
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY,
secretKey: process.env.SOFTWARE_SECRET_KEY, // sk01_... opaque API key from Console
})
const serverQuery = createServerQueryHooks(server)
// Create order (server only)
const order = await server.commerce.orders.create({
orderNumber: generateOrderNumber(),
customerSnapshot: { email: 'user@example.com' },
shippingAddress: {
recipientName: 'John',
phone: '010-1234-5678',
postalCode: '12345',
address: 'Seoul',
detailAddress: 'Apt 101',
},
items: [
{ product: productId, variant: variantId, option: optionId, quantity: 1 },
],
totalAmount: 10000,
pgPaymentId: 'provider-payment-id', // optional (omit for free orders)
discountCode: 'WELCOME10', // optional
})
// SSR prefetch (server)
await serverQuery.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
Create-order inputs prefer items; existing orderItems callers remain
supported and are serialized to the same endpoint field. Order reads expose
joined line items at order.items.docs.
Always import createServerClient from @01.software/sdk/server so generated
code and bundlers do not blur the Secret Key boundary.
Server-rendered preview routes can use server.preview.detail() with the
short-lived preview token issued by Console:
const preview = await server.preview.detail(
{ collection: 'products', id: previewId },
{ previewToken },
)
For product pages, server.commerce.product.previewDetail({ id }, { previewToken }) returns the raw product detail payload for the saved
draft/unpublished record addressed by the preview token. detail() wraps the
published storefront payload in a { found, product | reason } result.
Use shaped content helpers when a browser storefront needs relationship-backed media for common public content. These helpers use the publishable key only; the server resolves the tenant, enforces the owning feature, excludes drafts, and returns allowlisted DTOs instead of raw Payload documents.
import { createClient } from '@01.software/sdk'
const client = createClient({ publishableKey: '<publishable-key>' })
const links = await client.content.links.list({
limit: 10,
categorySlug: 'social',
tagSlug: 'footer',
featured: true,
sort: 'title',
})
const gallery = await client.content.galleryItems.list({
gallerySlug: 'spring-lookbook',
limit: 24,
})
content.links.list() calls GET /api/links/storefront; link DTOs include
display fields, categories/tags, thumbnail, and icon media. Operator-only
fields such as tenant, metadata, click counters, and private storage details
are omitted. Use categorySlug and tagSlug for stable storefront URLs when
available; categoryId and tagId are also supported. Expired links are
excluded by default, though visible link DTOs may include expiresAt so
storefronts can render time-sensitive copy.
content.galleryItems.list() calls GET /api/gallery-items/storefront;
gallery item DTOs include title, description, content, gallery reference, and
image media. Tenant, metadata, draft status, and storage/provider internals are
omitted. Gallery item reads require either gallerySlug or galleryId; prefer
gallerySlug for storefront routes. The default gallery item sort is manual,
matching curator order from the Admin Panel.
Both helpers return Payload-style pagination (docs, totalDocs, page,
limit, etc.). limit is bounded server-side to 1..100; invalid query,
publishable-key, feature, rate-limit, and server errors surface through the
SDK's typed error classes and preserve request IDs on client.lastRequestId.
SDK sort inputs are typed public allowlists: links support created/updated/
published dates, title, and featured ordering; gallery items support manual
curator order, created/updated dates, and title.
Use client.collections.from(...).find() only as the advanced raw collection
escape hatch. Browser publishable-key raw reads stay shallow (depth: 0,
joins: false) and are not for relationship-expanded media. Use shaped helpers
for storefront content media, or createServerClient() when a server route
needs full raw collection access with server credentials.
The recommended way to fetch a single product is the shaped helper:
import { createClient } from '@01.software/sdk'
const client = createClient({
publishableKey: '<publishable-key>',
})
const result = await client.commerce.product.detail({
slug: 'every-peach-tee',
})
if (!result.found) {
return notFound()
}
const { product } = result
// product: { product, variants, options, brand, categories, tags, images, videos,
// featuredImage, priceRange, compareAtPriceRange, availableForSale, selectedOrFirstAvailableVariant }
detail() returns ProductDetailResult, a discriminated union:
{ found: true, product: ProductDetail } | { found: false, reason }. The
reason value is one of not_found, not_published, or
feature_disabled, so storefronts can choose between a standard 404, preview
CTA, or feature gating UI. Permission/auth errors, including 403 tenant
mismatches, still throw
typed SDKError subclasses and preserve request IDs through the existing
lastRequestId / onRequestId path.
Public product visibility has two axes. Lifecycle status must still be
published and publishedAt must be current or empty. Then
storefrontVisibility decides storefront exposure: listed products appear in
listing helpers and direct detail, unlisted products are omitted from listing
helpers but can be fetched by direct detail slug/id, and hidden products return
{ found: false, reason: 'not_published' } from detail and are omitted from
listings. Legacy products without storefrontVisibility are treated as
listed.
The successful product payload exposes inventory rollups without sentinel
values: product.totalInventory is the tracked stock sum across non-unlimited
variants, null when no variants are tracked, and
product.hasUnlimitedVariant signals whether any variant is unlimited.
When a PDP or listing UI is served behind the Console Edge CDN, prefer the
catalog/stock split instead of reading variant.stock from a cached
detail() response (inventory in that payload can lag for the catalog TTL).
import { createClient, mergeProductDetailWithStock } from '@01.software/sdk'
const client = createClient({ publishableKey: '<publishable-key>' })
const catalog = await client.commerce.product.detailCatalog({
slug: 'every-peach-tee',
})
if (!catalog.found) return notFound()
const variantIds = catalog.product.variants.map((v) => v.id)
const snapshot = await client.commerce.product.stockSnapshot({ variantIds })
const { product, stockMergeStatus } = mergeProductDetailWithStock(
catalog.product,
snapshot,
)
// stockMergeStatus: 'complete' | 'partial' — partial when a variant id is missing from snapshot
Listing UIs use the same pattern: listingPage() for PLP/search grids, or
listingGroupsCatalog({ productIds }) when curated product IDs are already
known, then stockSnapshot() (or stock-check at cart/checkout) for live
availability. detail() and POST detail/listing endpoints are unchanged during
the migration window; see ADR 0012 addendum in docs/decisions/0012-sdk-public-commerce-contract.md.
Recommended path: Use commerce.product.listingPage() (or
createQueryHooks(client).useProductListingPage()) for greenfield storefront
PLPs. It wraps the cacheable listing-groups query endpoint, keeps the raw
Payload pagination response, and adds cards built with
buildProductListingCard(). The endpoint returns pre-grouped listing data and
avoids the top-level products.options / products.variants join truncation
that raw REST product queries hit by default.
const response = await client.commerce.product.listingPage({
search: 'shirt',
limit: 24,
filters: {
categoryIds: ['category-1'],
price: { min: 10000, max: 50000 },
availableForSale: true,
},
basePath: '/shop',
})
const cards = response.cards
Use commerce.product.listingGroupsCatalog({ productIds }) when product IDs
are already known, for example curated rails, recommendations, or editorial
sections:
const response = await client.commerce.product.listingGroupsCatalog({
productIds: ['product-1', 'product-2'],
})
Server-auth escape hatch: When server code deliberately needs raw
products collection reads (bulk operations, custom filters, fields the helper
does not expose), use createServerClient() and spread
PRODUCT_PLP_FIND_OPTIONS to raise the default Payload join limit of 10:
import {
PRODUCT_PLP_FIND_OPTIONS,
projectProductToListingShape,
} from '@01.software/sdk'
import { createServerClient } from '@01.software/sdk/server'
const server = createServerClient({
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
apiUrl: process.env.SOFTWARE_API_URL!,
secretKey: process.env.SOFTWARE_SECRET_KEY!,
})
const now = new Date().toISOString()
const { docs } = await server.collections.from('products').find({
...PRODUCT_PLP_FIND_OPTIONS,
where: {
and: [
{ status: { equals: 'published' } },
{
or: [
{ publishedAt: { less_than_equal: now } },
{ publishedAt: { exists: false } },
{ publishedAt: { equals: null } },
],
},
{
or: [
{ storefrontVisibility: { equals: 'listed' } },
{ storefrontVisibility: { exists: false } },
{ storefrontVisibility: { equals: null } },
],
},
],
},
limit: 24,
})
const listingProducts = docs.map(projectProductToListingShape)
PRODUCT_PLP_FIND_OPTIONS sets joins.variants and joins.options to safe
limits with sort: '_order'. It cures top-level products.options and
products.variants join truncation but cannot cure the nested
options[].values.docs join — the Payload REST joins param is flat and
nested join limits require the listing-groups endpoint. This preset is not
accepted by publishable createClient().collections.from('products').find()
because browser-public raw reads are constrained to depth: 0 and
joins: false, and cannot use populate.
import {
buildProductHref,
buildProductOptionMatrixFromDetail,
getProductSelectionImages,
resolveProductSelectionFromMatrix,
} from '@01.software/sdk'
const matrix = buildProductOptionMatrixFromDetail(product)
const selection = resolveProductSelectionFromMatrix(
matrix,
{ search: '?opt.color=black&opt.size=s' },
undefined,
{ detail: product },
)
const images = getProductSelectionImages(selection) // object media only, deduped
const href = buildProductHref(product, {
optionSlug: 'color',
optionValueSlug: 'black',
})
Selection media follows the resolved selection: a complete variant uses that variant's media first; a partial option selection uses selected option-value media first, then matching variant media, before falling back to listing or product media. This keeps listing-card selection links and detail-page images aligned without rebuilding media priority in storefront code.
For new storefront work, prefer pool-pointer and gallery-aware resolution from
commerce.product.detail() + resolveProductSelection() (and
getProductSelectionImages() when a list is needed). Direct pre-ADR-0025 fields like
variant.thumbnail and option-value direct images are still accepted as
transitional input, but are no longer primary storefront media sources.
availableValuesByOptionSlug / availableValuesByOptionId include
availableStock, isUnlimited, and availableForSale per value so option UIs
can render stock state without recalculating from variants. Each entry also
exposes handoff-aligned aliases (exists == available, label == value) and
an optional Shopify-shaped swatch: { color, image } alongside flat thumbnail
and images. Only swatch.image falls back to the first entry in images when
thumbnail is absent; flat thumbnail and images on the value object are
unchanged from the matrix source. Option-value upsert and detail/matrix shapes use
nested swatch only (swatch.color for hex); flat swatchColor is rejected.
import { createQueryHooks } from '@01.software/sdk/query'
const query = createQueryHooks(client)
const { data: product, isLoading } = query.useProductDetailBySlug(slug)
Cache key is ['products', 'detail', { slug }]. Mutations on products, product-variants, product-options, product-option-values, brands, brand-logos, images, and related collections automatically invalidate this cache.
Use createProductSelectionCodec(detail) when product pages need to keep option
selection in the URL. By default, complete selections emit variant=<variantId>
and partial selections emit slug-compat params such as ?opt.color=ivory.
Inbound canonical ID params (?opt.<optionId>=<valueId>) and compatibility slug
params (?opt.<optionSlug>=<valueSlug>) still parse. Plain bare keys such as
?color=ivory are rejected.
import {
createProductSelectionCodec,
resolveProductSelection,
} from '@01.software/sdk'
const codec = createProductSelectionCodec(product)
const normalizedSelection = codec.parse('?opt.color=ivory')
const selection = resolveProductSelection(product, normalizedSelection)
const selectionQuery = codec.stringify(normalizedSelection)
// selectionQuery === 'opt.color=ivory' for partial selections
// selectionQuery === 'variant=variant-black-s' once a complete variant is selected
// selection.selectedVariant, selection.price, selection.stock, selection.media
When selection input is omitted, resolveProductSelection() applies
selectedOrFirstAvailableVariant so PDP defaults match listing cards. That is
separate from fillDefaults and is not gated by a flag.
// PDP default (uses selectedOrFirstAvailableVariant when selection is omitted)
resolveProductSelection(product)
// Catalog: keep price range / no concrete variant
resolveProductSelection(product, { valueIds: [] })
By default, partial selections (for example color only) leave
selectedVariant as null. Opt in to Shopify-style
selectedOrFirstAvailableVariant behavior with fillDefaults: true:
const resolution = resolveProductSelection(
product,
codec.parse('?opt.color=ivory'),
{
fillDefaults: true,
},
)
// resolution.selectedVariant is concrete; unselected options are filled
// using the same available-by-order rule that derives selectedOrFirstAvailableVariant.
For option-click handlers, use selectNext() to apply a slug transition,
keep compatible prior selections, and re-default incompatible ones.
selectNext() already fills missing options internally:
import { resolveProductSelection, selectNext } from '@01.software/sdk'
const nextSelection = selectNext(product, currentSelection, 'color', 'ivory')
const resolution = resolveProductSelection(product, nextSelection)
Use fillDefaults: true on resolveProductSelection() when you have a
partial URL or selection state and need a concrete variant without calling
selectNext(). It does not change codec parse/stringify behavior.
Opt out of slug-compat outbound URLs with
createProductSelectionCodec(product, { emit: 'canonical-id' }).
Normalized selection state uses stable option/value/variant IDs internally. Slugs in URLs are a compatibility/readability layer, not the identity source.
For listing cards, pass the listing group returned by
buildProductListingGroupsByOption() or the listing-groups endpoint into
buildProductHref(product, group, { detail }). Listing swatch hrefs emit
partial slug-compat hints such as ?opt.color=ivory by default. When full
detail is not available on a product-list page, pass the group without
detail; buildProductHref() still emits the best available selection hint and
the detail page can resolve it through resolveProductSelection().
Use preferCompleteVariantFromHint: true on buildProductHref() only when a
listing card should deep-link a complete hint variant instead of a color-only
partial hint.
Do not use bare option query keys such as ?size=large. The SDK rejects them
as ambiguous because product pages commonly share URLs with unrelated search,
filter, analytics, or framework parameters. Namespacing selection keys under
opt. lets the codec distinguish product-option state from ordinary query
parameters while still allowing unrelated parameters such as utm_campaign to
coexist without being interpreted as selection state.
For SEO, treat the product path without selection params as the canonical URL. Selection query params are share/deep-link state, not index targets.
buildProductListingCard(item, options?) turns a single
commerce.product.listingPage() or listingGroupsCatalog() response item into
a render-ready ProductListingCard. Each item includes listingGroupingState (grouped,
no_primary_option, or empty) and, when empty, listingGroupingEmptyReason
(primary_option_not_linked, primary_option_has_no_values, or
no_variants_for_primary_option). Each group includes public-safe
variants[] alongside variantIds/variantCount so storefronts can render or
inspect grouped variant fields without a follow-up fetch. The by-ids response
also returns missing: string[] for requested product IDs that were not found,
not published, or not accessible; docs preserve the input productIds order
for returned products. The helper populates optional representativeVariant, a
PDP-seeded href, representative media from Shopify-shaped
product.featuredImage with product gallery fallback, Product-level
priceRange / compareAtPriceRange, Product-level availableForSale, and a
swatches[] array derived from groups when there is more than one. Single-group
products emit swatches: []; storefronts that disagree can read item.groups
directly.
buildProductListingCard() derives card swatches from listing-group
optionValueSwatch. Image swatches use swatch.mediaItemId; color swatches use
swatch.color. Option-value thumbnail/gallery fields are no longer part of the
public listing-group or product-detail contract. New PLP filters and sorts
should use Product-shaped names such as priceRange.minVariantPrice.amount,
priceRange.maxVariantPrice.amount, and availableForSale.
import {
buildProductListingCard,
type ProductListingCard,
} from '@01.software/sdk'
const cards: ProductListingCard[] = response.docs.map((item) =>
buildProductListingCard(item, { basePath: '/shop' }),
)
The card href is the product path by default; the PDP resolves the
representative variant through resolveProductSelection(detail) without a
selection param. Each swatch carries a hint-only slug-compat href such as
?opt.color=ivory; the detail page resolves it through
resolveProductSelection(detail, { search }). Use
preferCompleteVariantFromHint: true on buildProductListingCard() only when
the card should deep-link a complete hint variant.
commerce.product.listingPage() or useProductListingPage() (GET /api/products/listing-groups/query/catalog, CDN-cacheable, card-ready). Use listingGroupsCatalog({ productIds }) only when IDs are already known. Treat the full listing-groups response shape as a server-auth escape hatch because it can include operational stock fields. Avoid fetching a product list and then calling detail() per card.useProductDetailBySlug() / commerce.product.detail(). Override staleTime / retry on the hook when you need fresher catalog data or faster failure on errors.detailCatalog() and listingGroupsCatalog() (GET, cacheable) plus batched stockSnapshot() for live inventory.getQueryClient() keeps SSR data fresh forever (staleTime: Infinity). For client-only storefronts, use getStorefrontQueryClient() (~1 minute staleTime) when creating query hooks:import { createClient } from '@01.software/sdk'
import {
createQueryHooks,
getStorefrontQueryClient,
} from '@01.software/sdk/query'
const client = createClient({ publishableKey: '...' })
const query = createQueryHooks(client, getStorefrontQueryClient())
Most consumers should use the helper APIs above (commerce.product.detail, etc.). The query builder below is the escape hatch for advanced cases the helpers do not cover: bulk operations, custom filter combinations, or fields the helper response does not expose.
depth — how deep to populate relationship fieldsdepth is the primary control for populating relationships like category, images, brand. Browser publishable-key raw collection reads are constrained to depth: 0 with joins: false and no populate; relationship-rich storefront reads should use shaped helpers such as commerce.product.detail() / listingPage(), or a createServerClient() raw query when server credentials are appropriate. Browser SDK raw reads add those safe defaults automatically and reject relationship-expanded raw read options before making a request.
const product = await client.collections.from('products').findById(id, {
depth: 0,
joins: false,
})
populate — which fields come back for populated relationshipspopulate controls which fields are returned per collection. It does NOT decide which relationships to populate — that is depth.
await server.collections.from('products').find({
depth: 2,
populate: {
categories: { title: true, slug: true },
images: { url: true, alt: true },
},
})
joins — Payload join-field reverse-relationsjoins is the correct control for Payload type: 'join' virtual reverse-relation fields. In this platform's SDK schema, browser-public relations such as products.variants, products.options, and article-authors.articles, plus server-auth relations such as customers.orders, customers.addresses, posts.comments, and orders.{items,transactions,fulfillments,returns}, are all join fields — you must use joins (not depth/populate) to control their pagination, sorting, filtering, and count. Internal backing joins such as product collection memberships are intentionally omitted from SDK collection types.
// Canonical product detail query — variants/options are join fields on Products
await server.collections.from('products').find({
where: { slug: { equals } },
joins: {
variants: { limit: 50, sort: '_order' },
options: {},
},
depth: 2, // also populate normal relationships (category, brand, etc.)
})
// Disable all join-field population for a lightweight list query
await client.collections.from('products').find({
depth: 0,
joins: false,
})
Each join field defaults to limit 10 when joins is omitted. depth does not raise that cap — storefront PLPs that call products.find() with only depth and then buildProductListingGroupsByOption() can silently drop color swatches. Prefer listingPage() for PLP cards, or use PRODUCT_PLP_FIND_OPTIONS only in server-auth raw product queries (see PLP join-safe queries above).
Publishable-key browser raw reads must keep depth: 0, joins: false, and omit populate; relationship-expanded public storefront reads belong behind shaped helpers. Use createServerClient() for raw joins queries that need server credentials.
joins does NOT populate normal relationship fields. Keys that do not match a type: 'join' field on the queried collection are silently ignored — e.g. joins: { category: {} } on Products is a no-op because category is not a join field there. For normal relationships use depth (and optionally populate).
Use id-based filters as the default — they're the most reliable:
await client.collections.from('product-variants').find({
where: { product: { equals: productId } },
})
Dotted-path filters (where: { 'product.slug': { equals } }) are Payload-native but may silently return empty when access control restricts the related document or when the relation is polymorphic.
Checklist when find() returns docs: [] unexpectedly, in order of likelihood:
status: 'published', a current or unset publishedAt, and listable storefront visibility (listed, legacy missing, or null) for publishable-key raw listing reads. Draft, future, unlisted, hidden, or malformed products silently disappear from raw listing results even when their slug or ID matches. Use the shaped product detail helper for direct-link unlisted products. Correlate with backend logs via client.lastRequestId (or catch SDKError.requestId).generateStaticParams / generateMetadata / the page render must all see the same tenant context. A wrong or missing key at build time produces a baked-in empty response.cache: 'no-store' or export const revalidate = 0 on server components that should reflect live data.where: { slug: 'x' } string shorthand. Always use { slug: { equals: 'x' } } — bare strings silently match nothing.joins. Keys not matching a type: 'join' field on the queried collection are silently ignored (no error). For normal relationship fields use depth/populate, not joins.where: { 'category.slug': { equals } }) under polymorphic or access-control constraints — switch to id-based filter: where: { category: { equals: id } }.depth impacts static generation cost. Deeper populates = larger build payloads. Use select/populate to trim response shape.cache: 'no-store' or export const revalidate = 0 on the route segment, or pass per-fetch options if you proxy the SDK behind your own fetcher.// app/products/[slug]/page.tsx
import { createClient } from '@01.software/sdk'
export const revalidate = 60 // ISR — adjust per page freshness need
export default async function ProductPage({ params }) {
const client = createClient({
publishableKey: '<publishable-key>',
})
const result = await client.commerce.product.detail({ slug: params.slug })
if (!result.found) return notFound()
const { product } = result
// ...
}
const client = createClient({
publishableKey: string, // Required
apiUrl?: string, // Optional API origin override
})
const server = createServerClient({
publishableKey: string,
secretKey: string, // sk01_... or pat01_...
apiUrl?: string, // Optional API origin override
})
| Option | Type | Description |
|---|---|---|
publishableKey | string | API publishable key |
secretKey | string | API secret key or PAT (server only) |
apiUrl | string | Optional API origin override for staging, preview, or proxies |
Use apiUrl: string when an SDK instance should target a non-default API
origin.
API URL resolution order:
apiUrl passed to createClient() or createServerClient()SOFTWARE_API_URL (server) or NEXT_PUBLIC_SOFTWARE_API_URL (browser)DEFAULT_API_URL when injected at build time; otherwise dev-tagged SDK builds (-dev. versions) use https://api.stg.01.software, and regular releases use https://api.01.softwareAccess collections via client.collections.from(slug).
Note: the root
client.collections.from()type exposes the lightweight read surface (find,findById,count). Metadata helpers live behind the optional@01.software/sdk/metadataentry, and write operations (create,update,remove,updateMany,removeMany) are only available onserver.collections.from().
// List query - returns PayloadFindResponse
const { docs, totalDocs, hasNextPage } = await client.collections
.from('products')
.find({
limit: 20,
page: 1,
sort: '-createdAt',
depth: 0,
select: { title: true, slug: true },
})
// Query with populate/joins control
const { docs } = await client.collections.from('products').find({
select: { title: true, slug: true, price: true, thumbnail: true },
joins: false, // disable joins for lightweight list
})
// Override relationship populate and join expansion (server credentials only)
const product = await server.collections.from('products').findById(id, {
populate: { brands: { name: true, logo: true } },
joins: { variants: { limit: 50 } },
})
// Single item query - returns document directly
const product = await client.collections.from('products').findById('id')
Raw collection mutations are an escape hatch. For ecommerce product catalog
writes, prefer server.commerce.product.upsert() so options, option-values,
and variants are written through the domain transaction.
// Create (server only) - returns PayloadMutationResponse
const { doc, message } = await server.collections
.from('articles')
.create({ title: 'Article' })
// Create with file upload (server only) - uses multipart/form-data
const { doc } = await server.collections
.from('images')
.create({ alt: 'Hero image' }, { file: imageFile, filename: 'hero.jpg' })
// Update (server only) - returns PayloadMutationResponse
const { doc } = await server.collections
.from('articles')
.update('id', { title: 'Updated article' })
// Update with file replacement (server only)
await server.collections
.from('images')
.update('id', { alt: 'New alt' }, { file: newFile })
// Delete (server only) - returns document directly
const deletedDoc = await server.collections.from('articles').remove('id')
// Count
const { totalDocs } = await client.collections.from('products').count()
// SEO Metadata (generate from a fetched document)
import { extractSeo, generateMetadata } from '@01.software/sdk/metadata'
const { docs } = await client.collections.from('products').find({
where: { slug: { equals: 'my-product' } },
limit: 1,
depth: 0,
})
const metadata = docs[0]
? generateMetadata(extractSeo(docs[0]), { siteName: 'My Store' })
: null
// Bulk operations (server only)
await server.collections.from('articles').updateMany(where, data)
await server.collections.from('articles').removeMany(where)
The SDK returns Payload CMS native response types without wrapping:
// find() returns PayloadFindResponse<T>
interface PayloadFindResponse<T> {
docs: T[]
totalDocs: number
limit: number
totalPages: number
page: number
pagingCounter: number
hasPrevPage: boolean
hasNextPage: boolean
prevPage: number | null
nextPage: number | null
}
// create() / update() returns PayloadMutationResponse<T>
interface PayloadMutationResponse<T> {
message: string
doc: T
errors?: unknown[]
}
// findById() / remove() returns T (document directly)
| Operation | Response Type |
|---|---|
find() | PayloadFindResponse<T> - { docs, totalDocs, hasNextPage, ... } |
findById() | T - document object directly |
create() | PayloadMutationResponse<T> - { doc, message } |
update() | PayloadMutationResponse<T> - { doc, message } |
remove() | T - deleted document object directly |
count() | { totalDocs: number } |
React Query helpers are opt-in through @01.software/sdk/query. Install
@tanstack/react-query (and React peers) only when your app imports this
sub-path. Browser components should use createQueryHooks(client) for
browser-safe reads and customer auth hooks. Collection writes belong in trusted
server code via createServerClient.
import { createQueryHooks } from '@01.software/sdk/query'
const query = createQueryHooks(client)
// List query
const { data, isLoading } = query.useQuery({
collection: 'products',
options: { limit: 10 },
})
// Suspense mode
const { data } = query.useSuspenseQuery({
collection: 'products',
options: { limit: 10 },
})
// Query by ID
const { data } = query.useQueryById({
collection: 'products',
id: 'product_id',
})
// Infinite scroll
const { data, fetchNextPage, hasNextPage } = query.useInfiniteQuery({
collection: 'products',
options: { limit: 20 },
})
// SSR Prefetch
await query.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
await query.prefetchQueryById({
collection: 'products',
id: 'product_id',
})
await query.prefetchInfiniteQuery({
collection: 'products',
pageSize: 20,
})
// Cache utilities
query.invalidateQueries('products')
query.getQueryData('products', 'list', options)
query.setQueryData('products', 'detail', id, data)
// Customer auth hooks (Client only)
const { data: profile } = query.useCustomerMe()
const { mutate: login } = query.useCustomerLogin()
const { mutate: register } = query.useCustomerRegister()
const { mutate: logout } = query.useCustomerLogout()
login({ email: 'user@example.com', password: 'password' })
// Other customer mutations
query.useCustomerForgotPassword()
query.useCustomerResetPassword()
query.useCustomerChangePassword()
// Customer cache utilities
query.invalidateCustomerQueries()
query.getCustomerData()
query.setCustomerData(profile)
// Server action / API route for collection writes
import { createServerClient } from '@01.software/sdk/server'
const server = createServerClient({
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
secretKey: process.env.SOFTWARE_SECRET_KEY!,
})
await server.collections.from('articles').update('article_id', {
title: 'Updated article',
})
Customer auth methods currently cover local email/password flows: register,
login, refresh, password reset, profile read/update, and password change.
CustomerProfile.authProvider may contain google, apple, kakao, or
naver for accounts created through platform/provider integrations, but the
SDK does not expose social-login initiation or callback helpers yet.
Available on Client via client.customer.auth.*.
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
customer: { persist: true },
})
// Register & login
const { customer } = await client.customer.auth.register({
name: 'John',
email: 'john@example.com',
password: 'secure123',
})
const { token, customer } = await client.customer.auth.login({
email: 'john@example.com',
password: 'secure123',
})
// Profile & token management
const profile = await client.customer.auth.me()
client.customer.auth.isAuthenticated()
client.customer.auth.logout()
// Authenticated customer's own orders (Client-only)
const orders = await client.commerce.orders.listMine({
page: 1,
limit: 10,
displayFinancialStatus: 'paid',
})
// Password
await client.customer.auth.forgotPassword('john@example.com')
await client.customer.auth.resetPassword(token, newPassword)
await client.customer.auth.changePassword(currentPassword, newPassword)
Available on ServerClient via server.commerce.orders.*. checkout and listMine are also on Client and return sanitized customer-facing order DTOs. Customer order DTOs expose the independent status axes (displayFinancialStatus, displayFulfillmentStatus, returnStatus, primaryDisplayStatus, returnDisplayStatus); status and displayStatus are read-only compatibility aliases for older clients. Use server.collections.from('orders') for raw operational order documents.
// Orders
await server.commerce.orders.create(params)
await server.commerce.orders.update({ orderNumber, status: 'confirmed' })
await server.commerce.orders.checkout({ cartId, pgPaymentId?, orderNumber, customerSnapshot, discountCode? })
// Single-order lookup (getOrder endpoint removed — use collection find)
const { docs: [order] } = await server.collections.from('orders').find({
where: { orderNumber: { equals: 'ORD-...' } },
limit: 1,
depth: 1,
})
// Fulfillment
await server.commerce.orders.prepareFulfillmentOrder({ orderNumber })
await server.commerce.orders.createFulfillment({ orderNumber, items })
await server.commerce.orders.updateFulfillment({ fulfillmentId, carrier, trackingNumber })
await server.commerce.orders.bulkImportFulfillments({ items: [{ orderNumber, carrier, trackingNumber }] })
// Provider-verified payment confirmation
// Provider webhook handlers should verify with the provider first, then call:
await server.commerce.orders.confirmPayment({
orderNumber,
pgProvider: 'provider-name',
pgPaymentId,
amount,
providerStatus: 'PAID',
providerEventId,
})
// Low-level transaction annotation / compatibility path. Prefer confirmPayment()
// for normal provider-verified paid transitions.
// status: 'failed' records a retryable PG failure on the Transaction only; the
// Order stays pending until verified payment succeeds or the merchant cancels it.
await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMethod, receiptUrl })
// Returns
await server.commerce.orders.createReturn({
orderNumber,
returnItems: [{ orderItem, quantity, restockingFee? }],
refundAmount,
returnShippingFee?,
initialShippingRefundAmount?,
initialShippingRefundOverrideNote?,
reason?,
})
await server.commerce.orders.updateReturn({ returnId, status })
await server.commerce.orders.returnWithRefund({
orderNumber,
returnItems: [{ orderItem, quantity, restockingFee? }],
refundAmount,
returnShippingFee?,
initialShippingRefundAmount?,
initialShippingRefundOverrideNote?,
pgPaymentId,
})
Available on both Client (customer JWT) and ServerClient (secretKey).
await client.commerce.discounts.validate({ code, orderAmount })
await client.commerce.shipping.calculate({ shippingPolicyId?, orderAmount, postalCode? })
// calculateShipping returns: { shippingAmount, baseShippingAmount, extraShippingAmount, freeShipping, freeShippingMinAmount, isJeju, isRemoteIsland }
Product reads are available on both Client and ServerClient via commerce.product.*.
Product catalog writes are ServerClient-only.
Use server.commerce.product.upsert() for product catalog writes that include
options, option values, and variants. It is the tenant-admin safe path because
it applies the product/option/variant transaction that raw collection writes do
not provide.
commerce.product.upsert() is a graph-write API. For Admin Panel product
documents, Payload saves product fields first; upsert receives productId,
graphRevision (required when updating an existing product via productId -
load from GET /api/products/:id/composer-draft), options, and variants. Do
not send removed pre-ADR-0025 media inputs (optionValue.thumbnail,
optionValue.images, variant.thumbnail); use swatch.mediaItemId and
variant images[]. Unknown keys are not part of the published upsert contract.
| Payload | Valid | Invalid |
|---|---|---|
| Create | product: { title, ... } + graph | productId on create; missing product.title |
| Edit graph | productId + graphRevision? + graph | product.title / SEO on upsert; conflicting productId vs product.id |
| Edit existing | product: { id } + graph only | product: { id, title } on edit |
const result = await server.commerce.product.upsert({
product: {
title: 'Every Peach Tee',
slug: 'every-peach-tee',
status: 'published',
},
options: [
{
title: 'Color',
slug: 'color',
values: [
{
value: 'Black',
slug: 'black',
swatch: { type: 'color', color: '#111111' },
},
{
value: 'White',
slug: 'white',
swatch: { type: 'color', color: '#ffffff' },
},
],
},
{
title: 'Size',
slug: 'size',
values: [
{ value: 'Small', slug: 's' },
{ value: 'Medium', slug: 'm' },
],
},
],
variants: [
{
optionValues: { color: { valueSlug: 'black' }, size: { valueSlug: 's' } },
sku: 'TEE-BLK-S',
price: 29000,
stock: 10,
isActive: true,
},
{
optionValues: { color: { valueSlug: 'white' }, size: { valueSlug: 'm' } },
sku: 'TEE-WHT-M',
price: 29000,
stock: 8,
isActive: true,
},
],
})
if (!result.ok) {
throw new Error(result.message)
}
Existing product graph edits send the saved product id and the composer draft baseline, without product document fields:
await client.commerce.product.upsert({
productId: 'prod_123',
graphRevision: draft.graphRevision,
options: draft.options,
variants: draft.variants,
})
For updates to existing options or option-values, prefer id / valueId when
available so rename-safe updates do not depend on slugs.
// Batch stock check (point-in-time read, NOT a reservation)
const { results, allAvailable } = await client.commerce.product.stockCheck({
items: [{ variantId: '...', quantity: 2 }],
})
for (const item of results) {
if (item.status === 'available' && item.isUnlimited) {
// Unlimited stock is explicit; availableStock is a numeric count, not a sentinel.
continue
}
if (item.status === 'not_published' || item.status === 'archived') {
// Variant still exists, but its parent product is not currently sellable.
continue
}
}
Available on both Client and ServerClient via commerce.cart.*. These helpers return sanitized customer-facing cart DTOs; use server.collections.from('carts' | 'cart-items') for raw operational cart documents.
// Add item to cart
await client.commerce.cart.addItem({
cartId,
product,
variant,
option,
quantity,
})
// Update item quantity
await client.commerce.cart.updateItem({ cartItemId, quantity })
// Remove item from cart
await client.commerce.cart.removeItem({ cartItemId })
// Other cart operations return sanitized customer-facing cart DTOs.
await client.commerce.cart.get(cartId)
await client.commerce.cart.applyDiscount({ cartId, discountCode })
await client.commerce.cart.removeDiscount({ cartId })
await client.commerce.cart.clear({ cartId })
Available only on ServerClient via server.community.moderation.*.
await server.community.moderation.banCustomer({ customerId, isPermanent?, bannedUntil?, reason? })
await server.community.moderation.unbanCustomer({ customerId })
Create or open the endpoint in Console → Integrations → Webhooks. After saving,
use Reveal signing secret once for the initial value, or Rotate secret to
invalidate the previous secret and receive a new one-time plaintext value. Set the
copied value in the receiver environment as WEBHOOK_SECRET.
Webhook list/read responses do not include plaintext signing secrets — only
the Console reveal/rotate flows return them. Use WebhookWithoutSecret and
WebhookSigningSecretReveal from @01.software/sdk/webhook for typed clients.
This value is not
SOFTWARE_SECRET_KEY; multiple webhook endpoints may require multiple receiver
secrets.
Use HMAC-SHA256 signature verification:
import { handleWebhook, createTypedWebhookHandler } from '@01.software/sdk'
const handler = createTypedWebhookHandler('orders', async (event) => {
// event.data is typed as Order
console.log(event.data.orderNumber)
})
export async function POST(request: Request) {
const secret = process.env.WEBHOOK_SECRET
if (!secret) throw new Error('WEBHOOK_SECRET is required')
return handleWebhook(request, handler, {
secret,
})
}
// Signed deliveries include x-webhook-signature, x-webhook-timestamp,
// and x-webhook-delivery-id. handleWebhook rejects stale or unsigned
// deliveries when secret is set.
// Customer auth helper
import { createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
async function sendPasswordResetEmail(
email: string,
resetPasswordToken: string,
): Promise<void> {
console.log('Send password reset email', email, resetPasswordToken)
}
const customerAuthHandler = createCustomerAuthWebhookHandler({
passwordReset: async ({ email, resetPasswordToken }) => {
await sendPasswordResetEmail(email, resetPasswordToken)
},
})
// Semantic order-change events keep operation as "update" for compatibility.
// Use isOrderChangedWebhookEvent when you need to distinguish manual ordering
// from content field edits.
import {
handleWebhook,
isOrderChangedWebhookEvent,
} from '@01.software/sdk/webhook'
function getWebhookSecret(): string {
const secret = process.env.WEBHOOK_SECRET
if (!secret) throw new Error('WEBHOOK_SECRET is required')
return secret
}
export async function POST(request: Request) {
return handleWebhook(
request,
async (event) => {
if (isOrderChangedWebhookEvent(event)) {
console.log(event.collection, event.change.scope, event.change.moved)
return
}
console.log(event.collection, event.operation)
},
{ secret: getWebhookSecret() },
)
}
Orderable Admin Panel drag operations are delivered as operation: "update"
with eventType: "collection.orderChanged". For join ordering, the webhook
uses the public parent collection as collection and points event.change.moved
at the public moved entity when one exists, so handlers do not need to depend on
hidden Payload order fields or private backing rows.
Customer group member ordering is currently treated as an unsupported hidden
join-order surface and does not emit a semantic order-change webhook.
Source of truth: packages/sdk/src/core/collection/const.ts (COLLECTIONS: 51).
| Category | Browser-public generic collections |
|---|---|
| Tenant | tenants, tenant-metadata |
| Products | products, product-variants, product-options, product-option-values, product-categories, product-tags, product-collections, brands |
| Customers | customer-profiles |
| Commerce | discounts, shipping-policies, shipping-zones |
| Content | documents, document-categories, document-types, articles, article-authors, article-categories, article-tags, links, link-categories, link-tags |
| Playlists / Tracks | playlists, playlist-categories, playlist-tags, tracks, track-categories, track-tags |
| Galleries | galleries, gallery-categories, gallery-tags, gallery-items |
| Canvas | canvases, canvas-node-types, canvas-edge-types, canvas-categories, canvas-tags |
| Videos | video-categories, video-tags |
| Forms | forms |
| Community | reaction-types, post-categories, post-tags, customer-profile-lists |
| Events | event-calendars, events, event-categories, event-occurrences, event-tags |
Server-only collections include raw media/logo records, customer and order
operational records, raw cart/cart-item records, form submissions, raw community documents
(posts, comments, reactions, bookmarks), live stream provider records,
Canvas graph shell rows (canvas-nodes, canvas-edges), segmentation records,
and moderation records. They remain available from
createServerClient().collections with secret/PAT credentials. Shaped
browser/customer helpers such as commerce.cart.*,
commerce.orders.listMine(), and commerce.orders.checkout() remain available
where customer-facing DTOs are needed, but raw slugs are intentionally absent
from browser collection discovery.
Resolves a Payload CMS relation field value. When depth is 0, relation fields return just an ID (number). When depth > 0, they return the full object. This utility normalizes both cases.
import { resolveRelation } from '@01.software/sdk'
const authors = post.authors?.map((a) => resolveRelation(a)) ?? [] // Author[]
Note: Prefer
resolveRelation. It covers the same normalization use case directly.
import { generateOrderNumber } from '@01.software/sdk'
const orderNumber = generateOrderNumber()
// "260121123456" (YYMMDDRRRRRR format)
import { formatOrderName } from '@01.software/sdk'
formatOrderName([{ product: { title: 'Product A' } }])
// "Product A"
formatOrderName([
{ product: { title: 'Product A' } },
{ product: { title: 'Product B' } },
])
// "Product A 외 1건"
React component for rendering Payload CMS Lexical rich text. Two variants:
RichTextContent — Base component with maximum flexibility (converters, blocks, nodeMap, disableDefaultConverters)StyledRichTextContent — Headless component with slot-based customization (components prop)import {
RichTextContent,
StyledRichTextContent,
richTextNodeMap,
} from '@01.software/sdk/ui/rich-text'
// Base: full converter control
<RichTextContent
data={content}
className="prose"
internalDocToHref={({ linkNode }) => `/articles/${linkNode.fields.doc?.value?.slug}`}
blocks={{
Iframe: ({ node }) => <iframe src={node.fields.url} />,
Player: ({ node }) => <VideoPlayer url={node.fields.url} />,
}}
/>
// Shared Payload view-map rendering for supported lightweight blocks
<RichTextContent data={content} nodeMap={richTextNodeMap} />
// Headless: component slots (Radix-style)
<StyledRichTextContent
data={content}
components={{
Heading: ({ tag: Tag, children }) => (
<Tag className={Tag === 'h1' ? 'text-4xl font-bold' : 'text-2xl'}>{children}</Tag>
),
Link: ({ href, children, target, rel }) => (
<a href={href} target={target} rel={rel} className="text-blue-600 underline">{children}</a>
),
Upload: ({ src, alt, width, height }) => (
<img src={src} alt={alt} width={width} height={height} className="rounded-lg" />
),
}}
/>
The SDK throws typed errors instead of returning error responses:
import { isNetworkError, isApiError, isValidationError } from '@01.software/sdk'
try {
const { docs } = await client.collections.from('products').find()
} catch (error) {
if (isNetworkError(error)) {
console.error('Network issue:', error.message)
} else if (isApiError(error)) {
console.error('API error:', error.status, error.message)
}
}
Error classes: SDKError, ApiError, NetworkError, ValidationError, ConfigError, TimeoutError
NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY=your_publishable_key
SOFTWARE_PUBLISHABLE_KEY=your_publishable_key
SOFTWARE_SECRET_KEY=sk01_... # Server only — opaque API key from Console
secretKeyshould only be used in server environments. Never expose it to the browser.
secretKeymay be ansk01_...API key or apat01_...personal access token. Server SDK calls must also send the matchingpublishableKey; PAT tenant selection is pinned server-side, and callers must not sendX-Tenant-Id.
API keys created without explicit scopes use the default ['read', 'write']. Console API-key creation can request narrower scopes[]; in the first explicit-scope slice tenant admins may grant only read and write. Higher-authority scopes such as webhook, analytics, and super-admin require platform authority and otherwise return scope_grant_forbidden.
SDK 0.9.0: Server auth now uses opaque bearer tokens (
sk01_...). Generate API keys from the Console.createServerToken,createApiKey, andparseApiKeyare no longer part of the SDK surface.
The complete, up-to-date release history (including 0.35.0+) lives in
CHANGELOG.md, and breaking-change migration steps live inMIGRATION.md. Both ship with the published package. The selected entries below are kept inline for historically significant breaking changes.
swatch, thumbnail, images) to Payload types and ecommerce utility shapes.stock - reservedStock, matching checkout stock checks.Breaking change. Option-value visual fields were collapsed into nested
swatch (type, color, mediaItemId). Legacy public fields are removed
from SDK input/output helpers and from listing/detail contracts.
| Surface | Removed | Replacement |
|---|---|---|
| Upsert option value | swatchColor, thumbnail, images | swatch.type, swatch.color, swatch.mediaItemId |
| Listing groups | optionValueSwatchColor, optionValueThumbnail, optionValueImages | optionValueSwatch |
| Product detail option value | swatchColor, thumbnail, images | swatch |
Migration steps:
swatch: { type: 'color', color: '#111111' }.swatch: { type: 'media', mediaItemId: '<product-pool-image-id>' }.products.images).optionValueSwatch / optionValue.swatch instead.See the v0.34.0 changelog entry above for the field mapping table and migration
steps. There is no compatibility alias window for removed option-value visual
fields; callers must migrate to nested swatch before upgrading.
New error codes propagated via SDKError.code (no breaking change; existing callers ignore unknown codes safely):
| Code | Phase | Trigger |
|---|---|---|
account_suspended | P1 | Suspended session / sk01_ / pat01_ / customer JWT — 401 |
pat_tenant_header_forbidden | P1 | pat01_ request carrying any X-Tenant-Id header — 401 |
tenant_mismatch | P3 | Cross-tenant FK rejection (forms / community / orders) |
server_derived | P3 | Body-driven write into a server-derived state field — 422 |
scope_denied | P5 | pat01_ whose ApiKeys.scopes lacks the operation |
P5 also adds JWT-jti revocation: revokeCustomerJti(jti, ttl) on the server invalidates a token immediately; subsequent SDK calls receive 401 { code: 'token_revoked' }.
COLLECTIONS and INTERNAL_COLLECTIONS are now both exported from @01.software/sdk. Use INTERNAL_COLLECTIONS to detect admin-only slugs in custom tooling.
Field renames — update any code that reads these fields from API responses:
| Collection | Old | New |
|---|---|---|
| Customers | socialId | providerUserId |
| Customers | loginAttempts | loginAttemptCount |
| Customers | resetPasswordExpiry | resetPasswordExpiresAt |
| Orders, Carts | shippingFee | shippingAmount |
| Carts | itemsTotal | subtotalAmount |
| Transactions | paymentId | pgPaymentId |
| Discounts | type | discountType |
| Discounts | value | discountValue |
| Discounts | usageLimit | maxUses |
| Discounts | usageCount | usesCount |
| Discounts | perCustomerLimit | maxUsesPerCustomer |
| ShippingPolicies | baseFee | baseAmount |
| ShippingPolicies | freeShippingThreshold | freeShippingMinAmount |
| Documents | effectiveDate | effectiveAt |
| Documents | expiryDate | expiresAt |
| Articles | readTime | readingMinutes |
| ApiUsage | count | apiCallCount |
| ApiUsage | storageUsed | storageUsedBytes |
| ApiUsage | totalDocuments | documentCount |
Collection renames:
order-products → order-itemsreturn-products → return-itemsexchanges, exchange-productsproduct-option-valuesBoolean field renames (6 collections):
status: 'active' | 'inactive' → isActive: boolean on Forms, ArticleAuthors, CustomerGroups, ShippingPolicies, ProductVariantsFAQs
01.software SDK
The npm package @01.software/sdk receives a total of 488 weekly downloads. As such, @01.software/sdk popularity was classified as not popular.
We found that @01.software/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Miasma Mini Shai-Hulud hits @immobiliarelabs Backstage plugins, targeting GitLab and LDAP auth packages on npm.

Security News
Rolldown paused Rust React Compiler integration after a 5MB binary size increase raised concerns about shipping React-specific code to all Vite users.

Security News
/Research
Mini Shai-Hulud expands into the Go ecosystem after hitting LeoPlatform npm packages and targeting GitHub Actions workflows.