Skip to content
littleworksdocs

Explore the documentation

Meet LittleworksA little backend for what you’re building on Shopify.Littleworks or your own appWhat it takes to give a Shopify feature a backend: with Littleworks, or with a custom app you build and run yourself.Connect to LittleworksChoose the CLI, a plugin, or a remote MCP connection.CLIConnect from your terminal with Shopify approval and no manual tokens.Claude pluginThe Littleworks MCP connection and agent guidance in one package.OpenAI pluginThe Littleworks MCP connection and agent guidance in one package.MCP with OAuthConnect a compatible client directly, without a marketplace plugin.Build with an agentThe operating guide for agents building on Shopify with Littleworks.Write a workA JavaScript handler, a small manifest, and a version you test and then publish.Keep secretsEncrypted credentials, shared across your store’s works.Call external servicesMake public HTTPS API requests from a work using http.fetch().Store dataDocument collections with a small API, scoped automatically to the work that uses them.Call ShopifyCall Shopify’s Admin and Storefront GraphQL APIs with credentials held by Littleworks.Store filesKeep uploads such as artwork and photos in the merchant’s Shopify Files, uploaded by the browser straight to Shopify.Expose an endpointConnect an existing frontend to a work through a small JSON API.Customer access and invitationsRequire a signed-in shopper or a narrow, expiring invitation before a work runs.RecipesUseful things to build for your store, with the backend already taken care of.Product reviewsCollect customer reviews, verify purchases, and publish approved content directly into your Shopify theme.Customer wishlistsGive signed-in customers a persistent list of products they can revisit across devices.Customer quote requestsCollect a customer’s products, quantities, and requirements without turning a request into an order.Back-in-stock alertsCollect confirmed signups on sold-out variants, email them through your own provider when stock returns, and see every signup and send in your admin.Transactional emailSend email from your works through your own provider, never twice for the same thing, and see what was sent and what failed.Build a merchant pageDescribe your records and what merchants do with them; Littleworks renders a native Shopify admin page.Run works on their ownRun a work when something happens in the store, on a schedule, or from a Shopify Flow workflow.Run background jobsQueue runs for later or in the background: fan out to thousands of customers, import in pages, follow up days later, and retry what fails.MCP tool referenceTools to inspect, deploy, run, and organize the backend for a connected store.Usage and allowancesSee what your works use, how much capacity remains, and when allowances reset.Inspect and troubleshootFind the relevant run, understand the failure, and make the next change deliberately.Limits and securityThe current runtime boundaries, data isolation model, and execution allowances.
Shopify admin
Build
Markdown

Customer access and invitations

Require a signed-in shopper or a narrow, expiring invitation before a work runs.

Choose the proof your build needs

Use auth: "customer" for signed-in storefront customers, on a theme or a headless storefront such as Hydrogen, or auth: "token" for an invitation that permits a specific action. Public works accept anonymous requests. Merchant auth is for merchants and connected agents. All customer-facing modes use automatic traffic protection; logging in does not exempt a caller from limits. Customer and invitation responses are never cached at the edge; only public GET works can be.

A signed-in storefront customer

Declare an HTTP trigger with auth: "customer", GET or POST, and the exact storefront origin. The shared Shopify app proxy forwards /apps/littleworks/your.work (the real URL of a published work) and /apps/littleworks/preview/{token}/your.work (the preview URL, which tests the newest version) to Littleworks. One proxy serves every work. deploy_function and get_function return both paths. Shopify can customize the proxy path; verify the installed path before wiring the frontend. Littleworks requires write_app_proxy, separately from optional Shopify data permissions.

Littleworks verifies Shopify’s query signature, timestamp and store, then requires a nonempty customer ID. The work receives context.customer.id as a Shopify customer GID. At the direct /f/... URL, customer identity comes only from an assertion signed by a headless storefront’s server (below); invoke_function cannot supply it. GET input excludes Shopify’s proxy parameters. POST accepts JSON.

Read only the signed-in customer’s records
export default async ({ context, db }) => {
  return db.collection("requests").list({
    where: { customerId: context.customer.id },
  });
};
Call from the signed-in storefront
const response = await fetch("/apps/littleworks/customer.requests", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({}),
});
if (response.status === 401) {
  // Ask the shopper to sign in using this store's customer login flow.
}

This mode supports storefront sessions where Shopify supplies logged_in_customer_id. Test the store’s actual customer-account setup; if Shopify supplies no customer ID, the request fails closed. Headless storefronts use the signed assertion below instead. Customer-account UI extensions need separate identity integration and cannot use this mode to impersonate a shopper. CORS and Origin are not proof of identity.

A signed-in customer on a headless storefront (Hydrogen)

Hydrogen and other headless storefronts have no app proxy, but their server already knows the signed-in customer from Shopify’s Customer Account API. That server signs a short assertion naming the customer and calls the work’s direct URL with it. The same auth: "customer" work serves both paths and receives the same context.customer.id, so one work can back a theme and a Hydrogen storefront. deploy_function, get_function and list_functions return a customer work’s direct URLs as headlessUrl (once published) and headlessPreviewUrl: https://littleworks.app/f/{store}/{name} and https://littleworks.app/f/{store}/preview/{token}/{name}.

  1. Create the key once, in two places. In the Littleworks app in Shopify, open Secrets, add LITTLEWORKS_CUSTOMER_KEY and choose Generate value. Copy the value it shows once into the storefront server’s environment (for Oxygen, the storefront’s environment variables) under the same name. The key must be at least 32 bytes.
  2. Sign on the server for each request. Read the customer ID from context.customerAccount, sign a JWT with HS256, and send it as Authorization: Bearer <assertion> to the work’s headlessUrl (or headlessPreviewUrl while testing).
  3. Use `context.customer.id` in the work, exactly as for the app proxy.
PartRequired value
HeaderExactly {"alg":"HS256","typ":"JWT"}. Other algorithms, none and extra header fields are rejected.
KeyThe UTF-8 bytes of the LITTLEWORKS_CUSTOMER_KEY value, used as the HMAC-SHA256 key.
subThe customer GID from the Customer Account API, such as gid://shopify/Customer/123.
aud"{store}/{work}": the store ID and work name from the URL. An assertion for one work does not open another.
iat, expWhole seconds. exp is at most 300 seconds after iat; use 60 or less. Littleworks allows 30 seconds of clock difference.
buyerIpOptional: the shopper’s IP address (on Oxygen, the oxygen-buyer-ip request header). Littleworks forwards it to the Storefront API as the buyer IP hint and never uses it for identity.
SizeAt most 2 KiB.
Sign and call from the Hydrogen server
// app/lib/littleworks.server.ts: runs only on the storefront server.
const encoder = new TextEncoder();
const base64url = (bytes: Uint8Array) =>
  btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const encode = (value: unknown) => base64url(encoder.encode(JSON.stringify(value)));

/** Calls a customer work as the signed-in customer. workUrl is its headlessUrl. */
export async function callAsCustomer(
  context: { env: { LITTLEWORKS_CUSTOMER_KEY: string }; customerAccount: any },
  request: Request,
  workUrl: string,
  input: unknown,
) {
  // The customer comes from Shopify's Customer Account API session, never from input.
  if (!(await context.customerAccount.isLoggedIn())) return null;
  const { data } = await context.customerAccount.query(`#graphql
    query LittleworksCustomer { customer { id } }`);
  const customerId: string = data.customer.id; // gid://shopify/Customer/...

  // aud is "{store}/{work}", taken from the URL: /f/{store}/{work} or /f/{store}/preview/{token}/{work}.
  const path = new URL(workUrl).pathname.split("/");
  const aud = `${decodeURIComponent(path[2])}/${decodeURIComponent(path[path.length - 1])}`;
  const iat = Math.floor(Date.now() / 1000);
  const buyerIp = request.headers.get("oxygen-buyer-ip");
  const unsigned = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({
    sub: customerId, aud, iat, exp: iat + 60, ...(buyerIp ? { buyerIp } : {}),
  })}`;
  const key = await crypto.subtle.importKey(
    "raw", encoder.encode(context.env.LITTLEWORKS_CUSTOMER_KEY),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const signature = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(unsigned)));

  return fetch(workUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${unsigned}.${base64url(signature)}` },
    body: JSON.stringify(input),
  });
}
A storefront route that uses it
// app/routes/api.points.tsx: the storefront calls its own route; the route calls Littleworks.
import { callAsCustomer } from "~/lib/littleworks.server";

export async function action({ request, context }) {
  const response = await callAsCustomer(context, request, context.env.LITTLEWORKS_POINTS_URL, await request.json());
  if (!response) return Response.json({ error: "Sign in to see your points." }, { status: 401 });
  return new Response(response.body, { status: response.status, headers: { "Content-Type": "application/json" } });
}

Call from the storefront server, server to server. It needs no allowed origins and keeps the assertion away from the browser. If the browser must call Littleworks directly, have a loader hand it a fresh assertion that expires within a minute and list the storefront’s exact origin in the work’s origins: until it expires, that assertion acts as the customer for that work.

To rotate the key, add the new value as LITTLEWORKS_CUSTOMER_KEY_NEXT; Littleworks accepts assertions signed with either. Switch the storefront to the new value and deploy it, then replace LITTLEWORKS_CUSTOMER_KEY with the new value and delete LITTLEWORKS_CUSTOMER_KEY_NEXT. If the key leaks, generate a new one at once and update the storefront.

ResponseMeaning
401 CUSTOMER_AUTH_REQUIREDNo assertion (and no app proxy signature). Sign the customer in, or check that the server sends the Authorization header.
401 CUSTOMER_ASSERTION_NOT_CONFIGUREDThe store has no LITTLEWORKS_CUSTOMER_KEY of at least 32 bytes.
401 INVALID_CUSTOMER_ASSERTIONThe signature, header, sub, aud, times, buyerIp or size is wrong. Check that both places hold the same key and that aud matches the URL.
401 CUSTOMER_ASSERTION_EXPIREDexp has passed. Sign a new assertion for each request, and check the server clock.

Public, token and merchant works ignore customer assertions: a token work still needs its invitation token in the Authorization header, and a public work never receives context.customer. Customer works keep the same traffic protection; Littleworks counts a signed-in customer as one visitor wherever their requests come from.

An invitation for a guest

First deploy the receiving HTTP work with auth: "token". Then use a private callable or merchant HTTP work to issue an invitation after checking eligibility. A public, customer or token work cannot mint invitations, even when invoked through an agent. The issuer and the target belong to the same store. A real run issues invitations for the target’s current version, so the target must be published; a test run issues them for the target’s newest version, which its preview URL runs.

Issue after validating the order and product
const invitation = await auth.issueToken({
  work: "reviews.submit",
  claims: { orderId: verifiedOrderId, productId: verifiedProductId },
  expiresInSeconds: 86400,
  singleUse: true,
});
// Deliver invitation.token only to its intended recipient through an
// authorized integration. Never log it or store it in a public record.

The result contains token and expiresAt. Tokens are opaque bearer credentials: anyone possessing one can perform its permitted action. Only a token hash and encrypted claims are stored by Littleworks. Defaults are one hour and single use; expiry may be 60 seconds to seven days. Claims are limited to 4 KiB and stores may have 1,000 unexpired tokens, including consumed tokens until expiry. Keep claims minimal, using IDs rather than personal details.

Use the verified claims in the receiving work
export default async ({ input, context, db }) => {
  const { orderId, productId } = context.action.claims;
  return db.collection("reviews").create({
    orderId,
    productId,
    rating: input.rating,
    status: "pending",
  });
};

Send the token in Authorization: Bearer <token> to the target’s URL: the real URL for an invitation from a real run, or the preview URL for one from a test run. It is never accepted from an input field or query parameter. For an email invitation, link to the build’s frontend with the token in the URL fragment; the frontend should read it, remove it from the address bar and send it in the Authorization header. Littleworks does not host that frontend. Fragments still need care: avoid exposing them to analytics or other scripts.

Each token binds to the exact store, work, method and version, and is accepted only while that version is the one that runs. Publishing another version invalidates invitations issued for the previous current version, and deploying a new version invalidates test invitations for the previous newest version. Publishing an earlier version again can make its unexpired, unused invitations valid again. Pause the work to stop all new executions. Choose a reusable token only for a build that needs repeated access, with a suitably short expiry.

Input and admission failures do not consume a single-use token. Admission and consumption are atomic, so concurrent requests cannot use it twice. Once execution starts, it stays consumed even if the work fails or returns an error, because earlier effects may have completed. Inspect the run before issuing another invitation. Tokens and customer login do not make external effects transactional.

Automatic protection

Littleworks limits bursts, simultaneous runs, per-work and store execution, stored data, and external requests. See Limits and security. Rejected admissions create no run and spend no run allowance. Littleworks HTTP 429 for a time-based allowance includes Retry-After and a retry time; storage needs space freed instead. Edge flood blocks may return a non-JSON response and appear only in Cloudflare security events. Back off, and do not automatically replay a mutation. Shopify shows traffic notices on affected works and explanations on failed runs.