The Swell Apps SDK is a TypeScript library for building server-side Swell apps. It provides access to the Backend and Storefront APIs, along with helpers for app configuration, customer sessions and store user identity.
Use it in Swell-hosted apps, Cloudflare Workers or Node.js servers. For browser
applications, use swell-js.
npm install @swell/apps-sdk@next swell-js@^5.9.2Supports Node.js 22.22.2+ and Cloudflare Workers, with no Node compatibility flags needed in Workers. Includes ES modules, CommonJS and TypeScript declarations.
Version 2 replaces the 1.x theme API. Existing theme applications should remain on
1.x until migrated to @swell/themes-sdk.
Swell supplies the current store's configuration, credentials and store user identity with each request. Verify this context once, then reuse it to create clients and check store user access. Use the same pattern for Swell-hosted and self-hosted frontends.
import { verifySwellContext } from '@swell/apps-sdk';
const context = await verifySwellContext(request.headers, {
appId: 'my-app', // Your app's configured slug.
});When an app component makes the request, context.surface says where it runs: 'admin', 'checkout' or 'storefront'. Treat 'storefront' and 'checkout' calls like public routes; check context.surface === 'admin' before doing anything that only a merchant may do.
In a server component, pass await headers(). Keep the context and clients on the
server, scoped to the incoming request.
import { SwellBackendAPI } from '@swell/apps-sdk';
// Reuse the context resolved for this request.
const backend = new SwellBackendAPI({ context });
// Fetch products from the Backend API.
const products = await backend.get('/products', { limit: 10 });For an external server, read credentials from your server configuration:
const backend = new SwellBackendAPI({ storeId, secretKey, apiHost });Use the Storefront API for customer-facing data and cart or account operations.
In this example, cookies is your framework's cookie jar. Its reads must include
pending writes and deletions.
import { getStorefrontConfig } from '@swell/apps-sdk';
import { createStorefrontClient } from '@swell/apps-sdk/storefront';
// Build public storefront configuration from the request context.
const config = getStorefrontConfig(context);
const storefront = createStorefrontClient(config, {
cookies: {
get: name => cookies.get(name)?.value,
set: (name, value, options) => { cookies.set(name, value, options); },
},
});
// Use the same methods available in swell-js.
const products = await storefront.products.list({ limit: 10 });External servers can supply storeId, publicKey and swell-js options such as
url, locale or currency directly as the config. Only the /storefront entry
loads the swell-js runtime.
Send the public config from getStorefrontConfig to your browser app through a
loader or endpoint with Cache-Control: private, no-store. Then initialize swell-js:
import swell from 'swell-js';
swell.init(config.storeId, config.publicKey, config);Send only this public config to the browser; the request context contains server credentials.
A store user is someone signed in to the store's Swell dashboard. Use
requireStoreUser to check that a request belongs to one before applying your
application's permission checks:
import { requireStoreUser } from '@swell/apps-sdk';
const storeUser = requireStoreUser(context); // { userId, storeId }, or a 401 SwellError.
const optional = context.storeUser; // null for a visitor; no exception needed.App components are small UI bundles from an app's components/ folder. Swell renders each one in an isolated iframe on the app installation's origin, so app code never runs in the host page. @swell/apps-sdk/components is browser code and is only bundled when you import it.
Components are Preact components. Import only the props type from the SDK:
import type { ComponentProps } from '@swell/apps-sdk/components';
export const config = { description: 'Brand color picker' };
export default function ColorPicker({ value, setValue, readonly }: ComponentProps<string>) {
return <input type="color" value={value} disabled={readonly} onInput={(e) => setValue(e.currentTarget.value)} />;
}| Prop | Description |
|---|---|
value, setValue(value) |
The bound value, when the place provides one (for example a content field). setValue re-renders the component with the new value (in a microtask, before the next event) and sends it to the host; the host sends a value back only when it changes elsewhere or when the host rejects the one you set |
context |
Data of the place: for a content field { record, field } |
params |
Configuration from the place that uses the component |
settings |
The app's public settings |
locale, readonly |
Display locale and read-only state |
setValidity(error) |
Report a validation error, or null when valid |
fetch |
Like fetch, but requests to the app's own origin (app functions, /app-api) carry a platform token when the host provides one, so they receive a verified Swell-Context with surface |
call(name, data) |
Calls a host action and resolves with its result; payment components get createIntent and getIntent |
on(event, handler) |
Handle a host event. Only the first handler registered for an event runs; its return value, or the error it throws, answers the host's emit |
A component becomes the UI of a payment method by declaring the extension it implements in config:
import { useEffect } from 'preact/hooks';
import type { ComponentProps, PaymentContext, PaymentSubmitResult } from '@swell/apps-sdk/components';
export const config = {
extension: { id: 'card' },
targets: [{ id: 'checkout.payment.methods' }],
};
export default function Card({ call, on, context }: ComponentProps<never, PaymentContext>) {
// Register in an effect: the frame runs only the first handler registered for an event, and `on` returns the unsubscribe
useEffect(() => on('submit', async (): Promise<PaymentSubmitResult> => {
const intent = await call<{ id: string }>('createIntent', { amount: context.cart.capture_total });
return { billing: { intent } };
}), [on, call, context]);
return <div>Card form</div>;
}extension: { id }ties the component to an extension of the app'sswell.json. An object is the new contract; a string is the legacyswell.appcontract.targets: [{ id: 'checkout.payment.methods' }]opts the component into hosted checkout's payment methods place.PaymentContextis thecontext: thecart(withaccount_id,currency,capture_total) and the selectedmethod. Pass your own cart type as its generic.submit(dataPaymentSubmitData) andredirect(dataPaymentRedirectData, after a redirect back to checkout) handlers return{ billing }(PaymentSubmitResult), which is merged into the cart's billing.call('createIntent' | 'getIntent', data)creates or reads a payment intent through the Swell vault; the account comes fromcontext.cart.account_id. The cart incontextcomes from the host page, so when the amount matters, compute or verify it on the server increate_intent.- Render visible content before opening a modal, so the customer sees the component in the payment methods list.
Hosts render installed apps' components with createComponents:
import { createComponents } from '@swell/apps-sdk/components';
const components = createComponents({ storeId: 'my-store', publicKey: 'pk_...' });
const badge = components.mount('#badge', {
app: 'my_app',
component: 'ProductBadge',
context: { product },
});
badge.on('change', (value) => { /* … */ });
badge.on('error', (error) => { /* show that the component is unavailable */ });
badge.update({ context: { product: nextProduct } });
await badge.ready;
const result = await badge.emit('submit', data);
badge.unmount();mount returns the handle right away and loads the app's component list in the background. It throws only when the target element does not exist.
| Handle | Description |
|---|---|
ready |
Resolves when the component has mounted in its frame. Rejects when the app or component cannot be loaded, when the frame page does not start within 10 seconds of loading, when the props cannot be sent, or when you unmount first |
on(event, handler) |
Listen to change (new value), validity (error or null) and error (load, start, token and component failures, including the ones that reject ready). Returns a function that removes the listener; other event names are ignored |
update(input) |
Re-render with new value, context, params, readonly or locale. Updates made before the component list loads are kept |
emit(event, data) |
Waits for ready, then resolves with the result of the component's first on handler for the event, or rejects with its error |
focus() |
Moves keyboard focus to the component's first control, for example when your form focuses its first invalid field. During overlay, to the component's modal |
unmount() |
Removes the frame. Before the component list loads, it cancels the mount; ready and pending emit calls reject |
Pass actions to mount to offer host actions that components reach with call(name, data). Payment components also get built-in createIntent and getIntent, served by the Swell vault at vaultUrl (default https://vault.schema.io). Set vaultUrl in createComponents to use another vault.
Props and event data must be structured-cloneable. Props that cannot be cloned fire error, and at mount they also reject ready; event data that cannot be cloned rejects that emit call.
Hosts that have their own session, like the Swell admin, pass getToken: (app) => Promise<{ token, expires }>. The token is refreshed a minute before it expires; if a refresh fails, the component keeps the current token and the host retries. Without getToken the frame gets no token, and props.fetch sends requests without a token header.
The frame is an iframe inside the target element, so it behaves like any element of your page: it takes part in the layout, your popovers, sticky bars and dialogs stack over it, scrolling containers clip it, and Tab and Shift+Tab move into it and on to the neighbouring elements. Tab skips a component whose frame has not started yet, failed to start or reported an error before it rendered. The target's height follows the component's content. Do not move the target in the DOM while the component is mounted: browsers reload a moved iframe, so the component would start again with the current props.
When a component opens a modal (a payment SDK's 3D Secure challenge, a QR code), its frame covers the viewport until the modal closes. The frame enters the browser's top layer (the Popover API), so z-index, transform, opacity and overflow on the target's ancestors cannot confine it; in browsers without the Popover API it is a fixed box with the highest z-index, which a transformed or filtered ancestor still confines. Meanwhile the rest of your page is inert and does not scroll, and keyboard focus moves into the modal.
Component frames load from the app installation's origin, a subdomain of swell.store. If the host page has a Content Security Policy, allow these origins in frame-src, for example frame-src https://*.swell.store.
Hosts that render the components of a place, such as hosted checkout, mount the place instead of one component:
const methods = components.mount('#payment', {
target: 'checkout.payment.methods',
select: (c) => c.app === method.extension_app_id && c.extension?.id === method.id,
context: { cart },
});
methods.on('validity', (error) => { /* … */ });
const results = await methods.emit('submit', { billing });The handle has ready, components (the mounted components), on('validity' | 'error'), update (context, params, readonly and locale; a place has no bound value), emit, focus and unmount. emit waits for ready and resolves with an array, one result per component that handled the event.
The platform returns every component of the store's active installed apps that declares the target; the host decides which of them to mount. select is called once for each of them before any frame is created, with its name, app (the app id), targets and extension (id and type), but not the app settings. It returns true for the components to mount; without select the place mounts all of them. A select that throws fails the place like a failed load: ready rejects, error fires, and no frames are created. select is fixed at mount; remount the place to change the method.
Hosts cannot override the built-in payment actions: createIntent and getIntent win over actions of the same names.
Pass { context } for a frontend request, or explicit credentials for an external
server. Backend calls require an access token or secret key and an absolute HTTP(S)
apiHost. Do not mix input sources. Invalid constructor options throw immediately;
all backend methods return promises and reject on failure.
| Method | Result |
|---|---|
get(path, query?) |
Response data |
post(path, data?), put(path, data?), delete(path, data?) |
Response data |
settings(appId?) |
Installed-app settings; defaults to the configured app ID |
workflows.create(name, params?) |
Created workflow instance |
transaction(ops, options?) |
Operation results in input order |
functions.call(appId, name, data?, options?) |
Function response payload |
Response generics describe expected data without validating it. SwellCollection<T>
is for ordinary paginated lists; aggregations and page: false return other shapes.
Requests stay on the configured host; redirects are refused. Use endpoint paths,
not absolute URLs. Request IDs are forwarded when supplied. GET queries use bracket
notation, omit undefined values, encode Dates as ISO strings, and preserve native
null separately from the string 'null'. Other methods send JSON.
Workflows: supplied parameters must be JSON-safe and at most 128 KiB of serialized
UTF-8 JSON. Omitted parameters are not sent. Invalid or oversized parameters reject
with workflow_params_unserializable or workflow_params_too_large.
Transactions: each operation contains method, url and optional data.
Retries are off by default. Set retry: true or
retry: { limit: 3, base: 100, max: 5000, jitter: true } to enable them. These are the
defaults: limit counts additional attempts, and delays are in milliseconds. Only
transaction_conflict and transaction_throttled retry. Ordinary requests and network
failures are not retried.
Private functions: use the app slug from context.appId and authorize the caller
first. options.method defaults to post; get, put and delete are also supported.
GET data must contain only flat string, number or boolean values. Caller headers are
not forwarded; response status and headers are not returned. Function errors and
non-2xx statuses reject with SwellError.
Private-app calls require platform support for app-slug lookup and fail on deployments
without it. Function execution and req.swell remain managed by the CLI and platform.
Cookie adapters exchange decoded values and own the current state. If your framework reads only incoming cookies, track pending writes and deletions in request-local state. The SDK keeps no cookie cache.
Native cookie names and defaults apply: path /, one week, sameSite: 'lax'.
cookieOptions replaces these defaults; {} delegates attributes to the adapter.
Per-write attributes take precedence.
Omit cookies.set for read-only access. Attempted writes then throw, including session
rotation during GET requests. A supplied writer may throw or skip a write; after a skip,
its reader must still report the actual state. Writer return values are ignored.
Persist cookies in writable route handlers or actions.
For API response caching, replace storefront.request before first use. The SDK
leaves response caching to your application.
verifySwellContext(headers, { env?, appId?, storeId?, vaultUrl? }) returns the request
context or throws if verification fails. Set appId and, for a single-store app,
storeId from trusted configuration to reject contexts intended for another app or
store. Without these options, it verifies the source but does not restrict the
destination. vaultUrl provides an optional vault endpoint override.
Configuration is read from process.env, or from env when supplied. Workers without
Node compatibility should pass their bindings as env.
| Variable | Default | Purpose |
|---|---|---|
SWELL_VERIFY_HEADERS |
enabled | Set exactly "false" to skip signature verification during local development. |
SWELL_HEADERS_JWKS_URL |
https://keys.swell.store/jwks.json |
Override the verification-key endpoint. |
For local development against a local Swell instance, put SWELL_VERIFY_HEADERS=false
in .dev.vars. Remove it or set it to "true" to restore verification. Token structure
and claim validation still apply, but the context is no longer authenticated: anyone
who can reach the frontend directly can supply forged context, including a store user.
Use this bypass only for local development.
For server integrations, construct SwellBackendAPI with explicit credentials and
createStorefrontClient with explicit public configuration. These clients do not
require an HTTP request or a store user.
requireStoreUser(context) returns { userId, storeId } or throws SwellError with
status 401 and code store_user_required. For optional access, read
context.storeUser, which is null for visitors.
A store user is anyone signed in to the store's dashboard, including partners and Swell support, who may not appear in the store's own user list. Swell's proxy handles their authentication and write-origin checks; your application decides what each store user may do.
Import SwellError from the root package. Use status and optional code/body for
error handling; message is for people and may change.
- Structured backend errors retain their body; string errors have no body.
- HTTP-200 non-GET validation failures use status 400 and the
errorsfield map asbody. Successful GET responses containingerrorsare returned as data. - Function invocation failures retain the response payload, or the invocation envelope
when the payload is null or absent, in
body. - Header verification uses 401 /
invalid_swell_contextfor absent, malformed, expired or rejected tokens, and 503 /swell_jwks_unavailablefor key-service failures. - Backend/storefront network errors remain native. Local configuration errors may be
ordinary
Errorinstances; not every failure is aSwellError.
npm ci
npm run verifyverify builds the SDK, runs unit tests and typechecks, then checks the packed package,
browser/Worker boundaries and workerd execution.