Building a Shopify app
A Shopify app is a Remix app that runs embedded in the Shopify admin (iframe +
App Bridge), built on @shopify/shopify-app-remix (shopifyApp() +
authenticate) with Polaris UI. Source lives in integrations/shopify-app/.
OAuth, session storage, webhook HMAC, and API versioning come from the package;
you supply routes and your own product's logic. Keep it a thin client over your
product's SDK / public REST API — all secrets and business logic stay server-side
(.server.ts modules). Config lives in shopify.app.toml (client_id, URLs,
scopes, declarative webhook subscriptions). Read this before the first file; the
command playbook is scripts/shopify-app/README.md in the scripts repo.
The trap that wastes a day: webhooks must 2xx even when your work fails
await authenticate.webhook(request) verifies the HMAC signature and returns
{ shop, topic, payload }. After that check passes, always return 200 —
catch every downstream error, log it, respond OK. Shopify retries non-2xx
deliveries with backoff, so an outage in your product's API turns into a webhook
storm: duplicate events, a flagged app, and (with enough failures) subscriptions
deleted by Shopify.
Rule: the webhook route authenticates, does its work best-effort, and 200s.
Never let your own downstream outage propagate back to Shopify.
The other traps that each cost a round-trip
- GDPR webhooks are mandatory for App Store distribution.
customers/data_request, customers/redact, shop/redact must be subscribed
in shopify.app.toml AND implemented — as acknowledged no-ops when you store
no buyer data. shop/redact and app/uninstalled must delete that shop's
settings and OAuth sessions. Missing endpoints block submission automatically.
shopify.app.toml edits do nothing until shopify app deploy. Webhook
topics, scopes, and URLs are declarative config — the local file is not live.
Editing the toml without deploying is the #1 "my webhook never fires" cause.
Adding scopes also forces existing merchants through re-authorization.
- The embedded frame needs the boundary exports. In the embedded root route
(
app.tsx), export ErrorBoundary = boundary.error(useRouteError()) and
headers = boundary.headers(...) from @shopify/shopify-app-remix/server, or
OAuth redirects thrown inside the iframe lose their headers and the app hangs
on a blank frame. The frame also needs <AppProvider isEmbeddedApp apiKey={apiKey}> (App Bridge), with the public API key passed from the loader.
- Review installs with zero of your state. A Shopify reviewer installs the
app on their own test store. Every screen must render a sane "not connected"
state, and auth to your product must be self-service in-app (e.g. paste the
team API key in a Settings page, validated on save). An app that assumes an
existing session elsewhere gets rejected. A public privacy-policy URL is
required for the listing.
Build path
shopify app dev (Shopify CLI) tunnels to a public URL, syncs
application_url + redirect URLs into the toml, and installs on your dev
store. shopify app config link binds the directory to the Partner app's
client_id.
- Server config:
shopifyApp({ apiKey, apiSecretKey, apiVersion, scopes, appUrl, authPathPrefix: "/auth", sessionStorage, distribution: AppDistribution.AppStore }). Match the toml's redirect_urls to the
authPathPrefix callback paths.
- Sessions: SQLite session storage is fine for dev
(
@shopify/shopify-app-session-storage-sqlite); production needs DB-backed
sessionStorage — file SQLite breaks on multi-instance hosts, and lost
sessions mean OAuth loops/reinstalls.
- UI: Polaris components inside App Bridge. (Polaris React is deprecated upstream
in favor of Polaris web components — pin what works, revisit when the official
app template moves.)
- Webhook handlers read per-shop settings (your product's API key, saved via the
embedded Settings page) and call your product's public REST API server-side.
Submission — Shopify App Store
Submittable: portal-review
No submit-for-review API. shopify app deploy / shopify app release push an
app version (toml, webhooks, extensions) — they do not host the Remix app
and do not file the review. Review is portal-only: Partner / Dev Dashboard →
Apps → [your app] → Distribution → Shopify App Store (also the App Store
review page). Partner account is free; public HTTPS hosting is required;
distribution method is one-way. Paid apps must use Shopify App Pricing / Billing
API (revenue share after the first $1M USD/year — verify current split). Docs:
shopify.dev/docs/apps/launch.
partners.shopify.com → Apps → create the app → Client ID/secret into .env
client_id in the toml. Bind with shopify app config link.
- Host the production app on public HTTPS (hostname must not contain "Shopify").
Set
SHOPIFY_APP_URL, application_url + redirect_urls, subscribe the three
compliance webhooks, shopify app deploy.
- Choose public / App Store distribution. Complete configuration: URLs,
GDPR webhooks, 1200×1200 JPEG/PNG icon (no text — corners auto-round),
emergency contact email + phone. If you touch buyer data, file protected
customer data access before review (can't apply while under review).
- Listing: name ≤30 chars (brand-led, unique), 100-char intro + 500-char details,
feature list (≤80 chars each), 3–6 screenshots at 1600×900 (16:9) (at least
one of the embedded UI, no browser chrome/PII/pricing), optional 1600×900
feature image or 2–3 min promo video, demo-store URL, pricing, support +
privacy-policy URL. Include a short reviewer screencast + test credentials.
- Pass the built-in automated checks (required) and optional AI self-review,
then Submit for review. Reviewers install on their own test store with none
of your state. SLA commonly weeks
(verify).
Silent-rejection gotchas: off-platform billing (hard reject); URLs containing
"Shopify"; OAuth that doesn't fire immediately on install; embedded app that
relies on third-party cookies; invalid SSL; GDPR webhook 404/500; missing
privacy-policy URL / test credentials / screencast; scopes the app never calls;
screens that break with no API key; toml never deployed. Payout business
verification (verify).
Parity checklist (prove in a real dev store before submitting)
install → OAuth lands in the embedded admin · save your product's API key in
Settings · trigger the subscribed event (create/update the object) → your API
receives the call · webhook route 200s even with your API down · uninstall →
that shop's settings + sessions wiped.
Related skills
zapier-integration — another thin-connector-over-your-REST-API build with a
live-usage submission gate.
canva-app / figma-plugin — the same embedded-iframe, thin-client shape on
design tools; different SDK + review surfaces.
connector-directory-submission — the cross-marketplace submission router.
1---2name: shopify-app3description: Build, run, and submit a Shopify app (a Remix embedded app under integrations/shopify-app/ built on @shopify/shopify-app-remix + App Bridge) and get it listed on the Shopify App Store. Use when creating a new Shopify app, wiring OAuth + an embedded admin UI, subscribing to shop webhooks (products/orders), deploying shopify.app.toml changes, or figuring out why App Store review bounces. Covers the whole path plus the traps that each cost a build/submit round-trip: webhook routes must return 2xx even when your downstream work fails (Shopify retries non-2xx into a storm), the three GDPR privacy webhooks are mandatory for distribution even as no-ops, shopify.app.toml edits do nothing until `shopify app deploy`, the embedded frame needs the boundary error/headers exports or OAuth redirects die in the iframe, and review installs your app on a store with zero of your state. Sibling of the other integration skills (canva-app, figma-plugin, browser-extension, zapier-integration, connector-directory-submission). Trigge4---56# Building a Shopify app78A Shopify app is a **Remix app that runs embedded in the Shopify admin** (iframe +9App Bridge), built on **`@shopify/shopify-app-remix`** (`shopifyApp()` +10`authenticate`) with Polaris UI. Source lives in `integrations/shopify-app/`.11OAuth, session storage, webhook HMAC, and API versioning come from the package;12you supply routes and your own product's logic. Keep it a thin client over your13product's SDK / public REST API — all secrets and business logic stay server-side14(`.server.ts` modules). Config lives in **`shopify.app.toml`** (client_id, URLs,15scopes, declarative webhook subscriptions). Read this before the first file; the16command playbook is `scripts/shopify-app/README.md` in the scripts repo.1718## The trap that wastes a day: webhooks must 2xx even when your work fails1920`await authenticate.webhook(request)` verifies the HMAC signature and returns21`{ shop, topic, payload }`. After that check passes, **always return 200** —22catch every downstream error, log it, respond OK. Shopify retries non-2xx23deliveries with backoff, so an outage in your product's API turns into a webhook24storm: duplicate events, a flagged app, and (with enough failures) subscriptions25deleted by Shopify.2627**Rule:** the webhook route authenticates, does its work best-effort, and 200s.28Never let your own downstream outage propagate back to Shopify.2930## The other traps that each cost a round-trip31321. **GDPR webhooks are mandatory for App Store distribution.**33 `customers/data_request`, `customers/redact`, `shop/redact` must be subscribed34 in `shopify.app.toml` AND implemented — as acknowledged no-ops when you store35 no buyer data. `shop/redact` and `app/uninstalled` must delete that shop's36 settings and OAuth sessions. Missing endpoints block submission automatically.372. **`shopify.app.toml` edits do nothing until `shopify app deploy`.** Webhook38 topics, scopes, and URLs are declarative config — the local file is not live.39 Editing the toml without deploying is the #1 "my webhook never fires" cause.40 Adding scopes also forces existing merchants through re-authorization.413. **The embedded frame needs the boundary exports.** In the embedded root route42 (`app.tsx`), export `ErrorBoundary = boundary.error(useRouteError())` and43 `headers = boundary.headers(...)` from `@shopify/shopify-app-remix/server`, or44 OAuth redirects thrown inside the iframe lose their headers and the app hangs45 on a blank frame. The frame also needs `<AppProvider isEmbeddedApp46 apiKey={apiKey}>` (App Bridge), with the public API key passed from the loader.474. **Review installs with zero of your state.** A Shopify reviewer installs the48 app on their own test store. Every screen must render a sane "not connected"49 state, and auth to your product must be self-service in-app (e.g. paste the50 team API key in a Settings page, validated on save). An app that assumes an51 existing session elsewhere gets rejected. A public privacy-policy URL is52 required for the listing.5354## Build path5556- `shopify app dev` (Shopify CLI) tunnels to a public URL, syncs57 `application_url` + redirect URLs into the toml, and installs on your dev58 store. `shopify app config link` binds the directory to the Partner app's59 `client_id`.60- Server config: `shopifyApp({ apiKey, apiSecretKey, apiVersion, scopes, appUrl,61 authPathPrefix: "/auth", sessionStorage, distribution:62 AppDistribution.AppStore })`. Match the toml's `redirect_urls` to the63 `authPathPrefix` callback paths.64- Sessions: SQLite session storage is fine for dev65 (`@shopify/shopify-app-session-storage-sqlite`); production needs DB-backed66 `sessionStorage` — file SQLite breaks on multi-instance hosts, and lost67 sessions mean OAuth loops/reinstalls.68- UI: Polaris components inside App Bridge. (Polaris React is deprecated upstream69 in favor of Polaris web components — pin what works, revisit when the official70 app template moves.)71- Webhook handlers read per-shop settings (your product's API key, saved via the72 embedded Settings page) and call your product's public REST API server-side.7374## Submission — Shopify App Store7576**Submittable: portal-review**7778No submit-for-review API. `shopify app deploy` / `shopify app release` push an79**app version** (toml, webhooks, extensions) — they do **not** host the Remix app80and do **not** file the review. Review is portal-only: Partner / Dev Dashboard →81**Apps → [your app] → Distribution → Shopify App Store** (also the App Store82review page). Partner account is free; **public HTTPS hosting** is required;83distribution method is one-way. Paid apps must use Shopify App Pricing / Billing84API (revenue share after the first $1M USD/year — verify current split). Docs:85`shopify.dev/docs/apps/launch`.86871. `partners.shopify.com` → Apps → create the app → Client ID/secret into `.env`88 + `client_id` in the toml. Bind with `shopify app config link`.892. Host the production app on public HTTPS (hostname must not contain "Shopify").90 Set `SHOPIFY_APP_URL`, `application_url` + `redirect_urls`, subscribe the three91 **compliance webhooks**, `shopify app deploy`.923. Choose **public / App Store** distribution. Complete configuration: URLs,93 GDPR webhooks, **1200×1200** JPEG/PNG icon (no text — corners auto-round),94 emergency contact email + phone. If you touch buyer data, file **protected95 customer data** access *before* review (can't apply while under review).964. Listing: name ≤30 chars (brand-led, unique), 100-char intro + 500-char details,97 feature list (≤80 chars each), **3–6 screenshots at 1600×900 (16:9)** (at least98 one of the embedded UI, no browser chrome/PII/pricing), optional 1600×90099 feature image or 2–3 min promo video, demo-store URL, pricing, support +100 **privacy-policy URL**. Include a short reviewer screencast + test credentials.1015. Pass the built-in **automated checks** (required) and optional AI self-review,102 then **Submit for review**. Reviewers install on their own test store with none103 of your state. SLA commonly weeks `(verify)`.104105**Silent-rejection gotchas:** off-platform billing (hard reject); URLs containing106"Shopify"; OAuth that doesn't fire immediately on install; embedded app that107relies on third-party cookies; invalid SSL; GDPR webhook 404/500; missing108privacy-policy URL / test credentials / screencast; scopes the app never calls;109screens that break with no API key; toml never deployed. Payout business110verification `(verify)`.111112## Parity checklist (prove in a real dev store before submitting)113114install → OAuth lands in the embedded admin · save your product's API key in115Settings · trigger the subscribed event (create/update the object) → your API116receives the call · webhook route 200s even with your API down · uninstall →117that shop's settings + sessions wiped.118119## Related skills120121- `zapier-integration` — another thin-connector-over-your-REST-API build with a122 live-usage submission gate.123- `canva-app` / `figma-plugin` — the same embedded-iframe, thin-client shape on124 design tools; different SDK + review surfaces.125- `connector-directory-submission` — the cross-marketplace submission router.