---
description: Rules for a DZBuild app (OAuth install with PKCE, install tokens, REST API at api.dzbuild.app, signed webhooks)
alwaysApply: true
---

This repository is a DZBuild app: merchants install it on their DZBuild store (https://dzbuild.com) and
the app reads and changes that store through https://api.dzbuild.app/v1. DZBuild runs none of this code.
Developer kit 2026.09.30.

Read before changing anything: the docs MCP (`claude mcp add dzbuild-docs -- npx -y @dzbuild/docs-mcp`), or any page as Markdown at its URL plus
`.md` (index https://dzbuild.dev/llms.txt), the rules in https://dzbuild.dev/skills/dzbuild-apps/SKILL.md
and the OpenAPI description https://dzbuild.dev/openapi/dzbuild-apps-v1.json. Do not infer DZBuild
behaviour from general OAuth or webhook knowledge.

- Secrets (`DZBUILD_CLIENT_SECRET`, `DZBUILD_SIGNING_SECRET`, every install token) come from the
  environment or the secret store. Never write them into code, tests, fixtures, logs or chat.
- The token answer carries `stores[]` with one `access_token` per store; store each by `store_id`.
  Tokens have no refresh and no expiry; a `401` means the merchant uninstalled the app.
- Every `POST`, `PATCH` and `DELETE` carries an `Idempotency-Key` (1 to 64 characters of
  `A-Z a-z 0-9 _ - : .`), one key per operation, reused on every retry. Back off on `429` using
  `error.retry_after`. Stop on `403` with an `app_*` code and show the message.
- Webhook handlers read the raw body, verify `X-DZ-Signature` (`t=<ts>,v1=<hex>`, HMAC-SHA256 with the
  signing secret over `t + "." + raw body`) in constant time, reject `|now - t| > 300`, dedupe on the
  envelope `id` and answer `200` at once. The webhook URL must be on a domain you own; `*.workers.dev`
  is refused, so poll `GET /v1/orders?since=` once a minute until then.
- `dz_launch` is an HS256 JWT signed with the signing secret: check the signature, `alg`, `iss`
  (`dzbuild`), `aud` (your client id), `exp` and a single-use `jti`. The launch URL opened without
  `dz_launch` starts the install flow.
- Redirect URIs are compared character for character with the ones registered in the developer console.
- On `app.uninstalled`, drop the store's token and delete its data within 30 days.
