Hydrogen quickstart
Wishlists in a Hydrogen or any headless storefront — public key in the browser, private key in the loader, your own UI.
This page wires wishlist state into a Hydrogen storefront: reads and toggles from the browser with a public key, customer login handled server-side.
Envie is headless-native: the API returns identity (productId, variantId, handle), you render
from your own product data. No iframe, no hosted page, no markup you didn't write.
1. Get a public key and declare your origin
In the Shopify admin, open Envie → Developers:
- Copy the public key (
env_pk_…). It is browser-safe by design — anonymous scope only. - Add your storefront's origin (for example
https://shop.example.com) to Headless origins. CORS is enforced against this list; until the origin is declared, browser calls are blocked.
2. Install the SDK
npm i @getenvie/client3. Create the client in the browser
// app/lib/envie.client.ts
import { createEnvie } from "@getenvie/client";
export const envie = createEnvie({
mode: "public",
shop: "your-store.myshopify.com",
publicKey: "env_pk_your_store_xxxxxxxx",
});The client manages the shopper's anonymous id for you (generated once, kept in localStorage),
retries idempotent reads, and honors Retry-After on rate limits.
4. Read and toggle
// app/components/SaveButton.tsx
import { useEffect, useState } from "react";
import { envie } from "~/lib/envie.client";
export function SaveButton({ productId, handle }: { productId: string; handle: string }) {
const [saved, setSaved] = useState(false);
useEffect(() => {
return envie.subscribe((state) => {
setSaved(state.items.some((item) => item.productId === productId));
});
}, [productId]);
return (
<button
type="button"
aria-pressed={saved}
onClick={() => envie.wishlist.toggle({ productId, handle })}
>
{saved ? "Saved" : "Save for later"}
</button>
);
}subscribe gives you a reactive snapshot (count, items, config) shared across every
component — one fetch, N buttons in sync.
Prefer ready-made elements?
@getenvie/components works headless too, and the wiring is automatic: import both packages and
createEnvie connects itself to the components' shared store.
// Anywhere in your client bundle — order doesn't matter:
import "@getenvie/components"; // registers <envie-button>, <envie-badge>, <envie-list>, <envie-share>
import { envie } from "~/lib/envie.client"; // your createEnvie(...) instance// Then use the elements exactly as a Liquid theme would:
<envie-button product-id={product.id} handle={product.handle} />Bringing your own client instead of @getenvie/client? Hand it over explicitly:
window.__envie.setClient(myClient) — anything implementing getConfig, getWishlist and
toggle qualifies.
5. Attach the customer on login (server-side)
A public key cannot act as a customer — that's the point of it being safe to ship to browsers. When a shopper logs in, merge their anonymous list into their customer list from your server, with a private key:
// app/routes/account.login.tsx (in your action, after authentication succeeds)
import { createEnvie } from "@getenvie/client";
const envieServer = createEnvie({
mode: "private",
shop: "your-store.myshopify.com",
secretKey: context.env.ENVIE_SECRET_KEY, // env_sk_… — server only, never in the bundle
anonymousId, // sent up from the browser — the SDK keeps it in localStorage as `envie:aid`
});
await envieServer.identify({
customerId, // the Shopify customer id you just authenticated
});Items are merged (duplicates collapse), and the shopper's next read returns the combined list.
Common mistakes
- Calling
identify()from the browser. Rejected in public mode, always — a browser asserting "I am customer 42" is exactly what the mode exists to prevent. Identity comes from your server. - Forgetting to declare the origin. Everything works in server-side rendering, then browser calls fail CORS preflight. Add the origin in Developers first.
- Shipping
env_sk_…in client code. Private keys have full shop scope. If one ever reaches a browser bundle, rotate it in Developers immediately. - Rendering from
titleSnapshot. It exists for exports, not UIs. Render from your own product data (Storefront API) keyed byproductId/handle— prices and titles stay correct.