I just built a complete pre-purchase product offer application using Shopify’s checkout UI extensions and Gadget in under 25 minutes, and I want to share exactly how I did it. This tutorial shows you how to create an app that lets merchants select products to offer customers during checkout – all without writing complex authentication or data sync code.
Key Benefits You’ll Get
- Complete full-stack solution: Backend API, frontend admin interface, and checkout extension in one codebase
- Zero boilerplate setup: Gadget handles authentication, database management, and Shopify integration automatically
- Real-time data sync: Product changes in Shopify automatically update your app database
- Production-ready deployment: Extensions deploy directly to Shopify’s hosting infrastructure
What We’re Building
A pre-purchase offer system where:
- Merchants select which product to offer in their admin app
- The selected product appears in the checkout as an upsell opportunity
- Customers can add the offered product directly from the checkout page
- Everything syncs automatically with Shopify’s product catalog
Step 1: Setting Up Your Gadget App and Shopify Connection
First, I created a new Gadget app called “pre-purchase-v1-tutorial”. When you create a Gadget app, you automatically get:
- Hosted and scaled PostgreSQL database
- Serverless Node.js backend
- React frontend powered by Vite
- Everything configured and ready to deploy
To connect with Shopify checkout extensions, you need to set up the connection through the Partner Dashboard (not the regular Shopify connection method).
Partner Dashboard Setup
- Go to your Shopify Partner Dashboard
- Create a new app manually
- Use the same name as your Gadget app
- Copy the Client ID and Secret to your Gadget app
Configuring API Scopes and Data Models
In Gadget’s Shopify connection setup:
- Select read_products scope (to sync product data)
- Choose the Product data model for automatic syncing
This setup gives you several powerful features automatically:
- Webhook subscriptions to Shopify product webhooks
- Historical data sync of existing products
- Real-time updates when merchants change products
The historical sync respects Shopify’s rate limits and imports your existing product catalog as efficiently as possible.
Step 2: Adding Custom Shop Metadata Field
I added a custom field to store which product should be offered in checkout:
// Custom field on Shop model
prePurchaseProduct: {
type: "string", // Stores product ID
namespace: "gadget-tutorial",
key: "pre-purchase-product"
}
This creates a Shopify metafield that checkout extensions can easily access.
Step 3: Building the Backend API
I created a custom action on the Shop model called savePrepurchaseProduct:
Custom Action Code
export const run = async ({ params, api, connections, logger }) => {
// Get product ID from parameters
const productId = params.productId;
// Use Shopify GraphQL API to set metafield
const response = await connections.shopify.current.graphql(`
mutation($input: MetafieldsSetInput!) {
metafieldsSet(metafields: [$input]) {
metafields {
id
key
namespace
value
}
userErrors {
field
message
}
}
}
`, {
input: {
ownerId: connections.shopify.current.id,
namespace: "gadget-tutorial",
key: "pre-purchase-product",
type: "product_reference",
value: productId
}
});
logger.info(response);
};
export const params = {
productId: { type: "string", required: true }
};
Important: Enable access control for “Shopify app users” so merchants can call this action from the frontend.
Step 4: Creating the Admin Interface
The frontend lets merchants select which product to offer:
Key Components
| Component | Purpose | Technology |
|---|---|---|
| Product Selector | Dropdown of available products | Shopify Polaris Select |
| Form Handler | Manages form state and API calls | Gadget’s useActionForm hook |
| Data Fetcher | Loads products and shop data | Gadget’s useFindMany hook |
Frontend Implementation
// Fetch existing products
const [{ data: products, fetching, error }] = useFindMany(api.shopify.product);
// Get current shop ID
const [{ data: shop }] = useFindFirst(api.shopify.shop, {
select: { id: true }
});
// Form handler for custom action
const [{ submit, formState }] = useActionForm(api.shopify.shop.savePrepurchaseProduct, {
defaultValues: { id: shop?.id }
});
The form automatically handles:
- Form state management
- API calls to your custom action
- Loading states and error handling
- Shopify Polaris component integration
Step 5: Building the Checkout Extension
Extension Setup
I generated the checkout extension directly in my Gadget project:
yarn generate extension
# Select: checkout-ui
# Name: pre-purchase-ext
# Connect to existing Partner app
# Language: JavaScript
Extension Configuration
In shopify.extension.toml, I configured the extension to receive the metafield:
[[extensions.settings]]
key = "product_id"
type = "single_line_text_field"
name = "Product ID"
description = "Product to offer in checkout"
[input.variables]
product_id = "$app:gadget-tutorial.pre-purchase-product"
Extension Logic
The checkout extension:
- Reads the metafield: Gets the selected product ID from app metafields
- Queries product data: Uses Shopify’s Storefront API to get product details
- Checks cart contents: Ensures the product isn’t already in the cart
- Renders the offer: Shows product image, title, price, and add button
- Handles add to cart: Uses Shopify’s cart modification hooks
// Get metafield input
const metafields = useAppMetafields();
const productId = metafields.find(m => m.key === 'pre-purchase-product')?.value;
// Query product details
const { data: product, loading } = useApi().query(PRODUCT_QUERY, {
variables: { id: productId }
});
// Add to cart functionality
const applyCartLinesChange = useApplyCartLinesChange();
const addToCart = () => {
applyCartLinesChange({
type: 'addCartLine',
merchandiseId: product.variants.nodes[0].id,
quantity: 1
});
};
Step 6: Deployment and Testing
Local Development
# Start extension development server
yarn dev
# Select your development store
# Copy preview URL to test checkout
Production Deployment
# Deploy extension to Shopify
yarn deploy
# Release as new version
# Configure in Checkout Editor
The deployed extension appears in your store’s Checkout Editor where you can:
- Position it anywhere in the checkout flow
- Customize its appearance
- Enable/disable as needed
Integration Results
Here’s what merchants and customers experience:
Merchant Experience
- Simple dropdown to select offer product
- One-click save to activate
- Immediate preview in checkout
- Easy product switching anytime
Customer Experience
- Relevant product suggestions during checkout
- One-click add to cart
- Seamless checkout flow
- No page redirects or interruptions
Development Benefits with Gadget
| Traditional Approach | With Gadget |
|---|---|
| Set up authentication manually | Auto-configured OAuth |
| Build webhook handlers | Auto-generated webhook endpoints |
| Manage database schema | Visual schema editor |
| Handle rate limiting | Built-in rate limit management |
| Deploy backend separately | One-click deployment |
Practical Tips for Production
Performance Optimization:
- Extension only renders when product is selected
- Uses Shopify’s optimized Storefront API
- Minimal bundle size with tree shaking
Error Handling:
- Graceful fallback when products are unavailable
- Clear error messages for merchants
- Automatic retry for failed API calls
Customization Options:
- Extend to support multiple product offers
- Add variant selection in admin interface
- Include custom messaging per product
- Track conversion rates with analytics
Common Challenges and Solutions
Issue: Extension not showing in checkout
Solution: Verify metafield namespace and key match exactly between backend and extension config
Issue: Products not syncing from Shopify
Solution: Check webhook subscriptions in Connected Apps section and manually trigger sync if needed
Issue: Access denied errors
Solution: Ensure “Shopify app users” access control is enabled for custom actions
FAQ
Can I offer multiple products in a single checkout?
Yes, you can extend this pattern by storing an array of product IDs in the metafield and rendering multiple offer blocks in your extension.
How do I track which offers convert to sales?
Shopify’s checkout extensions provide analytics data, or you can implement custom tracking by sending events to your analytics platform when customers interact with offers.
Can I customize the offer appearance for different products?
Absolutely. Store additional metadata like custom messages, images, or styling preferences alongside the product ID, then use that data to customize the extension’s rendering.
What happens if the offered product goes out of stock?
The Storefront API will return availability data. You can check inventory levels and hide the offer or show an “out of stock” message accordingly.
Is this approach suitable for high-traffic stores?
Yes, Gadget handles scaling automatically, and Shopify’s checkout extensions are designed for high-performance scenarios with optimized rendering and caching.
This approach gives you a production-ready pre-purchase offer system that’s maintainable, scalable, and follows Shopify’s best practices. The entire codebase stays in one place, making it easy to iterate and add features as your business grows.