
Security News
Risky Biz Podcast: AI Agents Are Raising the Stakes for Software Supply Chain Security
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.
@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, ./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'
// 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/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/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,
where: { status: { equals: 'published' } },
})
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', address1: 'Seoul', address2: 'Apt 101' },
orderItems: [...],
totalAmount: 10000,
pgPaymentId: 'pay_123', // optional (omit for free orders)
discountCode: 'WELCOME10', // optional
})
// SSR prefetch (server)
await serverQuery.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
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 same shaped product detail as detail(), but allows
the saved draft/unpublished record addressed by the preview token.
The recommended way to fetch a single product is the shaped helper:
import { createClient } from '@01.software/sdk'
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
})
const product = await client.commerce.product.detail({
slug: 'every-peach-tee',
})
if (!product) {
return notFound() // returned null — product missing, unpublished, or not in this tenant
}
// product: { product, variants, options, brand, categories, tags, images, videos, listing }
detail() returns ProductDetail | null. A null result covers every "no result" reason: not_found, not_published, tenant_mismatch, feature_disabled. Render the same "not available" UI for all four. To recover the exact reason for triage, 404 maps to null rather than a thrown error — inspect client.lastRequestId and match against backend logs.
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.
availableValuesByOptionSlug / availableValuesByOptionId include
availableStock, isUnlimited, and availableForSale per value so option UIs
can render stock state without recalculating from variants.
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. Complete selections use variant=<variantId>; partial
selections use opt.<optionId>=<valueId>. Older
opt.<optionSlug>=<valueSlug> URLs still parse during Stage 1, but slugs are
compatibility metadata rather than canonical identity.
import {
createProductSelectionCodec,
resolveProductSelection,
} from '@01.software/sdk'
const codec = createProductSelectionCodec(product)
const normalizedSelection = codec.parse('?opt.option-color=color-black')
const selection = resolveProductSelection(product, normalizedSelection)
const selectionQuery = codec.stringify(normalizedSelection)
// selectionQuery === 'opt.option-color=color-black' for partial selections
// selectionQuery === 'variant=variant-black-large' once a complete variant is selected
// selection.selectedVariant, selection.price, selection.stock, selection.media
Use IDs from detail.options[].id and detail.options[].values[].id when
building selection state. Slugs remain useful for display and old inbound URLs,
but new outbound URLs should use the codec output.
For listing cards, pass the listing group returned by
buildProductListingGroupsByOption() or the listing-groups endpoint into
buildProductHref(product, group, { detail }). The detail object lets the SDK
emit canonical variant=<variantId> or opt.<optionId>=<valueId> params. 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().
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.
buildProductListingCard(item, options?) turns a single
commerce.product.listingGroups() response item into a render-ready
ProductListingCard. The card carries product-level hero media
(product.thumbnail -> first product.images -> null), an aggregated
price range across all option-value groups, 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.
import {
buildProductListingCard,
type ProductListingCard,
} from '@01.software/sdk'
const cards: ProductListingCard[] = response.docs.map((item) =>
buildProductListingCard(item, { basePath: '/shop' }),
)
Each swatch carries a hint-only option-value href
(?opt.<optionId>=<valueId>); the detail page resolves it through
resolveProductSelection(detail, { search }).
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. The configured Payload default applies when unset.
const product = await client.collections.from('products').findById(id, {
depth: 2, // populates product.category, product.category.parent, etc.
})
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 client.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 public SDK schema, products.variants, products.options, customers.orders, customers.addresses, posts.comments, article-authors.articles, orders.{items,transactions,fulfillments,returns}, and similar reverse-relations 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 public SDK collection types.
// Canonical product detail query — variants/options are join fields on Products
await client.collections.from('products').find({
where: { slug: { equals } },
joins: {
variants: { limit: 50, sort: 'sortIndex' },
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({
joins: false,
})
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:
composedReadStatusPublished on products restricts unauthenticated reads to status: 'published'). A draft or unpublished document silently disappears from results even when its slug matches. 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: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
})
const product = await client.commerce.product.detail({ slug: params.slug })
if (!product) return notFound()
// ...
}
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',
where: { status: { equals: 'published' } },
depth: 2,
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
const product = await client.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: 1,
})
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,
status: '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.*. Only checkout and listMine are also on Client.
// Orders
await server.commerce.orders.create(params)
await server.commerce.orders.update({ orderNumber, status })
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 & transactions
await server.commerce.orders.createFulfillment({ orderNumber, carrier, trackingNumber, items })
await server.commerce.orders.bulkImportFulfillments({ items: [{ orderNumber, carrier?, trackingNumber? }] })
await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMethod, receiptUrl })
// Provider-verified payment confirmation
// Existing Toss server-confirm callers may keep using updateTransaction with paymentKey + amount.
// PortOne/Stripe/etc. webhook handlers should verify with the provider first, then call:
await server.commerce.orders.confirmPayment({
orderNumber,
pgProvider: 'portone',
pgPaymentId,
amount,
providerStatus: 'PAID',
providerEventId,
})
// Returns
await server.commerce.orders.createReturn({ orderNumber, returnItems, refundAmount, reason? })
await server.commerce.orders.updateReturn({ returnId, status })
await server.commerce.orders.returnWithRefund({ orderNumber, returnItems, refundAmount, 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.
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', swatchColor: '#111111' },
{ value: 'White', slug: 'white', swatchColor: '#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)
}
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 }],
})
Available on both Client and ServerClient via commerce.cart.*.
// 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
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 })
import {
handleWebhook,
createCustomerAuthWebhookHandler,
createTypedWebhookHandler,
} from '@01.software/sdk'
// Basic handler
export async function POST(request: Request) {
return handleWebhook(request, async (event) => {
console.log(event.collection, event.operation, event.data)
})
}
// With HMAC-SHA256 signature verification (recommended)
export async function POST(request: Request) {
return handleWebhook(request, handler, {
secret: process.env.WEBHOOK_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.
// Type-safe handler
const handler = createTypedWebhookHandler('orders', async (event) => {
// event.data is typed as Order
console.log(event.data.orderNumber)
})
// Customer auth helper
const customerAuthHandler = createCustomerAuthWebhookHandler({
passwordReset: async ({ email, resetPasswordToken }) => {
await sendPasswordResetEmail(email, resetPasswordToken)
},
})
Source of truth: packages/sdk/src/core/collection/const.ts (COLLECTIONS: 73).
| Category | Collections |
|---|---|
| Tenant | tenants, tenant-metadata, tenant-logos |
| Products | products, product-variants, product-options, product-option-values, product-categories, product-tags, product-collections, brands, brand-logos |
| Orders | orders, order-items, returns, return-items, fulfillments, fulfillment-items, transactions |
| Customers | customers, customer-profiles, customer-addresses |
| Carts | carts, cart-items |
| 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, canvas-nodes, canvas-edges |
| Videos | videos, video-categories, video-tags |
| Live Streams | live-streams |
| Media | images |
| Forms | forms, form-submissions |
| Community | posts, comments, reactions, reaction-types, bookmarks, post-categories, customer-profile-lists |
| Events | event-calendars, events, event-categories, event-occurrences, event-tags |
Server-only collections: customer-groups, reports, and community-bans
are available from createServerClient().collections for segmentation and
moderation workflows, but 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.
swatchColor, thumbnail, images) to Payload types and ecommerce utility shapes.stock - reservedStock, matching checkout stock checks.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 830 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.

Security News
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.

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