Skip to content

Dev Environment Setup

Everything you need to get a local dev environment running against a client’s Shopify theme data. This covers fresh machine setup, worktree-based workflows, and the preview data sync layer.

Each client needs a .mise.local.toml at the repo root with:

[env]
CLIENT_SLUG = "osco" # client slug
THEME_ID = "154775126243" # Shopify theme numeric ID
CLOUDFLARE_ACCOUNT_ID = "..." # CF account for KV/D1 API access
CLOUDFLARE_API_TOKEN = "..." # CF API token with KV + D1 permissions

Mise doesn’t auto-inject into subshells. Load env vars before running any script:

Terminal window
eval "$(mise env)"

The renderer imports a generated registry.ts that maps section/snippet names to transpiled render functions. This file doesn’t exist until you build:

Terminal window
pnpm build:shim # Transpiles all .liquid → .ts, generates registry (~2-3 min)

Without this step, pnpm dev fails with Could not resolve "../registry".

Local dev needs data seeded into two Miniflare instances:

Renderer (cf-client-site):

  1. COMPONENTS KV — transpiled section/snippet render functions (produced by build:shim + seed:kv)
  2. CLIENTS config — client config, features, domain mappings, fetch manifest (from D1 database)

Dashboard (cf-app):

  1. CONFIG_DB D1 — client config, deploys, domains, builds
  2. LAYERKICK_KV — metrics data (lighthouse scores, deploy performance)

In production these live in Cloudflare KV + D1. pnpm dev --reset, pull:prod, and pull:preview seed both targets.

Rather than pulling directly from production (fragile — data can change mid-session), we use a preview snapshot layer:

Production KV + D1
│ pnpm sync:preview
Preview KV + Preview D1 ◄── stable snapshot
│ pnpm pull:preview
Local Miniflare ◄── your dev environment

The preview namespace acts as a stable, versioned copy of production data. You sync it once, then pull from it repeatedly without worrying about production changes.

Terminal window
pnpm sync:preview # sync CLIENT_SLUG
pnpm sync:preview -- --slug=osco # sync a specific client

This command:

  • Self-bootstraps the Preview D1 database (creates it on first run, caches the ID in .preview-config.json)
  • Copies the production D1 schema dynamically (no hardcoded DDL)
  • Syncs the client row, domains, and deploys from production D1
  • Syncs all {slug}:* keys from production COMPONENTS KV to PREVIEW KV
  • Cleans up stale preview keys that no longer exist in production
  • Writes sync metadata (_sync:meta:{slug}) for staleness tracking

When to run: After deploying transpiled changes to production KV, or when onboarding a new client. You don’t need to run this daily — only when production data changes.

Crash safety: Data is written before stale keys are cleaned up. If the script crashes mid-run, preview has a superset of production data. Just re-run.

Terminal window
pnpm pull:preview # pull CLIENT_SLUG
pnpm pull:preview -- --slug=osco # pull a specific client

This command:

  • Lists PREVIEW KV keys for the slug — fails loudly if empty (no silent fallback to production)
  • Logs staleness: “Preview data synced 2h ago (2026-03-01T14:30:00Z)”
  • Fetches all component entries from PREVIEW KV
  • Reads client config from Preview D1 and reconstructs CLIENTS-binding entries
  • Seeds everything into the running local Miniflare via /__dev/seed

Requires: The renderer must be running (pnpm dev starts it).

If you need to bypass preview and pull directly from production KV + D1:

Terminal window
pnpm pull:prod # pull from production KV + D1

Same data sources, no preview layer in between. Prefer pull:preview for normal dev.

Terminal window
eval "$(mise env)"
pnpm dev # fast-start with existing local Miniflare state

Default (no flags) uses existing local data — no fetch or seed. This is the fastest way to restart.

To wipe and re-seed everything from the CF API:

Terminal window
pnpm dev --reset # wipe state → fetch → seed renderer + dashboard

To also wipe build output:

Terminal window
pnpm dev --clean # implies --reset, also wipes .build/
Terminal window
eval "$(mise env)"
pnpm build:shim # generate registry (~2-3 min)
pnpm sync:preview # snapshot production → preview
pnpm dev --reset # wipe + fetch + seed from CF API

If the client has product/collection data that hasn’t been seeded to production yet, you also need:

Terminal window
pnpm seed:kv --slug=fmdf --manifest='{"products":{"metafields":[{"namespace":"farmd","key":"product_details"}]}}'

See Product data below.

ServiceDefaultWorktree (offset 10)
Renderer87878797
Dashboard30003010

Ports are offset by WT_PORT_OFFSET in .mise.local.toml. All scripts auto-detect the offset.

Git worktrees let you run multiple branches simultaneously with isolated ports, KV data, and Miniflare state. Managed via just wt:

Terminal window
just wt new my-feature

This:

  1. Creates the worktree at ../threshold-platform.wt/my-feature with a new branch
  2. Copies .mise.local.toml from main and adds a unique WT_PORT_OFFSET
  3. Runs pnpm install
  4. Starts a tmux dev session (renderer + dashboard)
  5. Opens Cursor at the worktree path
Terminal window
just wt dev my-feature # start dev server in tmux
just wt attach my-feature # attach to tmux session (view logs)
just wt stop my-feature # stop the dev server
just wt open my-feature # open in Cursor
just wt list # list all worktrees with ports + status
just wt rm my-feature # remove worktree (keeps branch)

Each worktree gets a unique port offset (auto-assigned as (worktree_count - 1) * 10):

main → Renderer: 8787, Dashboard: 3000 (offset 0)
my-feature → Renderer: 8797, Dashboard: 3010 (offset 10)
another-branch → Renderer: 8807, Dashboard: 3020 (offset 20)

The offset is stored in each worktree’s .mise.local.toml:

WT_PORT_OFFSET = "10"

pull:preview mirrors what’s in production KV. For clients that need product metafield data seeded from scratch, use seed:kv with a fetch manifest:

Terminal window
pnpm seed:kv --slug=fmdf --manifest='{"products":{"metafields":[{"namespace":"farmd","key":"product_details"},{"namespace":"farmd","key":"product_guarantee"}]}}'

Building the manifest: Grep transpiled sections for metafield access patterns:

Terminal window
grep -roh 'product?\.\w\+?\.\w\+?\.\w\+' .build/{slug}-{themeId}/ | sort -u

Look for product?.metafields?.{namespace}?.{key} and add each pair. Use "reference": true for file_reference or media_image_reference types.

Without product data: Template suffix resolution fails (falls back to default product template), and metafield-gated sections render empty.

The renderer reads client config from a D1 database (CONFIG_DB binding). The wrangler.toml needs D1 bindings for both top-level and [env.dev]:

[[d1_databases]]
binding = "CONFIG_DB"
database_name = "layerkick-config-local"
database_id = "local-config-dev"

The /__dev/seed endpoint auto-creates tables on first seed.

SymptomFix
Could not resolve "../registry"Run pnpm build:shim
pnpm dev can’t find THEME_IDRun eval "$(mise env)"
No preview data for slug 'X'Run pnpm sync:preview first
D1 Cannot read properties of undefinedMissing [[d1_databases]] in wrangler.toml
RENDER_ERROR / wrong template loadingProduct data not in KV — run pnpm seed:kv --manifest=...
Miniflare serving stale pagesrm -rf packages/cf-client-site/.wrangler/state/v3/cache
Seed endpoint 500D1 tables don’t exist yet — auto-migration runs on first seed

When Claude Code agents work on this codebase:

  1. Check .mise.local.toml — ensure CLIENT_SLUG and THEME_ID are set
  2. Load env: eval "$(mise env)"
  3. First time: pnpm build:shim then pnpm sync:preview
  4. Start dev: pnpm dev (fast-start) or pnpm dev --reset (wipe + re-fetch + seed)
  5. After transpiler changes: pnpm build:shim to regenerate the registry, then the liquid watcher handles hot reload
  6. After production deploy: pnpm sync:preview to refresh the preview snapshot, then pnpm dev --reset
  7. Type checking: pnpm typecheck runs tsc --noEmit against scripts/**/*.ts

For parity work, see AGENTS.md — the canonical agent policy for transpiler fixes and section-by-section parity loops.

CommandDescription
pnpm devFast-start renderer + dashboard (existing local state)
pnpm dev --resetWipe state → fetch from CF API → seed renderer + dashboard
pnpm dev --cleanSame as --reset + wipes build output
pnpm build:shimTranspile .liquid and generate registry
pnpm typecheckRun tsc --noEmit against scripts/**/*.ts
pnpm sync:previewSnapshot production KV + D1 → preview
pnpm pull:previewPull preview → local Miniflare (renderer + dashboard)
pnpm pull:prodPull production KV + D1 → local Miniflare (renderer + dashboard)
pnpm seed:kvSeed theme config + business data from Shopify API
just wt new <name>Create worktree with isolated ports + tmux session
just wt dev <name>Start dev server in worktree tmux session
just wt listList all worktrees with ports and status
just wt rm <name>Remove a worktree