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.checkoutUrl→ Shopify Payments (iDEAL, kaart, Shop Pay). - SEPA-abonnementen (later, buiten deze fase): Firmhouse + Mollie, náást Shopify Payments.
Koppeling per site
Section titled “Koppeling per site”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).
| Actor | Pad 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) |
| Checkout | Redirect naar cart.checkoutUrl → Shopify hosted checkout |
Beveiliging
Section titled “Beveiliging”- SSRF:
normalizeShopDomain()(@beam/shared/shopify) staat alleen*.myshopify.comof 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 tabelshopify_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; depublic_resolve_shopify_connectionSECURITY 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 alswebhook_secretbewaard (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.
API Worker endpoints (/shopify)
Section titled “API Worker endpoints (/shopify)”| Endpoint | Doel |
|---|---|
GET /shopify/connection | Koppelstatus (public token + gemaskeerde secret-hints) |
POST /shopify/connect | Credentials 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/clear | Wis één opgeslagen secret (key-rotatie) — veld-whitelist, owner/admin |
DELETE /shopify/connection | Ontkoppelen (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-options | Collecties + product-types + tags voor de grid-sidebar-pickers (KV-gecached) |
GET /shopify/products/:handle | Eén product (editor-preview) |
Componenten
Section titled “Componenten”| Laag | Bestand |
|---|---|
| Shared types + queries + normalizers | packages/shared/src/shopify.ts |
| Worker proxy | apps/api/src/routes/shopify.ts |
| api-client | apps/dashboard/src/lib/api-client.ts (api.shopify.*) |
| Hooks | apps/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 |
| Productpagina | apps/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-UI | apps/dashboard/src/components/shopify-connection-panel.tsx (Addons-pagina) |
Afbeeldingen
Section titled “Afbeeldingen”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.
Performance & caching
Section titled “Performance & caching”- Edge-cache: de productdetailpagina (
products/[handle].astro) zets-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 eenproducts/*- ofcollections/*-webhook wordt de KV-cache voor die site geleegd (invalidateShopifyCacheForSite) → wijzigingen in Shopify werken direct door. Site-herkenning viaX-Shopify-Shop-Domain; verificatie met het per-winkelwebhook_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 viawebhookSubscriptionCreate(Admin GraphQL,apps/api/src/lib/shopify-admin.ts→ensureShopifyWebhooks). 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 bijconnectén viaPOST /shopify/webhooks/register(de “opnieuw aanmaken”-knop). Signing key = de app-secret inwebhook_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.
Secrets
Section titled “Secrets”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.