Skip to content

Configuration

OpenShop apps export a configured instance from openshop.config.ts. The instance contains the validated config and can dispatch its registered flows.

import { cron } from 'openshop'
import { app } from '#app'
import { syncOrders } from '#flows/syncOrders'
const openshop = app.defineConfig({
flows: { syncOrders },
crons: [
{ name: 'Quick sync', schedule: cron('*/5 * * * *'), flow: 'syncOrders', input: { limit: 10 }, shops: 'all' },
],
worker: {
concurrency: 5,
},
retryPolicy: {
maxAttempts: 3,
initialIntervalMs: 1000,
backoffCoefficient: 2,
maxIntervalMs: 30000,
},
onError(error, context) {
console.error('[openshop:error]', context, error)
},
})
export default openshop

Import this default export from a public route when you need to enqueue durable work. openshop.dispatchFlow() captures the active config, restricts flowName to registered flow keys, and types input from the selected flow.

Option Type Required Default
providers Record<string, ProviderDefinition> In defineOpenShop() Supplied by the app builder
flows Record<string, FlowDefinition> Yes —
shopify ShopifyConfig No Single app resolved from env and Shopify TOML
functions Record<string, FunctionDefinition> No {}
mcp McpConfig No Core MCP capabilities enabled; no custom capabilities
webhooks Record<string, WebhookDefinition> No {}
crons CronEntry[] No []
experimental OpenShopExperimentalConfig No Custom pages disabled; see Custom admin pages
pages AdminPagesConfig No Every admin page visible
worker Partial<WorkerConfig> No See Worker defaults
retryPolicy Partial<RetryPolicy> No See Retry defaults
onError (error, context?) => void | Promise<void> No No hook

providers and flows are always present, even when empty. defineOpenShop({ providers }) supplies the provider registry to app.defineConfig().

Omitting shopify.apps enables single-app mode:

  • SHOPIFY_API_KEY and SHOPIFY_API_SECRET provide the credentials;
  • HOST, then SHOPIFY_APP_URL, provides the public app URL;
  • shopify.scopes, or the first matching shopify.app*.toml, provides scopes.

For multiple apps, define one entry per stable app handle:

export default app.defineConfig({
shopify: {
scopes: 'read_products,read_orders',
apps: {
retail: {
toml: 'shopify.app.retail.toml',
apiSecret: process.env.SHOPIFY_RETAIL_API_SECRET!,
},
wholesale: {
apiKey: process.env.SHOPIFY_WHOLESALE_API_KEY!,
apiSecret: process.env.SHOPIFY_WHOLESALE_API_SECRET!,
appUrl: process.env.SHOPIFY_WHOLESALE_APP_URL,
},
},
},
flows: {},
})
Field Type Behavior
shopify.scopes string Global non-empty scope string. All configured apps share it.
shopify.apps Record<string, ShopifyAppConfig> Enables multi-app mode. Handles may contain letters, numbers, _, and -.
apps.*.toml string Reads client_id and application_url from this project-relative TOML path. Mutually exclusive with apiKey.
apps.*.apiKey string Client ID for a non-TOML app. Required when toml is omitted.
apps.*.apiSecret string Required non-empty secret. Keep it in an environment variable.
apps.*.appUrl string Optional URL override. TOML entries otherwise use application_url, HOST, then SHOPIFY_APP_URL; non-TOML entries use HOST, then SHOPIFY_APP_URL.

Per-app scopes are not supported. If shopify.scopes is omitted, all configured app TOML files must resolve to the same scope string.

Register flow definitions under stable keys. The object key is the name used by dispatch and crons. Keep it equal to the definition’s name for clear logs. The flow reference owns the definition options, context, checkpoint contract, and timeout behavior.

Field Type Default
name string Omitted; display logs fall back to the flow name
schedule cron expression string Required
flow registered flow key Required
input the selected flow input {}
shops 'global' | 'all' | string | string[] 'global'

pages controls which embedded admin screens appear in the Shopify sidebar and whether their admin API stays reachable. Home is always visible. Flows, Providers, Crons, and Functions are automatically hidden from the sidebar when their corresponding config collection is empty; their direct URLs and admin APIs remain available. MCP stays visible by default because OpenShop provides core MCP capabilities without custom configuration.

Runtime behavior is unchanged: crons still fire, webhooks still run, Shopify Functions still execute, and POST /mcp still follows mcp.enabled.

export default app.defineConfig({
flows: { syncOrders },
pages: {
functions: 'hidden',
mcp: 'disabled',
},
})
Mode Sidebar Direct URL Admin API /api/*
visible (default) Shown when the corresponding config has entries Works 200
hidden Hidden Works 200
disabled Hidden Shows “This page is disabled” 404

Supported keys: flows, providers, crons, functions, mcp. You can set defaults on defineOpenShop({ pages }) and override individual pages in defineConfig().

hidden is navigation-only and must not be used as an access-control boundary. Use disabled when shop staff must not be able to open a page or call its admin API.

The MCP dashboard and protocol are independent:

export default app.defineConfig({
flows: { syncOrders },
pages: { mcp: 'disabled' }, // Block the dashboard and /api/mcp/*.
mcp: { enabled: false }, // Block POST /mcp.
})
  • Set only pages.mcp: 'disabled' to block MCP administration while existing MCP tokens can continue using POST /mcp.
  • Set only mcp.enabled: false to stop the protocol while leaving the dashboard available for token administration.
  • Set both values to disable MCP completely.

app.defineConfig() validates the runtime shape early:

  • cron entries must reference registered flows;
  • providers and functions must declare valid fields;
  • Shopify Function handles must be unique;
  • worker and retry numbers must be positive;
  • flow timeouts and step timeouts must be positive when set;
  • pages keys must be known admin screens and values must be visible, hidden, or disabled.

Cron entries support these shop modes:

Value Behavior
global Run once without a shop context.
all Run once per installed (appHandle, shop) installation.
shop.myshopify.com Run only for one shop.
['a.myshopify.com', 'b.myshopify.com'] Run for selected shops.

worker accepts a partial object. Missing fields use these runtime defaults:

Field Type Default Purpose
concurrency positive integer 5 Maximum active runs in one worker process.
pollIntervalMs positive number 1000 Initial queue polling interval and delay while all slots are occupied.
pollMaxIntervalMs positive number 5000 Maximum empty-queue backoff.
pollBackoffCoefficient positive number 1.5 Multiplier applied after consecutive empty polls.
leaseDurationMs positive number 30000 Claim lease and graceful-stop deadline. Active runs refresh their lease on heartbeat.

The current openshop worker CLI supplies concurrency 5 when its flag is omitted; set --concurrency=N explicitly for a different production value. Other worker settings come from config. Run multiple worker processes to scale horizontally; PostgreSQL claims work with FOR UPDATE SKIP LOCKED.

App-level retryPolicy supplies defaults for registered flows. Flow and dispatch settings override it field by field. See the retry defaults and precedence for the authoritative option table and deadline behavior.

See Environment variables for required values, defaults, .env loading, and resolution order. openshop dev loads the project-root file; build, migration, and production commands use the process environment.