arky-sdk
v0.15.0
Published
Official TypeScript SDK for Arky - website backend for custom frontends
Maintainers
Readme
arky-sdk
Official TypeScript SDK for Arky, the website backend and Admin client for custom frontends.
Installation
npm install arky-sdkStorefront quick start
The current browser contract is [email protected]. Pin that exact version during the coordinated
prelaunch cutover so the Server, App, and storefront route/header contracts move together:
npm install --save-exact [email protected]Copy the Store publishable key from Developer and initialize one client:
import { initialize } from "arky-sdk/storefront";
export const arky = initialize(import.meta.env.PUBLIC_ARKY_PUBLISHABLE_KEY);initialize is synchronous. It makes no request and creates no visitor. Production requests use https://api.arky.io by default.
For local development or an explicit initial context:
export const arky = initialize("arky_pk_...", {
apiUrl: "http://localhost:8000",
locale: "it",
market: "ita",
});The SDK accepts only an arky_pk_... publishable key. A personal arky_api_... token or an arky_vst_... visitor session is rejected at initialization. Publishable keys identify a Store; they grant no Admin access and are safe to include in browser code.
Read content and submit forms
Anonymous CMS and catalog reads do not create a visitor:
const page = await arky.cms.entry.get({
collection_id: "pages",
key: "homepage",
});
const titleBlock = page.blocks.find((block) => block.key === "title");
const title = arky.utils.getBlockTextValue(titleBlock, arky.getLocale());Stateful operations identify the visitor lazily. Concurrent first operations share one identify request:
await arky.cms.form.submitByKey({
key: "contact",
values: {
email: "[email protected]",
message: "Hello from the storefront",
},
});The browser persists only the arky_vst_... visitor-session token. Storage is isolated by API endpoint and a fingerprint of the publishable key.
Products, services, and checkout
const { items: products } = await arky.eshop.product.list({ limit: 20 });
const product = await arky.eshop.product.get({ id: products[0].id });
await arky.eshop.cart.addProduct(product, product.variants[0], 2);
await arky.eshop.cart.quote();
const order = await arky.eshop.cart.checkout({
payment_method_key: "cash",
});Scheduled services use the same cart:
const { items: services } = await arky.eshop.service.list({ limit: 20 });
await arky.eshop.service.initialize();
await arky.eshop.service.select(services[0]);
arky.eshop.service.findFirstAvailable();
const state = arky.eshop.service.state.get();
if (state.slots[0]) {
arky.eshop.service.selectTimeSlot(state.slots[0]);
arky.eshop.service.nextStep();
await arky.eshop.service.addToCart();
}Nano Stores expose reactive module state:
const unsubscribe = arky.eshop.cart.snapshot.subscribe((snapshot) => {
console.log(snapshot.item_count, snapshot.cart?.id);
});
await arky.eshop.cart.load();
unsubscribe();Locale and market context
Locale and market are independent. Neither is inferred from the other, browser language, IP address, or geolocation:
arky.setContext({ locale: "bs" });
arky.setContext({ market: "bih" });Use an isolated scoped client for SSR, static generation, or parallel contexts:
const italian = arky.withContext({ locale: "it", market: "ita" });
const page = await italian.cms.entry.get({
collection_id: "pages",
key: "homepage",
});Changing the scoped client does not mutate the original client. A market change while the cart contains items throws CART_MARKET_LOCKED; the SDK never silently clears or reprices the cart.
Embedded card checkout
Store setup is fetched lazily and deduplicated:
const setup = await arky.store.load();
console.log(setup.languages.default, setup.markets.default);Payment configuration belongs to Arky. A card checkout returns a short-lived embedded Stripe action. The SDK mounts that exact Checkout Session inside the merchant page; it never redirects the ordinary purchase to a Stripe-hosted Checkout page and it never exposes secret credentials:
import { mountCheckoutAction } from "arky-sdk";
const result = await arky.eshop.cart.checkout({
payment_method_key: "credit_card",
return_url: window.location.href,
});
const mounted = await mountCheckoutAction(
result.payment_action,
"#payment",
{ onComplete: () => arky.eshop.order.get({ id: result.order_id }) },
);
// Call mounted?.destroy() when the checkout view is disposed.Embedded Checkout completion and a browser return are navigation signals only. Authoritative Arky state, advanced by a signed Stripe event or an exact provider read, settles the payment.
SSR and static generation
Anonymous reads work without browser storage. Stateful SSR requires an explicit request-local adapter so a server module cannot retain one visitor across requests:
const arky = initialize(process.env.ARKY_PUBLISHABLE_KEY!, {
locale: requestLocale,
market: requestMarket,
sessionStorage: {
getItem: (key) => requestSession.get(key) ?? null,
setItem: (key, value) => requestSession.set(key, value),
removeItem: (key) => requestSession.delete(key),
},
});Create one client per request. withContext also creates an isolated visitor session; when used during SSR it reuses the request-local adapter under a separate scoped storage key. The SDK does not ship framework-specific cookie adapters.
Low-level storefront client
The module facade exposes its low-level client as arky.client:
await arky.client.eshop.product.find({ limit: 20 });
const cart = await arky.client.eshop.cart.current();
await arky.client.eshop.cart.get({ id: cart.id, token: cart.token });
await arky.client.cms.entry.find({
collection_id: "pages",
key: "homepage",
limit: 1,
});Low-level requests use Store-ID-free /v1/storefront routes and send connection context as headers:
X-Arky-Publishable-Key: arky_pk_...
X-Arky-Locale: it
X-Arky-Market: ita
Authorization: Bearer arky_vst_...For cart recovery, cart.get({ id, token }) sends the recovery credential as
X-Arky-Cart-Token. It is never placed in the request URL or body, and the corresponding response
is private and non-cacheable.
Locale and market headers are omitted when no explicit context is set, allowing the server to use Store defaults.
Configuration
initialize(publishableKey: string, {
apiUrl?: string,
locale?: string,
market?: string,
sessionStorage?: StorefrontSessionStorage,
});One storefront client always represents one publishable key and one Store. To connect to another Store, initialize a second explicit client with its publishable key.
Releasing
SDK packages are released only by tagging the current protected master commit with the exact
v<package.json version> tag. The Publish SDK workflow reruns npm test and publishes through
npm trusted publishing with provenance; configure that workflow as the package's trusted publisher
instead of storing a long-lived npm token.
Admin client
Private operator integrations use the separate Admin client. Personal API tokens must never be exposed in browser code:
import { createAdmin } from "arky-sdk/admin";
const admin = createAdmin({
baseUrl: "https://api.arky.io",
storeId: "internal-store-id",
apiToken: process.env.ARKY_PERSONAL_API_TOKEN,
});Store connection management is available through the Admin surface:
const store = await admin.store.regeneratePublishableKey({
store_id: "internal-store-id",
});
await admin.store.update({
id: store.id,
default_market_id: "market-id",
});TypeScript
import {
initialize,
type ArkyStore,
type StorefrontDto,
type StorefrontSetup,
} from "arky-sdk/storefront";
import type { Block, Cart, Order, Price, Product, Service } from "arky-sdk";
type StorefrontProduct = StorefrontDto<Product>;
type StorefrontCart = StorefrontDto<Cart>;Storefront request types intentionally contain no Store routing ID. Admin request types remain Store-explicit.
Verification
Run the complete SDK package contract with one command:
npm testIt builds the distributable package and runs every SDK contract case. App alone owns cross-repository
Server compatibility against the exact immutable test Server image digest. Each storefront owns a
hermetic repo-local build/preview Playwright smoke through its own npm test; storefronts never pull
or run the shared test Server image.
Adding an endpoint
When adding SDK methods:
- Mirror server response DTOs in
src/types/index.tsor the relevant API module. - Keep Admin inputs Store-explicit, but omit
store_idfrom every storefront input, URL, and body. - Use
/v1/storefront/...keyless routes and let the shared client attach publishable-key, locale, market, and visitor headers. - Mark customer mutations as stateful so they call the deduplicated visitor-session lifecycle.
- Add explicit response generics to every HTTP call and re-export consumer-facing types.
