Build your first OpenShop app
In this lesson, we will create an embedded Shopify app and run syncOrders.
The flow will read orders from a development store and pass them to a sample
warehouse provider. The sample logs the records; it does not send them to a real
warehouse.
Before you start
Section titled “Before you start”Have these ready:
- Node.js 26 and pnpm 11. Check with
node --versionandpnpm --version. - Docker with its daemon running. We will use a PostgreSQL 17 container.
- Shopify CLI installed and available as
shopify. - A Shopify development store and an account allowed to create and install an app on it.
- Basic familiarity with TypeScript and running commands in a terminal.
Use a development store throughout this lesson. An empty order list is fine. If you already run PostgreSQL on port 5432, use a dedicated database there and substitute its connection URL in the database steps below.
1. Start the database
Section titled “1. Start the database”docker run --name openshop-postgres \ -e POSTGRES_DB=openshop \ -e POSTGRES_USER=openshop \ -e POSTGRES_PASSWORD=openshop \ -p 5432:5432 \ -d postgres:17-alpineWait until this command reports that PostgreSQL is accepting connections:
docker exec openshop-postgres pg_isready -U openshop -d openshopOn later sessions, reuse the container with docker start openshop-postgres.
The username and password above are local development values.
2. Create the project
Section titled “2. Create the project”pnpm dlx openshop init my-appcd my-apppnpm installThe CLI prints Created my-app. Keep this terminal in my-app for the remaining
commands. The generated project pins the OpenShop version that created it.
Open these three files:
| File | What it contains |
|---|---|
openshop.app.ts |
The registered warehouse provider |
flows/syncOrders.ts |
The sample flow with fetch-orders and send-to-warehouse steps |
openshop.config.ts |
The flow registry and a sample cron schedule |
There is also a drizzle/ directory containing the initial database migration.
The project structure reference describes the
other files when you need them.
3. Configure local storage
Section titled “3. Configure local storage”Create .env in my-app with this content:
DATABASE_URL=postgresql://openshop:openshop@localhost:5432/openshopAppend a new encryption key once:
printf 'ENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" >> .envKeep this key for subsequent sessions. The generated .gitignore excludes
.env. openshop dev reads it automatically.
Migration commands use the process environment, so export the database URL in this terminal before applying the initial migration:
export DATABASE_URL=postgresql://openshop:openshop@localhost:5432/openshoppnpm run db:migratepnpm run db:statusThe status output should report no pending migration. If the database connection fails, check that the container is ready and that the exported URL matches it.
4. Keep the first run manual
Section titled “4. Keep the first run manual”Replace openshop.config.ts with:
import { app } from '#app'import { syncOrders } from '#flows/syncOrders'
export default app.defineConfig({ flows: { syncOrders }, crons: [],})We will run the flow from the admin UI after saving its provider credentials. This removes the generated schedule for now. The flow guide adds a schedule that explicitly targets installed shops.
5. Link and start the Shopify app
Section titled “5. Link and start the Shopify app”shopify auth loginshopify app config linkFollow the prompts to select your organization and create or select a development
app. Check that the linked Shopify app configuration requests read_products
and read_orders, as the template does. Then start development:
pnpm run shopifyFollow Shopify CLI’s prompts to select your development store. This command starts the tunnel and OpenShop together. OpenShop generates GraphQL types, synchronizes the local database schema, and starts the API, admin UI, scheduler, and worker. Leave it running.
Open the preview URL printed by Shopify CLI (or press p) and install the app.
You should see the OpenShop home page inside Shopify admin. Opening
http://localhost:3000 directly does not provide the Shopify session the
embedded UI needs.
6. Configure the sample provider
Section titled “6. Configure the sample provider”In Providers, open warehouse and save:
| Field | Development value |
|---|---|
| API URL | https://example.com |
| API Key | development-only |
Run the provider check. It should succeed: the generated checker only verifies
that both values are present. The generated push method only logs a message;
these placeholder credentials never contact example.com.
7. Run the integration
Section titled “7. Run the integration”In Flows, open syncOrders, trigger a run, and enter:
{ "limit": 10 }Open the run. It should reach completed and show two completed steps:
fetch-orders and send-to-warehouse. Its logs should include Orders synced.
The count may be zero on a new development store.
If it fails, open the failed step and its logs. A Shopify authorization error
usually means the app installation or read_orders permission needs attention;
see troubleshooting. A run that stays pending
needs a running worker; in this lesson, pnpm run shopify starts it for you.
In a second terminal, from my-app, check the project:
pnpm run lintThis generates GraphQL types, checks TypeScript, and runs ESLint.
What you built
Section titled “What you built”You now have an installed app, saved provider configuration, and a completed background job with inspectable steps. Continue with Build a checkpointed flow to write a small flow of your own and watch it resume after a pause.
When you finish the session, stop Shopify CLI with Ctrl+C. You can stop the
local database with docker stop openshop-postgres and start the same container
next time without recreating it.