Everything here is grounded in the source:
src/lib/shopify/ in hiveku_builder, the webhook route at src/app/api/webhooks/shopify/route.ts, and the cron registry in hiveku_cron_worker/src/registry.ts. Env vars and routes are quoted verbatim.Design principles at a glance
Bring your own Shopify app
Each account registers its own Shopify app. The
client_id + client_secret live per-account in oauth_apps (provider='shopify'), not in a shared platform env var.Tokens encrypted at rest
Admin and Storefront access tokens are AES-256-GCM enveloped with
SHOPIFY_TOKEN_ENCRYPTION_KEY. They never appear in API responses or logs.Shopify stays the source of truth
Products, inventory, payments, taxes, and fulfillment stay in Shopify and render live. Hiveku persists only the few rows it needs to operate.
Self-healing by cron
A set of reliability sweeps recover missed orders, dead webhooks, scope drift, and stale endpoint URLs instead of failing silently.
The bring-your-own-app OAuth model
Hiveku does not ship a single platform-wide Shopify app. Every account registers its own Shopify app (via the Shopify Partner dashboard) and pastes its credentials into Hiveku. Those credentials live per-account in theoauth_apps table with provider='shopify', and every store connected on that account reuses the same app.
The connect dialog at
/dashboard/commerce/settings/shopify can register the app inline. From there, OAuth follows Shopify’s standard authorize flow:
- The authorize URL is
https://{shop}.myshopify.com/admin/oauth/authorize, built bybuildAuthorizeUrlwith the app’sclient_id, the comma-separated scopes, the redirect URI, and astatevalue. - Hiveku requests an offline token —
buildAuthorizeUrlintentionally omitsgrant_options[]=per-user, so the resulting Admin API token is long-lived (“until the app is uninstalled”), which is what server-to-server Admin API calls need. - The Shopify API version is pinned to
2025-04(SHOPIFY_API_VERSIONinsrc/lib/shopify/auth.ts).
Two distinct HMACs
src/lib/shopify/auth.ts deliberately keeps two separate HMAC verifiers, because mixing them up would silently pass signature verification on the wrong message shape:
Both compare with
timingSafeEqual, and both are keyed by the OAuth app’s client_secret (for shop-scoped webhooks created via OAuth, the webhook secret equals client_secret).
Token encryption at rest
Access tokens are wrapped with a Shopify-scoped envelope so a leak of the Shopify key can’t compromise other integrations (for example voice SIP passwords, which use their own key).1
Encryption
src/lib/shopify/crypto.ts builds encryptShopifyToken / decryptShopifyToken via the generic envelope helper (makeEnvelopeCrypto). The stored format is an AES-256-GCM envelope (v1:iv:ct:tag). The Admin token lands in shopify_connections.admin_access_token_enc, the Storefront token in storefront_access_token_enc, and (when present) a per-shop webhook signing secret in webhook_secret_enc.2
Key configuration
The primary key is
SHOPIFY_TOKEN_ENCRYPTION_KEY (a 32-byte base64 value, e.g. openssl rand -base64 32). Lose it and every connection’s tokens are unrecoverable.3
Rotation
Set
SHOPIFY_TOKEN_ENCRYPTION_KEY_PREVIOUS to the old key and SHOPIFY_TOKEN_ENCRYPTION_KEY to the new one. Decryption falls back to the previous key automatically, and each token re-wraps with the new key on its next write. Remove _PREVIOUS once everything has been re-saved.Data model: what persists vs. what stays in Shopify
The integration is intentionally light on stored state. Shopify remains the source of truth; Hiveku persists only the rows it needs to route webhooks, run automation, and give you a CRM view of buyers.Rows Hiveku persists
Orders also fan into the CRM:
crm_contacts gains rollup columns (shopify_total_spent_cents, shopify_order_count, shopify_first_order_at/_id, shopify_last_order_at) and a crm_activities note per order.
What stays in Shopify (never mirrored)
Products, collections, inventory, the full order record, payments, taxes, shipping, fulfillment, and customer payment methods (PCI) all stay in Shopify and are queried live at render time. Hiveku deliberately does not copy them into its database — that avoids the classic sync-drift problem and keeps your Shopify admin authoritative.Webhook handling
All webhooks from every connected store land on a single endpoint:POST /api/webhooks/shopify.
1
Read the raw body
The handler reads
request.text() — HMAC requires byte-verbatim bytes, and a JSON.parse → JSON.stringify round-trip does not preserve them.2
Look up the connection
It finds the
shopify_connections row by the X-Shopify-Shop-Domain header. An unknown shop returns 404 (Shopify treats 4xx as no-retry — correct, we don’t recognize it).3
Verify the signature
verifyWebhookHmac checks the base64 X-Shopify-Hmac-SHA256 header against the connection’s OAuth-app client_secret. A mismatch returns 401.4
Deduplicate
A 10-minute in-memory set keyed by
connection.id + X-Shopify-Webhook-Id drops duplicates (Shopify retries a delivery up to 19× over 48h). Dedup runs after verification so an attacker can’t seed the cache to suppress real events.5
Route the topic and respond 200
Once verified, the handler routes by
X-Shopify-Topic and returns 200. Handler failures are logged but never 5xx — a 5xx just triggers Shopify retries that won’t help a broken handler.Topic routing
On every accepted delivery, the handler best-effort stamps
last_received_at on the matching shopify_webhook_subscriptions row so the health sweeps can spot a topic that has gone quiet.Order ingestion and the CRM bridge
Order ingestion is a single shared path so the live webhook and the backfill cron produce byte-identical rows and rollups.ingestShopifyOrder (src/lib/shopify/order-ingest.ts) is called by both the orders/* webhook handler and the shopify-order-backfill cron. Both hand it a REST-shaped order payload — which is exactly what the webhook body and Admin REST orders.json return. It:
- Upserts a
shopify_orders_logrow keyed by(shopify_connection_id, order_id). - Calls
linkOrderToCRMto unify the buyer into the CRM.
Under the hood: what linkOrderToCRM does
Under the hood: what linkOrderToCRM does
src/lib/shopify/order-to-crm.ts runs five steps. The rollup recompute (step 2) runs inside a single Prisma transaction so the first-order flag forwarded to the trigger reflects the customer’s state before the increment; the contact upsert, activity, backlink, and trigger fan-out each run in their own error-isolated block:- Contact upsert —
upsertContactByEmailkeyed by email,source: 'shopify'. Skipped for guest orders with no email. - Rollups —
shopify_total_spent_cents,shopify_order_count, and first/last order timestamps + ids. Incremented onorders/create; recomputed from the order log onorders/updatedso refunds/adjustments stay correct. - Activity — a
crm_activitiesnote row, deduped byexternal_id=shopify:order:<orderId>so re-deliveries don’t duplicate the timeline entry. - Backlink — sets
shopify_orders_log.crm_contact_idon the ingested row. - Triggers — fires
shopify.order.created/.updatedworkflow triggers. These fire even for guest orders without a contact, so ops-style automations (“alert me on every order”) still run.
orders/updated, which drives the idempotent recompute path — so recovering a missed order never re-fires first-order automations.
GDPR compliance webhooks
Shopify’s three mandatory privacy topics are logged and then fulfilled asynchronously. Logging (never dropping) an inbound request is what keeps the app App-Store-compliant.
The sweep marks
fulfilled_at when done and raises an urgent alert for anything left unfulfilled past 30 days (the Shopify App Store deadline).
Reliability crons
The Shopify integration is backstopped by five sweeps registered inhiveku_cron_worker/src/registry.ts. They emit deduped agent_ops_inbox_items suggestions rather than failing silently.
These crons live in the separate
hiveku_cron_worker service, which fires HTTPS calls at the builder’s /api/cron/* routes. Each call is authenticated with an Authorization: Bearer ${CRON_SECRET} header (the cron worker’s default auth mode), so CRON_SECRET must match on both the worker and the builder.Requested scopes
DEFAULT_SHOPIFY_SCOPES in src/lib/shopify/auth.ts is the single source of truth for the Admin API scopes Hiveku requests at connect time. The OAuth start route joins them comma-separated for the authorize URL, and shopify-scope-drift compares each store’s granted scope against this configured set. The scopes configured on your Shopify app must match this list.
Required environment variables
Set these on the builder web service (unless noted).SHOPIFY_PARTNER_API_TOKEN + SHOPIFY_PARTNER_ORG_ID are only needed if you provision Shopify development stores from Hiveku. Everyday connect/webhook/order flows do not use them.What’s next
Connecting Shopify
The OAuth flow, account-default vs. project-override, and what gets stored.
Storefront scaffolding
What the AI agent generates, and the env vars wired in at deploy time.
Subscriptions
Subscription contracts, billing-attempt webhooks, and customer self-service.
Overview
The architectural split: what Shopify owns vs. what Hiveku owns.