App SDK
Use @usequeek/app-sdk for install handoffs, embedded session tokens, webhook verification and a typed Merchant API client, with a working server example.
@usequeek/app-sdk is the server library for a Queek app. It verifies every signed request
Queek sends, stores installations encrypted, mints the short-lived tokens your app uses to call
the Merchant API, and gives you a typed client for those calls.
npm install @usequeek/app-sdk pgRequires Node.js 22.14 or later. pg backs the production installation store. The Hono
wrappers need the optional hono peer package, and the React helpers need react, only if you
use them. The SDK is pre-1.0, so pin an exact version.
A working server
This is a complete server module. It answers the install, uninstall and settings handoffs,
verifies and dispatches an orders/paid webhook, and calls the Merchant API from inside the
handler. Each route is a plain Request in, Response out function, so it mounts in a Next.js
route handler, an Express adapter or any Web-standard runtime.
import {
createAppTokenProvider,
createInstallationClient,
handleInstallRequest,
handleWebhookRequest,
loadAppCredential,
QueekApiError,
SqliteInstallationStore,
} from "@usequeek/app-sdk";
const store = new SqliteInstallationStore({
path: "./data/installations.db",
storeKey: process.env.APP_ENCRYPTION_KEY!,
});
// Reads APP_KEY_ID and APP_PRIVATE_KEY. The slug is the one in queek.app.toml.
const tokens = createAppTokenProvider({
credential: loadAppCredential({ appSlug: "my-app" }),
store,
});
export function POST(request: Request): Promise<Response> {
const { pathname } = new URL(request.url);
if (pathname.endsWith("/webhooks")) {
return handleWebhookRequest(request, {
store,
handlers: {
"orders/paid": async (envelope, context) => {
const client = createInstallationClient({
installationId: context.installation.installationId,
apiBase: context.installation.apiBase,
tokens,
});
try {
const orders = await client.requestWithRetry<{ data: unknown[] }>("GET", "/orders", {
query: { limit: 20 },
});
console.log(envelope.data, orders.data.length);
} catch (error) {
if (error instanceof QueekApiError) {
console.error(error.status, error.code, error.requestId);
}
throw error;
}
},
},
});
}
// Routes on the trailing path segment: install, uninstall or settings.
return handleInstallRequest(request, {
appSecret: process.env.QUEEK_APP_SECRET!,
store,
});
}For an order event, the handler receives the full envelope and the order is envelope.data.
There is no envelope.data.order wrapper. See
webhook topics and payloads. The webhook handler dedupes on
webhook-id, so each event runs your handler at most once per installation.
Import the right entry
| Import | Use |
|---|---|
@usequeek/app-sdk | Server handlers, Merchant client, stores and signature verifiers. Includes Node modules, so it is not browser-bundlable. |
@usequeek/app-sdk/server | Server-side verification of embedded session tokens. |
@usequeek/app-sdk/hono | Optional Hono wrappers for install, webhook and proxy handlers. |
@usequeek/app-sdk/react | QueekProvider and useQueek() for the embedded page. |
@usequeek/app-sdk/browser | Browser-only bridge, theme and authenticated fetch helpers, without React. |
Never import the main entry from browser code: a bundler will try to resolve its Node-only
files and fail. In the browser use /react or /browser.
Environment
| Variable | Value |
|---|---|
QUEEK_APP_SECRET | The whsec_ signing secret that verifies every handoff. Shown once on first deploy. |
APP_ENCRYPTION_KEY | 32 random bytes, base64 or hex. Encrypts installation data at rest. |
APP_KEY_ID | Id of the public key registered under the app's Keys in the Developer page. |
APP_PRIVATE_KEY | The matching private key, as PEM or base64 of the PEM. Never log it or ship it to a client. |
DATABASE_URL | Postgres connection string. Required in production; the SDK refuses the development SQLite file there. |
Embedded session tokens
Queek sends one kind of signed session token to an embedded page. The dashboard puts it in the
first-load queek_token parameter and can send the same token again over the bridge when the
page needs a refresh. The browser helper strips the parameter and exchanges the token with your
server. Verify both first-load and refresh tokens with the same verifier from
@usequeek/app-sdk/server, and keep the signing secret on the server.
The page tells the dashboard it is ready with a ready message; the dashboard waits 15 seconds for it and
shows an error card if it never arrives. installThemeListener and QueekProvider send it for you from
0.7.2 on, and versions up to 0.7.1 did not send it from installThemeListener. The messages, origins and
timings are in Embedded apps and the dashboard bridge.
Merchant API authentication is a separate, server-side flow. Your app's asymmetric credential
mints and caches short-lived installation tokens, and the Merchant client sends one as
X-Client-Key. Never send app credentials or Merchant API tokens to the browser.
Errors and retries
Every failed Merchant API call throws a QueekApiError with status, code, requestId,
field, errors and, on a 429, retryAfterMs. Branch on code, and log requestId.
- Use
client.requestWithRetryfor calls that may be retried. It retries on429and network errors with backoff, honoursRetry-After, and reuses oneIdempotency-Keyacross attempts, so a write cannot execute twice. - Use
client.requestwhen you want no retry. - On a stale token (
401, or403withapi_key_revoked,api_key_expiredorinvalid_client_key) the SDK re-mints once and retries once. 403 app_token_revokedmeans the app was disabled: the SDK stops minting until the app is re-enabled and the process restarts.409 app_installation_pendingmeans the installation is not active yet. Retry later.
The full code list is in Merchant API errors.
Typed Merchant API calls
Run queek app codegen to write types for the live Merchant spec to types/merchant.ts, then
pass them to the client: createInstallationClient<AppPaths>(…). Re-run it when the Merchant
API changes, and review the diff before you commit. The default types bundled with the SDK do
not update on their own.
Data and uninstall
The app database holds installations only: references, cached tokens, webhook secrets and
settings. Business data lives in Queek. Treat the app/uninstalled handoff as the signal to
delete that installation and anything of yours keyed to it; the default handler already deletes
the installation row.
The handoff body is { type, data }, where data carries installation and store. When the
merchant deletes the whole store, data also carries reason: "store_deleted". A merchant
uninstalling the app on its own sends no reason, so treat an absent reason as an ordinary
uninstall. A store is deleted only after its grace period ends, and the handoff arrives once per
installation, signed like every other handoff. Both cases mean the same for your data: delete
everything you hold for that installation, including any copy of the store's customer data. To
learn that one customer's data was requested or must be erased, subscribe to the
customer privacy topics.
See the @usequeek/app-sdk README for
Express, Fastify and Hono examples, the storefront proxy verifier, and resync after data loss.
App manifest
Configure a Queek app with queek.app.toml, scoped access, webhooks, extensions and dashboard UI.
Embedded apps and the dashboard bridge
How an app's page runs inside the Queek dashboard. The postMessage contract, exact origins, the handshake and its 5 and 15 second limits, how the frame is sized, and where to put dialogs.