Skip to content

Shopify (Headless Storefront)

De Shop-addon koppelt elke Beam-site aan een eigen Shopify-winkel (multi-tenant). Beam blijft de frontend/pagebuilder; Shopify is de commerce-backend (catalogus, orders, checkout, betalingen). Zie ADR-013.

  • Frontend: Beam rendert producten via de Shopify Storefront API (headless).
  • Losse verkoop + checkout: Shopify hosted checkout via cart.checkoutUrlShopify Payments (iDEAL, kaart, Shop Pay).
  • SEPA-abonnementen (later, buiten deze fase): Firmhouse + Mollie, náást Shopify Payments.

Elke site heeft één rij in shopify_connections (shop_domain + storefront_token). De Storefront-token is publiek-veilig (alleen producten lezen, carts beheren — geen admin-toegang) en mag daarom client-side gebruikt worden (cart-island).

ActorPad naar Shopify
Dashboard (editor)api.shopify.* → API Worker /shopify/* (auth) → Storefront API
Public site (SSR)getShopifyProduct() in de frontmatter → Storefront API (server-side)
Public site (cart)Browser-fetch met de publieke token → Storefront API (cartCreate/cartLinesAdd)
CheckoutRedirect naar cart.checkoutUrl → Shopify hosted checkout
  • SSRF: normalizeShopDomain() (@beam/shared/shopify) staat alleen *.myshopify.com of een geldig hostname toe; de Worker hervalideert vóór elke fetch.
  • RLS + secret-scheiding: shopify_connections (shop_domain + storefront_token, beide publiek-veilig) — read = site members, write = owner/admin. De secrets leven in een aparte tabel shopify_connection_secrets (webhook_secret, admin_api_token, storefront_private_token, customer_account_client_secret) met SELECT én write beperkt tot owner/admin (mig 101/102). Reden: de member-leesbare hoofdtabel mag geen secrets bevatten — anders kan een lager gerechtigd lid (editor) ze via PostgREST direct uitlezen (de app-masking is geen vervanging voor RLS). De public site leest nooit direct; de public_resolve_shopify_connection SECURITY DEFINER RPC geeft alleen domein + token voor een actieve site (mig 095).
  • Admin-token (optioneel, server-side only): voor automatische webhook-aanmaak kan een Shopify Admin API access token (custom app, scope read_products) worden opgeslagen in de secrets-tabel. Alleen de Worker gebruikt het (nooit client-side). De bijbehorende API secret key wordt als webhook_secret bewaard (Shopify ondertekent app-webhooks daarmee). Webhooks worden alleen in productie aangemaakt (een dev-URL zou een onbereikbare webhook registreren). Zonder Admin-token blijft de handmatige flow werken.
EndpointDoel
GET /shopify/connectionKoppelstatus (public token + gemaskeerde secret-hints)
POST /shopify/connectCredentials valideren + opslaan; maakt bij Admin-token de webhooks aan
POST /shopify/webhooks/register(Her)maak de product-webhooks met het opgeslagen Admin-token (alleen prod)
POST /shopify/secrets/clearWis één opgeslagen secret (key-rotatie) — veld-whitelist, owner/admin
DELETE /shopify/connectionOntkoppelen (verwijdert ook de secrets-rij)
GET /shopify/products/search?q=Productzoeker (picker)
GET /shopify/products?source=&collection=&types=&tags=&handles=&count=Grid-producten per databron (Alle/Collectie/Type/Tag/Handmatig); count=-1 = alle (max 250)
GET /shopify/filter-optionsCollecties + product-types + tags voor de grid-sidebar-pickers (KV-gecached)
GET /shopify/products/:handleEén product (editor-preview)
LaagBestand
Shared types + queries + normalizerspackages/shared/src/shopify.ts
Worker proxyapps/api/src/routes/shopify.ts
api-clientapps/dashboard/src/lib/api-client.ts (api.shopify.*)
Hooksapps/dashboard/src/lib/hooks/use-shopify-product.ts
Block (editor/preview)apps/dashboard/src/components/blocks/{editor,public}/shopify-product-block.tsx
Grid-block (editor/preview)apps/dashboard/src/components/blocks/{editor,public}/shopify-product-grid-block.tsx
Grid-block (Astro SSR)apps/public-site/src/components/blocks/ShopifyProductGridBlock.astro
Block (Astro SSR)apps/public-site/src/components/blocks/ShopifyProductBlock.astro
Productpaginaapps/public-site/src/pages/products/[handle].astro — variant-selector (maat/kleur) + media-galerij met video (autoplay muted/loop, previewImage als preloader, pauzeert buiten beeld; native <video> op Shopify’s mp4-bronnen, geen Bunny)
Cart-island (sticky mini-cart)apps/public-site/src/components/ShopifyCart.astro — full-width slide-up paneel met regels (afbeelding/titel/prijs), subtotaal + checkout-CTA (Beam-button-standaard)
Koppel-UIapps/dashboard/src/components/shopify-connection-panel.tsx (Addons-pagina)

Shopify-productafbeeldingen gebruiken Shopify’s eigen CDN-transformaties (shopifyImageUrl(url, width) / shopifyImageSrcSet(url) in @beam/shared/shopify, via de ?width=-query), niet Beam’s Cloudflare Image Resizing. Reden: de public-site heeft CF_IMAGE_RESIZING=1 en zou de externe cdn.shopify.com-origin niet zonder extra zone-config transformeren → gebroken afbeeldingen. De blur-placeholder is een kleine Shopify-variant (?width=24) die de CF-tiny-fallback in BlurImage short-circuit.

  • Edge-cache: de productdetailpagina (products/[handle].astro) zet s-maxage=300, stale-while-revalidate=600 + Vary: Host — de CF-edge serveert de HTML zonder RPC/Storefront-call.
  • KV cache-until-changed (24u safety-net TTL): connectie, product, product- lijst en recommendations worden KV-first geladen (write-on-miss). Gedeeld door de editor (via de API-Worker read-through, apps/api/src/lib/shopify-cache.ts) én de public-site (apps/public-site/src/lib/kv-cache.ts) — zelfde BEAM_CACHE + key-format. Beschermt tegen Storefront cost-throttling en maakt het plaatsen van blocks in de editor snel/betrouwbaar.
  • Event-invalidatie via Shopify webhooks: de API-Worker heeft een pre-auth, HMAC-geverifieerde endpoint POST /shopify/webhook. Bij een products/*- of collections/*-webhook wordt de KV-cache voor die site geleegd (invalidateShopifyCacheForSite) → wijzigingen in Shopify werken direct door. Site-herkenning via X-Shopify-Shop-Domain; verificatie met het per-winkel webhook_secret (mig 097). Ook connect/disconnect leegt de cache. Zonder geconfigureerde webhook valt het terug op de 24u-TTL.
  • Automatische webhook-aanmaak (mig 100): met een opgeslagen Admin API-token maakt de Worker de PRODUCTS_CREATE/UPDATE/DELETE + COLLECTIONS_UPDATE-webhooks aan via webhookSubscriptionCreate (Admin GraphQL, apps/api/src/lib/shopify-admin.tsensureShopifyWebhooks). Idempotent: Shopify weigert een dubbele topic+uri met een “already taken”-userError, die als “bestaat al” telt. De callback-URL is de Worker-eigen …/shopify/webhook. Gebeurt bij connect én via POST /shopify/webhooks/register (de “opnieuw aanmaken”-knop). Signing key = de app-secret in webhook_secret.
  • Connectie 1× per request via Astro.locals (getShopifyConnectionForRequest) — N Shopify-blocks op één pagina doen niet N identieke RPC’s.
  • Gedeelde fetch: storefrontFetch() in @beam/shared/shopify (timeout + errors-guard) wordt hergebruikt door Worker, SSR én de cart-island.
  • Dashboard-bundle: de Shopify-blocks zijn lazy()-geladen (aparte chunks), dus geen dead-weight in de editor-bundle voor sites zonder de addon.

Geen Worker-secrets nodig: de per-site token leeft in shopify_connections. De klant haalt de Storefront public token uit Shopify admin → Apps → Headless/Storefront API.