Hey there, future app creator!
So, you want to build a Shopify app? That's awesome! Whether you're a coding whiz or just starting out, this guide will walk you through creating your very own app for the Shopify platform. We'll cover everything from setting up your workspace to getting your app live in the Shopify App Store.
Quick Start (aka the "I'm in a hurry" version)
Short on time? No worries! Here's the express route:
- We've got a starter kit that handles all the boring setup stuff.
- You can jump right into making your app do cool things.
- This guide is still super helpful for understanding how Shopify apps work.
Want to dive in?
Grab our ShopiFast starter kit and let's go!
Now, if you're ready for the full adventure, let's get started!
1. Introduction
What are Shopify apps?
Shopify apps are extensions that add functionality to Shopify stores. They can range from simple tools to complex solutions that integrate with various aspects of a merchant's business.
Why build a custom Shopify app?
Building a custom Shopify app allows you to create unique solutions for merchants, potentially fill gaps in the market, and even generate revenue through the Shopify App Store.
Overview of the app we'll be building
In this tutorial, we'll create a simple inventory management app that helps merchants track their product stock levels and sends notifications when items are running low.
2. Prerequisites and Setup
Creating a Shopify Partner account
- Go to the Shopify Partners website.
- Click "Join now" and follow the registration process.
- Once approved, log in to your Shopify Partners dashboard.
Installing necessary tools
Node.js and npm
- Visit the official Node.js website.
Download and install the LTS (Long Term Support) version for your operating system.
-
To verify the installation, open a terminal and run:
node --version npm --version
Shopify CLI
- Open your terminal.
-
Install Shopify CLI globally by running:
npm install -g @shopify/cli @shopify/theme
-
Verify the installation:
shopify version
Setting up a development store
- Log in to your Shopify Partners dashboard.
- Click "Stores" in the left sidebar.
- Click "Add store" and select "Development store".
- Follow the prompts to create your development store.
3. Creating Your Shopify App
Using Shopify CLI to generate a new app
- Open your terminal and navigate to your desired project directory.
-
Run the following command to create new app with node.js template.
shopify app inti template=node
Note: running the command without template
argument , will create a remix app.
- Install dependencies
yarn install
- You can create a Shopify app either via the Shopify CLI or from the Shopify Partners Dashboard. If you create the app from the dashboard, you’ll need to connect it to your project manually. If you use the CLI, it will handle app creation for you.
- Choose the app name
- Choose the store you created before for development
- To install your app on a shop, it must be accessible from the internet. You have two options: use your own tunneling service, such as Ngrok, or let Shopify handle it, which is recommended. Shopify uses Cloudflare Argo Tunnel to expose your app to the world. When you use Shopify’s method, a new URL is generated each time you run the app. The main downside is that this URL changes every time.
- I f you chose to use your own tunnel tool, you need to update the urls in
shopify.app.toml
file with your tunnel urls. Else , just skip this step.
.......
application_url = "https://copy-b-christopher-editorials.trycloudflare.com"
.......
redirect_urls = [
"https://copy-b-christopher-editorials.trycloudflare.com/auth/callback",
"https://copy-b-christopher-editorials.trycloudflare.com/auth/shopify/callback",
"https://copy-b-christopher-editorials.trycloudflare.com/api/auth/callback"
]
.......
- Install the app on your store, by click on
p
congratulations you just created your first shopify app.
4. Understanding Shopify App Architecture
The generated project structure for a Shopify app consists of three main components. Let's explore each one:
1. Frontend
- This is the user interface of your app that will be embedded in the Shopify store admin.
- It's where merchants interact with your app.
- Typically built using React, Polaris, JavaScript.
The frontend
directory contains the React-based user interface:
App.jsx
andRoutes.jsx
: Define the main app structure and routing.assets
folder: Contains static files like images and SVGs.components
folder: Houses reusable React components.providers
: Contains context providers for app-wide state management.hooks
folder: Custom React hooks for shared logic.useAppQuery.js
anduseAuthenticatedFetch.js
: handle data fetching and authenticated data fetchting.pages
folder: Contains individual page components, used in Shopify admin.
2. Backend
- This is the server-side component aka API.
- It handles various backend tasks, including:
- Authentication
- Interacting with the Shopify API
- Database operations
- Business logic processing
This project is using Node.js, Express and sqlite database
index.js
: The main entry point for the Node.js server.database.sqlite
: SQLite database file for local data storage.privacy.js
: Handles app mandatory compliance webhooks, required for public apps listed on shopify app store.shopify.js
: configures a Shopify app using the@shopify/shopify-app-express
package. It sets up API version, authentication paths, webhooks, and uses SQLite for session storage. And, also includes billing configuration.
3. Shopify App Configurations
client_id = "8538fc16e6b9da87768f65308885c52c"
name = "hellofy"
handle = "hellofy"
application_url = "https://copy-b-christopher-editorials.trycloudflare.com"
embedded = true
[build]
automatically_update_urls_on_dev = true
dev_store_url = "helllofy.myshopify.com"
include_config_on_deploy = true
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "write_products"
[auth]
redirect_urls = [
"https://copy-b-christopher-editorials.trycloudflare.com/auth/callback",
"https://copy-b-christopher-editorials.trycloudflare.com/auth/shopify/callback",
"https://copy-b-christopher-editorials.trycloudflare.com/api/auth/callback"
]
[webhooks]
api_version = "2024-07"
[pos]
embedded = false
- This component contains the configuration for your app.
- It includes settings such as:
- App name, handle
- API scopes (permissions your app needs)
- App URLs (for installation, callback handling, etc.)
- And other configurations
Learn more about app configuration here: App configuration
Understanding these components and their roles will help you navigate and develop your Shopify app more effectively.
5. Configuring Your App
Setting up app name and description
- Open
shopify.app.toml
in your project root. - Update the
name
andscopes
fields to match your app's requirements.
Configuring app scopes and permissions
In the same shopify.app.toml
file, update the scopes
array with the required permissions, e.g.:
scopes = "write_products"
you can learn more about scopes here.
Setting up API credentials
Shopify credentials are handled differently in development and production environments.
- During development, the Shopify CLI automatically provides these credentials, which you can view by running "shopify app env show". There's no need to manually add them to your .env file.
- For production, however, you must explicitly set these credentials in your environment. You can obtain the necessary credentials (API key and API secret key) either by running the same CLI command or by accessing your Shopify Partners dashboard:
- In your Shopify Partners dashboard, go to "Apps" and select your app.
- Under "App credentials", you'll find your API key and API secret key.
Setting up the database
By default the create project comes with sqlite database, in most cases sqlite is optimal for production. There’re a various databases you can use based on your cases: MonogDB, Postgres, MySQL …
Let’s change it to Postgres with Prisma ORM.
- Install prisma
yarn add prisma @prisma/client
- Setup prisma
yarn prisma init
- Install session storage manager to let Shopify manage session:
yarn add @shopify/shopify-app-session-storage-prisma
- Update
shopify.js
to the following
import { LATEST_API_VERSION } from "@shopify/shopify-api";
import { shopifyApp } from "@shopify/shopify-app-express";
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { restResources } from "@shopify/shopify-api/rest/admin/2023-04";
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const storage = new PrismaSessionStorage(prisma);
const shopify = shopifyApp({
api: {
apiVersion: LATEST_API_VERSION,
restResources,
billing: undefined,
},
auth: {
path: "/api/auth",
callbackPath: "/api/auth/callback",
},
webhooks: {
path: "/api/webhooks",
},
sessionStorage: storage,
});
export default shopify;
- Update Prisma schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
firstName String?
lastName String?
email String?
accountOwner Boolean @default(false)
locale String?
collaborator Boolean? @default(false)
emailVerified Boolean? @default(false)
}
- Generate prisma models
yarn prisma generate
- create a docker-compose with postgres service
version: "3"
services:
postgres:
ports:
- 5432:5432
environment:
- POSTGRES_DB=hellofy
- POSTGRES_USER=default
- POSTGRES_PASSWORD=secret
image: postgres:13.3
volumes:
postgres_data:
- Run the database
docker-compose up -d postgres
- Update .env with the right database url
DATABASE_URL="postgresql://default:secret@localhost:5432/hellofy?schema=public"
- Generate database migration:
yarn prisma migrate dev
- enter migration name: ex:
init
Now, run the app again and you see the session now is storage in postgres
Congratulations 🥳! You just created a first Shopify App.
6. Developing Core Functionality
Building a Shopify App to Update Product Descriptions with OpenAI
Follow these steps to integrate OpenAI into your Shopify app to update product descriptions.
- Create an OpenAI Account
Create an OpenAI account by visiting OpenAI Platform.
- Get OpenAI Credentials
....
OPENAI_API_KEY=sk-proj-..........XZIFFL29UxC9T3BlbkFJ..............
- Install open ai library
yarn add openai
- Add openai library to
index.js
import OpenAI from "openai";
// Initialize OpenAI
const openai = new OpenAI({
apiKey: proccess.env.OPENAI_API_KEY,
});
- Create
products
page in pages folder
import {
Badge,
Card,
ChoiceList,
EmptySearchResult,
Frame,
IndexFilters,
IndexTable,
Page,
Toast,
useIndexResourceState,
useSetIndexFiltersMode,
} from "@shopify/polaris";
import { useCallback, useState, useEffect } from "react";
import { useAppQuery, useAuthenticatedFetch } from "../hooks";
import ProductUpdate from "../components/ProductUpdate";
import { useProducts } from "../services/product";
const Products = () => {
const emptyToastProps = { content: null };
const [toastProps, setToastProps] = useState(emptyToastProps);
const fetch = useAuthenticatedFetch();
const { data, isLoading } = useProducts();
const [showModal, setShowModal] = useState(false);
const [itemStrings, setItemStrings] = useState([
"All",
"Active",
"Draft",
"Archived",
]);
const [selected, setSelected] = useState(0);
const [sortSelected, setSortSelected] = useState(["product asc"]);
const { mode, setMode } = useSetIndexFiltersMode();
const [tone, setStatus] = useState(undefined);
const [type, setType] = useState(undefined);
const [queryValue, setQueryValue] = useState("");
const [products, setProducts] = useState([]);
useEffect(() => {
if (data?.data) {
const transformedProducts = data.data.map((product) => ({
id: product.id,
price: `$${product.variants[0].price}`,
product: product.title,
tone: (
<Badge tone={product.status === "active" ? "success" : "info"}>
{product.status}
</Badge>
),
inventory: `${product.variants[0].inventory_quantity} in stock`,
type: product.product_type,
description: product.body_html,
image: product.image?.src,
}));
setProducts(transformedProducts);
}
}, [data]);
const resourceName = {
singular: "product",
plural: "products",
};
const { selectedResources, allResourcesSelected, handleSelectionChange } =
useIndexResourceState(products);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const disambiguateLabel = (key, value) => {
switch (key) {
case "type":
return value.map((val) => `type: ${val}`).join(", ");
case "tone":
return value.map((val) => `tone: ${val}`).join(", ");
default:
return value;
}
};
const isEmpty = (value) => {
if (Array.isArray(value)) {
return value.length === 0;
} else {
return value === "" || value == null;
}
};
const deleteView = (index) => {
const newItemStrings = [...itemStrings];
newItemStrings.splice(index, 1);
setItemStrings(newItemStrings);
setSelected(0);
};
const duplicateView = async (name) => {
setItemStrings([...itemStrings, name]);
setSelected(itemStrings.length);
await sleep(1);
return true;
};
const onCreateNewView = async (value) => {
await sleep(500);
setItemStrings([...itemStrings, value]);
setSelected(itemStrings.length);
return true;
};
const handleStatusChange = useCallback((value) => setStatus(value), []);
const handleTypeChange = useCallback((value) => setType(value), []);
const handleFiltersQueryChange = useCallback(
(value) => setQueryValue(value),
[]
);
const handleStatusRemove = useCallback(() => setStatus(undefined), []);
const handleTypeRemove = useCallback(() => setType(undefined), []);
const handleQueryValueRemove = useCallback(() => setQueryValue(""), []);
const handleFiltersClearAll = useCallback(() => {
handleStatusRemove();
handleTypeRemove();
handleQueryValueRemove();
}, [handleStatusRemove, handleQueryValueRemove, handleTypeRemove]);
const appliedFilters = [];
if (tone && !isEmpty(tone)) {
const key = "tone";
appliedFilters.push({
key,
label: disambiguateLabel(key, tone),
onRemove: handleStatusRemove,
});
}
if (type && !isEmpty(type)) {
const key = "type";
appliedFilters.push({
key,
label: disambiguateLabel(key, type),
onRemove: handleTypeRemove,
});
}
const rowMarkup = products.map(
(
{ id, product, price, tone, inventory, type, description, image },
index
) => (
<IndexTable.Row
id={id}
key={id}
selected={selectedResources.includes(id)}
position={index}
selectionRange={{ startIndex: 0, endIndex: 2 }}
>
<IndexTable.Cell>
{image ? (
<img
src={image}
alt={"product thumbnail" + product}
width={40}
height={40}
/>
) : (
<div>no image</div>
)}
</IndexTable.Cell>
<IndexTable.Cell>{product}</IndexTable.Cell>
<IndexTable.Cell>{price}</IndexTable.Cell>
<IndexTable.Cell>{tone}</IndexTable.Cell>
<IndexTable.Cell>{inventory}</IndexTable.Cell>
<IndexTable.Cell>{type}</IndexTable.Cell>
<IndexTable.Cell>{description}</IndexTable.Cell>
</IndexTable.Row>
)
);
const emptyStateMarkup = (
<EmptySearchResult
title={"No products yet"}
description={"Try changing the filters or search term"}
withIllustration
/>
);
const primaryAction =
selected === 0
? {
type: "save-as",
onAction: onCreateNewView,
disabled: false,
loading: false,
}
: {
type: "save",
onAction: async () => {
await sleep(1);
return true;
},
disabled: false,
loading: false,
};
const [show, setShow] = useState(true);
const [description, setDescription] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const [active, setActive] = useState(false);
const toggleActive = useCallback(() => setActive((active) => !active), []);
const toastMarkup = active ? (
<Toast content="Select only 1 product" onDismiss={toggleActive} />
) : null;
const generate = () => {
if (selectedResources.length !== 1) {
toggleActive();
return;
}
setIsGenerating(true);
const response = fetch("/api/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ selectedResources }),
});
response
.then((response) => response.json())
.then((data) => {
setDescription(data.description);
setShow(true);
setIsGenerating(false);
})
.catch((error) => {
console.error("Error:", error);
setIsGenerating(false);
});
return true;
};
return (
<Frame>
{toastMarkup}
<ProductUpdate
active={show}
setActive={setShow}
description={description}
productId={selectedResources[0] || 9581094469949}
/>
<Page
title={"Products"}
primaryAction={{
content: "Generate",
onAction: generate,
loading: isGenerating,
}}
fullWidth
>
<Card padding="0">
<IndexFilters
sortOptions={[
{
label: "Product",
value: "product asc",
directionLabel: "Ascending",
},
{
label: "Product",
value: "product desc",
directionLabel: "Descending",
},
{ label: "Status", value: "tone asc", directionLabel: "A-Z" },
{ label: "Status", value: "tone desc", directionLabel: "Z-A" },
{ label: "Type", value: "type asc", directionLabel: "A-Z" },
{ label: "Type", value: "type desc", directionLabel: "Z-A" },
{
label: "Description",
value: "description asc",
directionLabel: "Ascending",
},
{
label: "Description",
value: "description desc",
directionLabel: "Descending",
},
]}
sortSelected={sortSelected}
queryValue={queryValue}
queryPlaceholder="Searching in all"
onQueryChange={handleFiltersQueryChange}
onQueryClear={() => {}}
onSort={setSortSelected}
primaryAction={primaryAction}
cancelAction={{
onAction: () => {},
disabled: false,
loading: false,
}}
tabs={itemStrings.map((item, index) => ({
content: item,
index,
onAction: () => {},
id: `${item}-${index}`,
isLocked: index === 0,
actions:
index === 0
? []
: [
{
type: "rename",
onAction: () => {},
onPrimaryAction: async (value) => {
const newItemsStrings = itemStrings.map(
(item, idx) => {
if (idx === index) {
return value;
}
return item;
}
);
await sleep(1);
setItemStrings(newItemsStrings);
return true;
},
},
{
type: "duplicate",
onPrimaryAction: async (name) => {
await sleep(1);
duplicateView(name);
return true;
},
},
{
type: "delete",
onPrimaryAction: async () => {
await sleep(1);
deleteView(index);
return true;
},
},
],
}))}
selected={selected}
onSelect={setSelected}
canCreateNewView
onCreateNewView={onCreateNewView}
mode={mode}
setMode={setMode}
filters={[]}
appliedFilters={appliedFilters}
onClearAll={handleFiltersClearAll}
/>
<IndexTable
resourceName={resourceName}
itemCount={products.length}
selectedItemsCount={
allResourcesSelected ? "All" : selectedResources.length
}
onSelectionChange={handleSelectionChange}
emptyState={emptyStateMarkup}
headings={[
{ title: "Thumbnail", hidden: true },
{ title: "Product" },
{ title: "Price" },
{ title: "Status" },
{ title: "Inventory" },
{ title: "Type" },
{ title: "Description" },
]}
>
{rowMarkup}
</IndexTable>
</Card>
</Page>
</Frame>
);
};
export default Products;
- create api route to get product in
index.js
const getProducts = async (_req, res) => {
try {
const products = await shopify.api.rest.Product.all({
session: res.locals.shopify.session,
});
res.status(200).send(products);
} catch (error) {
console.error(`Failed to fetch products: ${error.message}`);
res.status(500).send({ error: error.message });
}
};
app.get("/api/products", getProducts);
- Update
useFetch
hook
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { useAuthenticatedFetch } from './useAuthenticatedFetch';
export const useFetch = () => {
const fetch = useAuthenticatedFetch();
const queryClient = useQueryClient();
const get = useCallback(
(url) => {
return fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then((r) => r.text())
.then((text) => JSON.parse(text));
},
[fetch]
);
const post = useCallback(
(url, body) =>
fetch(url, {
method: 'POST',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
}).then((r) => r.json()),
[fetch]
);
return {
get,
post,
mutate: (key) => queryClient.invalidateQueries(key),
};
};
- Create
services/product.js
in frontend folder to handle product resource operations
import { useCallback, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useFetch } from '../hooks';
export const useProducts = () => {
const { get } = useFetch();
const { data, ...rest } = useQuery({
queryFn: () => get('/api/products'),
queryKey: ['products'],
refetchOnWindowFocus: false,
});
return {
data: data || null,
...rest,
};
};
export const useProductUpdate = () => {
const { post, mutate } = useFetch();
const queryClient = useQueryClient();
const [isLoading, setIsLoading] = useState(false);
const update = useCallback(
async (body) => {
setIsLoading(true);
const response = await post('/api/update', body);
await mutate("products")
setIsLoading(false);
return response;
}, [post, queryClient]);
return {
update,
isLoading,
};
}
- In the
products
page we select a single product and send it to the api to generate the description. Let’s add a api endpoint for that. Add the following changes toindex.js
:
const generateDescription = async (_req, res) => {
const { selectedResources } = _req.body;
if (!selectedResources) {
return res.status(400).send({ success: false, message: "No resources selected" });
}
try {
const productId = selectedResources[0];
const product = await shopify.api.rest.Product.find({
session: res.locals.shopify.session,
id: productId,
});
if (!product) {
return res.status(400).send({ success: false, message: "Product not found" });
}
const prompt = `Generate a good description for the product ${product.title}, ${product.body_html}, ${product.product_type}, ${product.handle}, ${product.tags}`;
const completion = await openai.chat.completions.create({
messages: [{ role: "system", content: prompt }],
model: "gpt-4o-mini",
});
const description = completion.choices[0]?.message.content;
res.status(200).send({ success: true, description });
} catch (error) {
console.error(`Error generating description: ${error.message}`);
res.status(500).send({ success: false, error: error.message });
}
};
app.post("/api/generate", generateDescription);
Here, we use a simple prompt based on some product fields to generate a better description. You can tweak the prompt with additional information to create more accurate descriptions.
- After generating the description, it returns to frontend for review and edit by show a modal with prompt output. Here’s the modal
import { Modal, TextField } from "@shopify/polaris";
import { useCallback } from "react";
import { useProductUpdate } from "../services/product";
export default function ProductUpdate({
active,
setActive,
description,
productId,
}) {
if (!active && !description && !productId) {
return null;
}
const handleChange = useCallback(() => setActive(!active), [active]);
const { update, isLoading } = useProductUpdate();
const handleUpdate = async () => {
await update({ description, productId });
handleChange();
};
return (
<Modal
open={active}
onClose={handleChange}
title="Update product description"
primaryAction={{
content: "Update",
onAction: handleUpdate,
loading: isLoading,
disabled: isLoading,
}}
secondaryActions={[
{
content: "Cancel",
onAction: handleChange,
},
]}
>
<Modal.Section>
<TextField
label="Generated description ✨"
value={description}
onChange={handleChange}
multiline={4}
autoComplete="off"
/>
</Modal.Section>
</Modal>
);
}
- After reviewing / editing the description, we update the description in the store product with this endpoint:
const updateProductDescription = async (_req, res) => {
const { description, productId } = _req.body;
if (!description) {
return res.status(400).send({ success: false, message: "Description is required" });
}
try {
const product = new shopify.api.rest.Product({ session: res.locals.shopify.session });
product.id = productId;
product.body_html = description;
await product.save({ update: true });
res.status(200).send({ success: true, data: product });
} catch (error) {
console.error(`Error updating product description: ${error.message}`);
res.status(500).send({ success: false, error: error.message });
}
};
app.post("/api/update", updateProductDescription);
Congratulations 🎉, you now have a useful app!
![helllofy--hellofy--Shopify-ezgif.com-video-to-gif-converter.gif]
7. Deploy the App to Fly.io
Fly.io is a great choice for deploying your Shopify app, providing a simple and efficient way to get your app online, but can you go with any other provider. Follow these steps to deploy your Shopify app to Fly.io.
Prerequisites
- You have a Shopify app ready for deployment.
- You have Docker installed on your machine.
- You have Flyctl (Fly.io command line tool) installed. If not, you can install it here.
1. Create a Fly.io Account
- Go to Fly.io.
- Click on Sign Up and create an account.
2. Install Flyctl
- Follow the installation instructions for your operating system from the Flyctl installation guide.
3. Initialize Fly.io in Your Project
- Open your terminal.
- Navigate to your project directory.
-
Run the following command to log in to Fly.io:
flyctl auth login
-
Initialize your Fly.io app:
flyctl launch
- Follow the prompts to create a new app.
- Choose a region close to you or your target users.
- When asked if you want to deploy now, select **no**. We will configure Docker first.
4. Create a database
There are many database options to choose from: Supabase, CockroachDB, Amazon RDS, or even managing your own on a VPS. For this example, since we're using Fly.io, we'll go with Fly.io Postgres. It's a great fit and integrates seamlessly with Fly.io's .
- Create a PostgreSQL Database
Create a PostgreSQL database instance using the Fly.io CLI:
flyctl postgres create --name your-db-name --region your-region
2.Configure Database Access
Fly.io will provide you with a connection string and credentials for your PostgreSQL database. You can view these details by running:
flyctl postgres attach --app your-app-name your-db-name
- Configure Fly.io
- Open the
fly.toml
file generated by theflyctl launch
command. -
Ensure the
[[services]]
section looks like this:[[services]] internal_port = 3000 protocol = "tcp" [[services.ports]] handlers = ["http"] port = 80 [[services.ports]] handlers = ["tls", "http"] port = 443 [[services.tcp_checks]] interval = "15s" timeout = "2s" grace_period = "1s" restart_limit = 0
Specify secrets for your Fly App
Required Secrets for Your App
To configure your app, you'll need the following secret values:
DATABASE_URL
OPENAI_API_KEY
SHOPIFY_API_KEY
SHOPIFY_API_SECRET
SCOPES
You can obtain the values for these variables from the previous setup steps.
Setting Secrets in Your Fly.io App
To set these secrets in your Fly.io app, follow these steps:
- Open your terminal and navigate to your project directory.
-
Use the Fly.io CLI to set each secret by running the following commands:
flyctl secrets set DATABASE_URL=your_database_url flyctl secrets set OPENAI_API_KEY=your_openai_api_key flyctl secrets set SHOPIFY_API_KEY=your_shopify_api_key flyctl secrets set SHOPIFY_API_SECRET=your_shopify_api_secret flyctl secrets set SCOPES=your_scopes
Replace your_database_url
, your_openai_api_key
, your_shopify_api_key
, your_shopify_api_secret
, and your_scopes
with the actual values you obtained earlier
6. Deploy Your App
-
In your terminal, run:
flyctl deploy
Fly.io will build and deploy your app. This may take a few minutes.
7. Access Your App
- Once the deployment is complete, Fly.io will provide a URL for your app.
- Open the URL in your browser to see your Shopify app running.
7. Connect Your App to Shopify
- Log in to your Shopify Partners dashboard.
- Go to Apps and find your app.
- In the App setup section, update the app URL to the Fly.io URL provided after deployment.
- Save the changes.
What's Next?
This app is just the beginning; there's a lot more you can do to enhance it. Here are some tasks to consider if you want to continue developing the app:
- Add more functionalities
- Implement user settings
- Improve the prompting system
- Enable multiple product generations
- Clean up the project structure
- Set up billing
- Learn how to handle mandatory webhooks
Additional Tips
- Deepen your understanding of the Shopify API. Shopify frequently updates its platform, so staying current is essential.
- Learn more about building Shopify extensions.
- Master Shopify billing.
- Get familiar with Liquid.js.
- Explore public app listing.
By focusing on these areas, you'll be well-equipped to create a robust and feature-rich Shopify app. Happy coding 🚀💻✨!
Get a full ready Shopify app boilerplate @ https://shopifast.dev
The source code for the app is available on github: https://github.com/adibmed/shopify-app-openai
Top comments (0)