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.
Prerequisites
Section titled “Prerequisites”Mise + env vars
Section titled “Mise + env vars”Each client needs a .mise.local.toml at the repo root with:
[env]CLIENT_SLUG = "osco" # client slugTHEME_ID = "154775126243" # Shopify theme numeric IDCLOUDFLARE_ACCOUNT_ID = "..." # CF account for KV/D1 API accessCLOUDFLARE_API_TOKEN = "..." # CF API token with KV + D1 permissionsMise doesn’t auto-inject into subshells. Load env vars before running any script:
eval "$(mise env)"Build the registry shim
Section titled “Build the registry shim”The renderer imports a generated registry.ts that maps section/snippet names to transpiled render functions. This file doesn’t exist until you build:
pnpm build:shim # Transpiles all .liquid → .ts, generates registry (~2-3 min)Without this step, pnpm dev fails with Could not resolve "../registry".
Data architecture
Section titled “Data architecture”Local dev needs data seeded into two Miniflare instances:
Renderer (cf-client-site):
- COMPONENTS KV — transpiled section/snippet render functions (produced by
build:shim+seed:kv) - CLIENTS config — client config, features, domain mappings, fetch manifest (from D1 database)
Dashboard (cf-app):
- CONFIG_DB D1 — client config, deploys, domains, builds
- 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.
The preview layer
Section titled “The preview layer”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 environmentThe 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.
Sync production to preview
Section titled “Sync production to preview”pnpm sync:preview # sync CLIENT_SLUGpnpm sync:preview -- --slug=osco # sync a specific clientThis 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.
Pull preview to local
Section titled “Pull preview to local”pnpm pull:preview # pull CLIENT_SLUGpnpm pull:preview -- --slug=osco # pull a specific clientThis 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).
Direct production access (escape hatch)
Section titled “Direct production access (escape hatch)”If you need to bypass preview and pull directly from production KV + D1:
pnpm pull:prod # pull from production KV + D1Same data sources, no preview layer in between. Prefer pull:preview for normal dev.
Startup sequence
Section titled “Startup sequence”Existing client (normal dev)
Section titled “Existing client (normal dev)”eval "$(mise env)"pnpm dev # fast-start with existing local Miniflare stateDefault (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:
pnpm dev --reset # wipe state → fetch → seed renderer + dashboardTo also wipe build output:
pnpm dev --clean # implies --reset, also wipes .build/New client (first time)
Section titled “New client (first time)”eval "$(mise env)"pnpm build:shim # generate registry (~2-3 min)pnpm sync:preview # snapshot production → previewpnpm dev --reset # wipe + fetch + seed from CF APIIf the client has product/collection data that hasn’t been seeded to production yet, you also need:
pnpm seed:kv --slug=fmdf --manifest='{"products":{"metafields":[{"namespace":"farmd","key":"product_details"}]}}'See Product data below.
| Service | Default | Worktree (offset 10) |
|---|---|---|
| Renderer | 8787 | 8797 |
| Dashboard | 3000 | 3010 |
Ports are offset by WT_PORT_OFFSET in .mise.local.toml. All scripts auto-detect the offset.
Worktrees
Section titled “Worktrees”Git worktrees let you run multiple branches simultaneously with isolated ports, KV data, and Miniflare state. Managed via just wt:
Create a new worktree
Section titled “Create a new worktree”just wt new my-featureThis:
- Creates the worktree at
../threshold-platform.wt/my-featurewith a new branch - Copies
.mise.local.tomlfrom main and adds a uniqueWT_PORT_OFFSET - Runs
pnpm install - Starts a tmux dev session (renderer + dashboard)
- Opens Cursor at the worktree path
Manage worktrees
Section titled “Manage worktrees”just wt dev my-feature # start dev server in tmuxjust wt attach my-feature # attach to tmux session (view logs)just wt stop my-feature # stop the dev serverjust wt open my-feature # open in Cursorjust wt list # list all worktrees with ports + statusjust wt rm my-feature # remove worktree (keeps branch)Worktree port isolation
Section titled “Worktree port isolation”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"Product data (fetch manifest)
Section titled “Product data (fetch manifest)”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:
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:
grep -roh 'product?\.\w\+?\.\w\+?\.\w\+' .build/{slug}-{themeId}/ | sort -uLook 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.
D1 config database
Section titled “D1 config database”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.
Common issues
Section titled “Common issues”| Symptom | Fix |
|---|---|
Could not resolve "../registry" | Run pnpm build:shim |
pnpm dev can’t find THEME_ID | Run eval "$(mise env)" |
No preview data for slug 'X' | Run pnpm sync:preview first |
D1 Cannot read properties of undefined | Missing [[d1_databases]] in wrangler.toml |
| RENDER_ERROR / wrong template loading | Product data not in KV — run pnpm seed:kv --manifest=... |
| Miniflare serving stale pages | rm -rf packages/cf-client-site/.wrangler/state/v3/cache |
| Seed endpoint 500 | D1 tables don’t exist yet — auto-migration runs on first seed |
Agent workflow
Section titled “Agent workflow”When Claude Code agents work on this codebase:
- Check
.mise.local.toml— ensureCLIENT_SLUGandTHEME_IDare set - Load env:
eval "$(mise env)" - First time:
pnpm build:shimthenpnpm sync:preview - Start dev:
pnpm dev(fast-start) orpnpm dev --reset(wipe + re-fetch + seed) - After transpiler changes:
pnpm build:shimto regenerate the registry, then the liquid watcher handles hot reload - After production deploy:
pnpm sync:previewto refresh the preview snapshot, thenpnpm dev --reset - Type checking:
pnpm typecheckrunstsc --noEmitagainstscripts/**/*.ts
For parity work, see AGENTS.md — the canonical agent policy for transpiler fixes and section-by-section parity loops.
Command reference
Section titled “Command reference”| Command | Description |
|---|---|
pnpm dev | Fast-start renderer + dashboard (existing local state) |
pnpm dev --reset | Wipe state → fetch from CF API → seed renderer + dashboard |
pnpm dev --clean | Same as --reset + wipes build output |
pnpm build:shim | Transpile .liquid and generate registry |
pnpm typecheck | Run tsc --noEmit against scripts/**/*.ts |
pnpm sync:preview | Snapshot production KV + D1 → preview |
pnpm pull:preview | Pull preview → local Miniflare (renderer + dashboard) |
pnpm pull:prod | Pull production KV + D1 → local Miniflare (renderer + dashboard) |
pnpm seed:kv | Seed 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 list | List all worktrees with ports and status |
just wt rm <name> | Remove a worktree |