Custom storefront
Introduction
This guide explains how to integrate Growave into environments where the standard Shopify theme extension is not available:
- Custom storefronts built with Hydrogen, Next.js, or other headless frameworks
- Mobile applications displaying Growave widgets in a WebView
By the end of this guide, you'll know how to initialize the SDK, authenticate customers, configure cart and discount integrations, and add widgets using placeholders.
Why custom integration is different
On a standard Shopify Online Store, Growave uses the theme extension to automatically access store data through Liquid templates. The integration also relies on:
- App Proxy — automatically identifies logged-in customers
- Standard Shopify endpoints —
/cart/add.js,/cart/update.js,/discount/<code> - Predictable URL structure —
/products/<handle>,/collections/<handle>
On a custom storefront, none of these are available. You must explicitly provide the information that Growave would normally obtain automatically.
1. Include the Growave script
Add the Growave bootstrap script to your page. This loads the SDK and makes window.gw.sdk available.
<script src="https://static.growave.io/bootstrap_growave.js"></script>
For mobile apps, load this script in your WebView. All subsequent SDK calls work the same way.
2. Initialize the SDK
Before calling init(), wait for the SDK to be ready. The gwSdkReady event fires when window.gw.sdk becomes available.
function onSdkReady() {
window.gw.sdk.init({
// Required options
enabledApps: ["reviews", "wishlists", "rewards"],
myshopifyDomain: "your-store.myshopify.com",
storefrontApiAccessToken: "your-storefront-api-token",
// Optional: authenticate immediately if token is available
growaveToken: "eyJhbGciOiJIUzI1NiIs...",
});
}
if (window.gw && window.gw.sdk) {
onSdkReady();
} else {
document.addEventListener("gwSdkReady", onSdkReady);
}
sdk.init()init(options: SdkInitOptions): voidParameters
optionsSdkInitOptions- Configuration options for SDK initialization
Key configuration options
| Option | Required | Description |
|---|---|---|
enabledApps | Yes | Which Growave apps to load (reviews, wishlists, rewards, etc.) |
myshopifyDomain | Yes | Your store's .myshopify.com domain |
storefrontApiAccessToken | Yes | Shopify Storefront API access token (critical for widget functionality) |
growaveToken | No | Growave JWT token for pre-authenticated users |
currencyIsoCode | No | Currency code (e.g., "USD", "EUR") |
languageIsoCode | No | Language for Storefront API requests |
The storefrontApiAccessToken is critical for Growave to function properly. Many widgets rely on the Shopify Storefront API to fetch product information, prices, inventory status, and other data. Without a valid token, widgets may not display correctly or may show incomplete information.
3. Authenticate customers
On a standard Online Store, Growave identifies customers automatically through App Proxy. On a custom storefront, you must authenticate customers explicitly.
Why session continuity matters
When a visitor browses your store as a guest, Growave creates a guest session that tracks their activity — wishlists they create, referral links they clicked, and other interactions. When the customer logs in, this guest session should transfer to their authenticated account. Otherwise:
- Wishlists added as a guest won't appear in the customer's account
- Referral attribution will be lost, and the referral program won't credit the referrer
- Other session data may not persist
Choose the authentication method that best preserves session continuity for your use case.
Signed authentication (recommended)
Use this method when you have access to the Shopify customer ID and can generate HMAC signatures on your server. This is the preferred method because it automatically preserves the guest session — all wishlist items and referral attribution transfer seamlessly to the authenticated customer.
How it works:
- Your server generates an HMAC-SHA1 signature using the customer ID, timestamp, and your Growave secret key
- You pass the signature to the SDK along with the customer ID and timestamp
- Growave verifies the signature server-side and links the guest session to the customer
// Server-side: generate the signature
// signature = sha1("{shopifyCustomerId}{signatureTimestamp}{secretKey}")
// Client-side: authenticate
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.AUTHENTICATION,
() => {
window.gw.sdk.authentication.methods.authenticateCustomer({
shopifyCustomerId: 7891234567890,
signatureTimestamp: 1703001234,
signature: "a1b2c3d4e5f6...",
});
},
);
sdk.authentication.methods.authenticateCustomer()authenticateCustomer(params: { shopifyCustomerId: number; signature: string; signatureTimestamp: number }): Promise<Success>Parameters
params{ shopifyCustomerId: number; signature: string; signatureTimestamp: number }- Authentication parameters object.
Returns
Promise<Success>Promise that resolves to a success result once the request is processed, or rejects with an error. If `logoutCustomer()`, `setToken()` or another `authenticateCustomer()` call was made while this request was in flight, the later call wins and this result is not applied. Check the outcome with {@link getCurrentRole}.
Always generate signatures on your server. Never expose your secret key in client-side code.
JWT token authentication
Use this method when your backend calls the Growave Private API to obtain a JWT token. This approach is flexible and works well for headless architectures.
How it works:
- Your backend calls the getStorefrontToken Private API endpoint
- The API returns a JWT token identifying the customer
- You pass this token to the SDK
Preserving session with JWT
To preserve the guest session when using JWT authentication, pass the guest's sessionToken when calling the getStorefrontToken API. This links the new JWT to the existing guest session, ensuring wishlists and referral attribution transfer correctly.
Pass token during initialization
If you know the customer's identity before initializing the SDK, pass the token via the growaveToken option:
window.gw.sdk.init({
enabledApps: ["reviews", "wishlists", "rewards"],
myshopifyDomain: "your-store.myshopify.com",
storefrontApiAccessToken: "your-storefront-token",
growaveToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", // From your backend
});
Set token after initialization
If the customer logs in after the SDK is initialized, use setToken. This method saves the token to localStorage. To avoid unnecessary overwrites, check if the customer is already authenticated using getShopifyCustomerId():
// Customer logs in, your backend fetches the Growave token
const growaveToken = await yourBackend.getGrowaveToken(customerId);
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.AUTHENTICATION,
() => {
const currentCustomerId =
window.gw.sdk.authentication.methods.getShopifyCustomerId();
if (currentCustomerId !== customerId) {
window.gw.sdk.authentication.methods.setToken(growaveToken);
console.log("Customer authenticated");
}
},
);
sdk.authentication.methods.getShopifyCustomerId()getShopifyCustomerId(): number | null
Returns
number | nullThe Shopify customer ID if authenticated, or `null` if not authenticated or token is invalid.
sdk.authentication.methods.setToken()When using setToken() or growaveToken, session continuity depends on how you obtain the JWT. If you call getStorefrontToken without the guest's sessionToken, the guest session will not transfer. Make sure to pass the sessionToken parameter to preserve wishlists and referral attribution.
Log out customers
When the customer signs out of your storefront, call logoutCustomer(). It removes the stored token right away, so getCurrentRole() returns null. The next Growave request gets a new guest token, and you don't need to reload the page. Other open tabs of your storefront switch to the new identity for their Growave requests too; the widgets they show update after a reload.
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.AUTHENTICATION,
() => {
if (window.gw.sdk.authentication.methods.getCurrentRole() === "customer") {
window.gw.sdk.authentication.methods.logoutCustomer();
}
},
);
Call logoutCustomer() only when the customer has actually signed out. If logoutCustomer() and authenticateCustomer() overlap, the call made last wins: an authenticateCustomer() request that is still in flight when you log out is discarded.
On a standard Online Store, Shopify owns the customer session. logoutCustomer() does not sign the customer out there: the next request authenticates them again through App Proxy. Log them out of Shopify instead.
sdk.authentication.methods.logoutCustomer()4. Customize URL handling
Growave widgets contain links to product pages, collection pages, and the login page. On a standard Online Store, these follow Shopify's URL structure (/products/<handle>, /collections/<handle>, /account/login). Your custom storefront likely uses different URLs.
Use setCustomURLsBuilder to provide custom builders for these URLs. All three methods are required — Growave will use them in place of the default Shopify URL builders for product, collection, and login links rendered in widgets.
window.gw.sdk.common.methods.setCustomURLsBuilder({
createProductUrl({ productHandle, variantId, productId }) {
// /products/blue-shirt → /p/blue-shirt
const url = `/p/${productHandle}`;
return variantId ? `${url}?variant=${variantId}` : url;
},
createCollectionUrl({ collectionHandle, collectionId }) {
// /collections/summer → /c/summer
return `/c/${collectionHandle}`;
},
createStorefrontLoginUrl() {
// /customer_authentication/login → /custom-auth-url
return "/custom-auth-url";
},
});
sdk.common.methods.setCustomURLsBuilder()setCustomURLsBuilder(customURLsBuilder: CustomURLsBuilderInterface): Promise<Success>Parameters
customURLsBuilderCustomURLsBuilderInterface- An object implementing the
CustomURLsBuilderInterfaceinterface.
Returns
Promise<Success>CustomURLsBuilderInterface
Your custom URL builder must implement this interface:
CustomURLsBuilderInterfaceMethods
createCollectionUrl(params)stringcreateProductUrl(params)stringcreateStorefrontLoginUrl()string
| Name | Type | Description |
|---|---|---|
| params | { collectionHandle: string; collectionId?: number } | — |
| Name | Type | Description |
|---|---|---|
| params | { productHandle: string; productId?: number; variantId?: number } | — |
Common URL transformations:
| Standard Shopify URL | Your custom URL | Builder method |
|---|---|---|
/products/blue-shirt | /p/blue-shirt | createProductUrl |
/collections/summer | /c/summer | createCollectionUrl |
/customer_authentication/login | /custom-auth-url | createStorefrontLoginUrl |
5. Integrate cart operations
Growave features like "Add to Cart from Wishlist" or "Redeem reward for free product" need to interact with your cart. On a standard Online Store, Growave uses Shopify's /cart/add.js and /cart/update.js endpoints. Your custom storefront needs a custom cart repository.
Use setCustomCartRepository to provide your own cart implementation:
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.CART,
() => {
window.gw.sdk.cart.methods.setCustomCartRepository({
async getCart() {
const response = await fetch("/api/cart");
const data = await response.json();
// Transform your API response to the expected format
return {
cart: {
token: data.id,
items: data.lines.map((line) => ({
id: line.id,
variant_id: line.variantId,
product_id: line.productId,
quantity: line.quantity,
})),
discount_codes: data.discountCodes || [],
},
};
},
async addToCart({ items }) {
const response = await fetch("/api/cart/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items }),
});
const data = await response.json();
return {
cart: {
token: data.id,
items: data.lines.map((line) => ({
id: line.id,
variant_id: line.variantId,
product_id: line.productId,
quantity: line.quantity,
})),
discount_codes: data.discountCodes || [],
},
};
},
});
},
);
The methods must return data in a specific format. Transform your API responses as needed:
{
cart: {
token: "cart-id",
items: [
{ id: "line-id", variant_id: 12345, product_id: 67890, quantity: 1 }
],
discount_codes: [
{ code: "SAVE10", applicable: true }
]
}
}
sdk.cart.methods.setCustomCartRepository()setCustomCartRepository(cartRepository: CartRepositoryInterface): voidParameters
cartRepositoryCartRepositoryInterface
CartRepositoryInterface
Your custom cart repository must implement this interface:
CartRepositoryInterfaceInterface for cart repository implementations. Defines methods for interacting with the cart API.
Methods
addToCart(params)Promise<{ cart: { discount_codes: { applicable: boolean; code: string }[]; items: { id: string | number; key?: string; product_id: number; quantity: number; variant_id: number }[]; token: string }; sections?: Record<string, string | null> }>- Adds items to the cart.
changeCartItem(params)Promise<{ cart: { discount_codes: { applicable: boolean; code: string }[]; items: { id: string | number; key?: string; product_id: number; quantity: number; variant_id: number }[]; token: string }; sections?: Record<string, string | null> }>- Updates the cart with discount codes, attributes, or other modifications.
getCart()Promise<{ cart: { discount_codes: { applicable: boolean; code: string }[]; items: { id: string | number; key?: string; product_id: number; quantity: number; variant_id: number }[]; token: string } }>- Retrieves the current cart.
updateCart(params)Promise<{ discount_codes: { applicable: boolean; code: string }[]; items: { id: string | number; key?: string; product_id: number; quantity: number; variant_id: number }[]; sections: Record<string, string | null>; token: string }>
| Name | Type | Description |
|---|---|---|
| params | { items: { properties?: Record<string, string>; quantity: number; variantID: number }[]; sections?: string[] } | Parameters for adding items |
| Name | Type | Description |
|---|---|---|
| params | { lineKey?: string; quantity: number; sections?: string[]; variantID: number } | Update parameters |
| Name | Type | Description |
|---|---|---|
| params | { attributes?: Record<string, string>; discount?: string; sections: string[] } | — |
6. Handle discount codes
When customers redeem rewards for discount codes, Growave needs to apply those codes to the cart. On a standard Online Store, this uses the /discount/<code> endpoint. Your custom storefront needs a custom discount strategy.
Use setCustomDiscountStrategy to provide your own implementation:
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.DISCOUNTS,
() => {
window.gw.sdk.discount.methods.setCustomDiscountStrategy({
async applyDiscount({ discountCode }) {
const response = await fetch("/api/cart/apply-discount", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: discountCode }),
});
if (!response.ok) {
throw new Error("Failed to apply discount");
}
},
});
},
);
sdk.discount.methods.setCustomDiscountStrategy()setCustomDiscountStrategy(strategy: DiscountServiceStrategyInterface): voidParameters
strategyDiscountServiceStrategyInterface
DiscountServiceStrategyInterface
Your custom discount strategy must implement this interface:
DiscountServiceStrategyInterfaceInterface for custom discount service strategy implementations. Allows you to define how discount codes are applied in your store.
Methods
applyDiscount(params)Promise<unknown>- Applies a discount code to the cart.
| Name | Type | Description |
|---|---|---|
| params | { discountCode: string } | Parameters for applying discount |
7. Add widgets with placeholders
Widgets are the visual components of Growave — review displays, wishlist buttons, rewards popups, etc. On a custom storefront, you add widgets by placing placeholder elements in your HTML.
A placeholder is a <div> with a specific class name. When Growave detects the placeholder, it automatically injects the corresponding widget.
<!-- Reviews widget placeholder -->
<div
class="gw-reviews-widget-placeholder"
style="display: block"
data-gw-product-id="7891234567890"
></div>
<!-- Wishlist button placeholder -->
<div
class="gw-add-to-wishlist-product-page-placeholder"
style="display: block"
data-gw-product-id="7891234567890"
data-gw-variant-id="45678901234567"
></div>
<!-- Rewards popup placeholder -->
<div class="gw-rewards-popup-widget-placeholder" style="display: block"></div>
Key points about placeholders
-
style="display: block"— Required because Shopify hides empty divs. Always include this. -
Data attributes — Many widgets require product or variant IDs. Check the widget documentation for required attributes.
-
Dynamic updates — Some data attributes (marked as dynamic in docs) can be updated after rendering. The widget will automatically re-render.
// Update variant when customer selects a different option
const placeholder = document.querySelector(
".gw-add-to-wishlist-product-page-placeholder",
);
placeholder.setAttribute("data-gw-variant-id", newVariantId);
For a complete list of available widgets and their placeholder classes, see the Placeholders documentation.
Complete integration example
Here's a complete example that ties everything together:
function initializeGrowave() {
// 1. Initialize with basic configuration
window.gw.sdk.init({
enabledApps: ["reviews", "wishlists", "rewards"],
myshopifyDomain: "your-store.myshopify.com",
storefrontApiAccessToken: "your-storefront-token",
});
// 2. Set up custom URL builders (if your routes differ from Shopify defaults)
window.gw.sdk.common.methods.setCustomURLsBuilder({
createProductUrl({ productHandle, variantId, productId }) {
const url = `/p/${productHandle}`;
return variantId ? `${url}?variant=${variantId}` : url;
},
createCollectionUrl({ collectionHandle, collectionId }) {
return `/c/${collectionHandle}`;
},
createStorefrontLoginUrl() {
return "/custom-auth-url";
},
});
// 3. Set up custom cart repository
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.CART,
() => {
window.gw.sdk.cart.methods.setCustomCartRepository({
async getCart() {
const data = await (await fetch("/api/cart")).json();
return { cart: transformCart(data) };
},
async addToCart({ items }) {
const data = await (
await fetch("/api/cart/add", {
method: "POST",
body: JSON.stringify({ items }),
})
).json();
return { cart: transformCart(data) };
},
});
},
);
// 4. Set up custom discount strategy
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.DISCOUNTS,
() => {
window.gw.sdk.discount.methods.setCustomDiscountStrategy({
async applyDiscount({ discountCode }) {
await fetch("/api/cart/apply-discount", {
method: "POST",
body: JSON.stringify({ discountCode }),
});
},
});
},
);
}
// Wait for SDK to be ready
if (window.gw && window.gw.sdk) {
initializeGrowave();
} else {
document.addEventListener("gwSdkReady", initializeGrowave);
}
// 5. Authenticate customer when they log in (using signed authentication)
function onCustomerLogin(shopifyCustomerId, signature, timestamp) {
window.gw.sdk.runIfModuleReady(
window.gw.sdk.common.constants.modules.AUTHENTICATION,
() => {
window.gw.sdk.authentication.methods.authenticateCustomer({
shopifyCustomerId,
signature,
signatureTimestamp: timestamp,
});
},
);
}