Install Apex on a headless Nuxt storefront
Add Apex to a Nuxt 3 Shopify storefront, configure OneTrust consent, handle SPA navigation, and preserve cart attribution.
This guide installs Apex on a Nuxt 3 storefront backed by Shopify. Add the loader once through your central Nuxt plugin, then configure the SPA and consent presets in Apex.
Install the loader
Replace <shopId> with the shop ID shown in Apex. Choose one loader variant; do not install both.
Direct load (recommended)
Use the direct loader when consent is configured in the Apex dashboard. The SDK then loads before consent, and Apex's consent integration controls storage and tracking.
Create plugins/apex.ts — a universal plugin, not .client.ts. During server-side rendering, useHead writes the script tag into the served HTML, so the browser discovers the SDK with the initial document instead of after the client bundle boots. (A nuxt.config.ts app.head.script entry achieves the same; use the plugin form when the shop ID comes from runtime config.)
export default defineNuxtPlugin(() => {
useHead({
script: [
{
key: "apex-loader",
src: "https://events.drip-apex.com/s/<shopId>.js",
async: true,
},
],
});
});The async loader does not block rendering, and because the tag is server-rendered it starts downloading with the initial document. It is still best-effort for the first paint: an async script can finish after the browser begins painting. If the shop requires the strongest pre-paint guarantee, use the synchronous loader pattern generated by Apex instead.
CMP-gated load (OneTrust)
If your storefront requires OneTrust to activate the Apex tag itself, emit the loader as an inert Targeting-cookie script (same universal-plugin file):
export default defineNuxtPlugin(() => {
useHead({
script: [
{
key: "apex-loader",
src: "https://events.drip-apex.com/s/<shopId>.js",
type: "text/plain",
class: "optanon-category-C0004",
},
],
});
});OneTrust changes this tag into an executable script after the visitor grants the C0004 category. This stricter gate means the SDK cannot run before consent, so the pre-paint anti-flicker path cannot protect the first paint. When flicker matters, prefer direct loading plus Apex's own consent gating.
Apply the Nuxt SPA preset
In the Apex dashboard, open the shop's Installation settings and set two controls:
- Apply timing (
spa.applyTiming): select After hydration. - DOM changes (
spa.domChangeTrigger): enable it.
After hydration waits for the Nuxt application to hydrate before mutations are applied — the SDK recognizes a Nuxt page through window.__NUXT__ and the #__nuxt root and holds mutations until hydration completes. DOM changes re-evaluates eligible mutations after Nuxt swaps or updates the view; enable it explicitly, because it is what keeps mutations applied across client-side updates on Nuxt. These two controls are the complete SPA setup for Nuxt.
Configure OneTrust consent
In the Apex dashboard, choose the OneTrust consent integration. Its default category is C0004, the standard OneTrust Targeting Cookies group. Keep that value when Apex belongs in the same category as your testing and analytics tools, or replace it with the category ID your consent policy assigns to Apex.
The SDK reads the category list from window.OnetrustActiveGroups. It starts consent-controlled storage and tracking only when the configured group is present. Test both a denied visit and a newly granted visit after changing the category.
If you selected the CMP-gated loader above, OneTrust controls when the SDK code loads as well as what the in-dashboard integration permits. With the recommended direct loader, the SDK is available for pre-paint work while the integration still gates consent-controlled behavior.
Handle client-side navigation
Nuxt's default history navigation works with Apex; install the plugin once rather than once per page. On client-side route changes, the SDK observes the URL and re-evaluates after the new view is rendered. With domChangeTrigger enabled, later component and DOM updates also cause eligible mutations to be re-applied.
See SPA and router modes for router overrides, manual activation, and debounce settings.
Preserve revenue attribution
Shopify's cartAttributesUpdate mutation replaces the full attributes array. In a Nuxt composable, read the cart's current attributes and merge the Apex stamp before every update. Re-sync after cartCreate and later cart mutations because an assignment can arrive after the first cart write.
export function useApexCartAttribution(storefrontClient) {
async function syncStandardAttribution(cartGid) {
const { cart } = await storefrontClient.query(CART_ATTRIBUTES_QUERY, {
variables: { cartId: cartGid },
});
const attributes = window.drip?.mergeCartAttributes?.(cart?.attributes)
?? cart?.attributes
?? [];
const result = await storefrontClient.mutate(CART_ATTRIBUTES_UPDATE, {
variables: { cartId: cartGid, attributes },
});
return result;
}
return { syncStandardAttribution };
}Call syncStandardAttribution(cart.id) after cartCreate succeeds and after each Storefront API cart mutation that may follow a new assignment.
Charge-affecting experiments must use the signed flow. For price, shipping, offer, or any other test that changes what the customer is charged, write every attribute returned by window.drip.getSignedCartAttributes(cartGid), then call confirmCartAttributesWritten(cartGid) only after cartAttributesUpdate succeeds with no userErrors. Never run an unsigned mergeCartAttributes() re-sync after a signed write on the same cart: it replaces the signed authorization, and commerce authorization fails closed at checkout.
getSignedCartAttributes returns only the Apex entries. cartAttributesUpdate replaces
the cart's full attributes array, so merge the signed entries with the cart's other
attributes before writing — never write the signed list alone, or you erase gift notes
and other apps' attributes. An empty signed result is fail-closed: skip the write
entirely and try again on the next cart mutation.
export function useApexSignedCartAttribution(storefrontClient) {
async function syncSignedAttribution(cartGid) {
const signed = (await window.drip?.getSignedCartAttributes?.(cartGid)) ?? [];
if (signed.length === 0) {
// Fail-closed: the SDK is not loaded yet (e.g. consent-gated loader) or
// authorization is not ready. Do not write, and do not substitute
// unsigned attributes on this cart.
return null;
}
const { cart } = await storefrontClient.query(CART_ATTRIBUTES_QUERY, {
variables: { cartId: cartGid },
});
const signedKeys = new Set(signed.map((entry) => entry.key));
const preserved = (cart?.attributes ?? []).filter(
(attribute) => !signedKeys.has(attribute.key),
);
const result = await storefrontClient.mutate(CART_ATTRIBUTES_UPDATE, {
variables: { cartId: cartGid, attributes: [...preserved, ...signed] },
});
const userErrors = result?.cartAttributesUpdate?.userErrors;
if (!Array.isArray(userErrors)) {
throw new Error("Shopify did not return cart attribute write status");
}
if (userErrors.length === 0) {
const acknowledged = window.drip.confirmCartAttributesWritten(cartGid);
if (!acknowledged) {
// The SDK rejected the acknowledgement (e.g. the staged treatment went
// stale while the write was in flight). Authorization stays pending —
// re-run this sync on the next cart mutation.
return null;
}
}
return result;
}
return { syncSignedAttribution };
}Cart attributes have no compare-and-set on the Storefront API, so a concurrent attribute write between the read and the update can be overwritten. Route all cart mutations in your app through one serialized cart store (queue writes; never fire cartAttributesUpdate concurrently), and re-run the sync after every later cart mutation — the same rule as the standard flow above.
Keep the signed and unsigned helpers on separate code paths. Orders can then be attributed server-side through Shopify order webhooks.
Verify the installation
- Publish an active experiment that targets a page in the Nuxt storefront.
- Open a fresh browser session. If OneTrust is enabled, exercise the consent state you intend to test.
- Confirm that the visit receives an experiment assignment and that the assigned variation appears.
- Navigate to another Nuxt route without a full reload and confirm another pageview arrives.
- In Apex, open Installation for the shop and run the verification check. Then open the experiment's results dashboard and confirm the visit and pageview are present.