EmofyEmofy Developers
Getting Started

Local Development Setup

From a fresh clone to a running, seeded local stack — prerequisites, environment, and your first health check.

This page takes you from a fresh clone to a running local stack. Budget about 15 minutes once the prerequisites are installed.

Prerequisites

ToolVersionNotes
Node.js22 (.nvmrc); >=20 requirednvm install 22 && nvm use 22
pnpm10.33.2 (packageManager in root package.json)npm install -g pnpm@10.33.2
Docker + Composeany recentRuns Postgres, Redis, and Meilisearch locally
Gitany recent

You do not need to install Postgres, Redis, or Convex binaries — Docker Compose and the Convex CLI handle local infrastructure.

The canonical sequence

# 1. Install dependencies (one-time)
pnpm install

# 2. Generate .env from Infisical (the secret SSOT)
pnpm env:pull development

# 3. Create your per-machine override layer
cp .env.local.example .env.local

# 4. Start local infra (Postgres + Redis + Meilisearch)
pnpm db:start

# 5. Apply the schema and seed baseline data
pnpm db:migrate
pnpm seed:bootstrap     # default org + admin user + roles
pnpm seed:dev           # test users, orgs, and demo data

# 6. Configure the local Convex deployment (real-time backend)
pnpm convex:local

# 7. Launch a dev profile
pnpm dev consumer       # account (3002) + consumer (3007) + backend (3006) + convex

Run pnpm dev --list to see all available profiles.

How environment loading works

Local config is a two-layer, first-wins stack:

.env.local   ← per-machine overrides (git-ignored, you create it)
.env         ← generated base (from `pnpm env:pull`, mirrors Infisical)

Both the backend loader and the turbo dev scripts read .env.local before .env, so any key you set locally wins. The schemas themselves are a folder-based SSOT in packages/env/src/ (one schema per deploy target), exposed to apps as @emofy/env. See Deployment & environments.

The localhost cookie footgun

Pin AUTH_COOKIE_DOMAIN= (empty) in .env.local. The generated .env may carry a value like .dev.emofy.net; the browser rejects that parent-domain cookie on localhost and sign-in silently fails. An empty value forces host-only cookies. This blank pin is sanctioned for local dev.

The other values a localhost stack needs (already present in .env.local.example):

VariableLocal valueWhy
BETTER_AUTH_URLhttp://localhost:3002Auth issuer; trusted origins derive from it
NEXT_PUBLIC_AUTH_URLhttp://localhost:3002Client redirect target
NEXT_PUBLIC_BACKEND_URLhttp://localhost:3006Client → local NestJS API
BACKEND_URLhttp://localhost:3006Server-side backend bridge
AUTH_COOKIE_DOMAIN(empty)Host-only cookies on localhost

Dev processes read env at boot. After changing .env or .env.local, restart the dev stack (Ctrl+C, then re-run pnpm dev <profile>).

Verify the stack is up

The backend exposes three health routes:

# Liveness — responds even when dependencies are down
curl http://localhost:3006/health/liveness
# → { "status": "ok", "timestamp": "2026-06-17T12:00:00.000Z" }

# Readiness — fails if any dependency is unhealthy
curl http://localhost:3006/health/readiness
# → { "status": "up", "details": {
#       "postgres_core": { "status": "up" },
#       "postgres_auth": { "status": "up" },
#       "redis": { "status": "up" } } }

Then open the apps:

Ports at a glance

App / servicePort
landing3000
account3002
admin3003
docs3004
template3005
backend3006
consumer3007
marketing3008
Storybook6006
Drizzle Studio (auth / core)4983 / 4984
Postgres / Redis / Meilisearch5432 / 6379 / 7700
Convex (local)auto-assigned by the CLI

Common reset recipes

pnpm dev:reset        # soft: clear test data, keep schema, re-seed
pnpm dev:reset:hard   # drop + recreate tables, re-seed
pnpm dev:cold-start -- --yes   # destroy Docker volumes and rebuild from scratch
pnpm clean:ports      # kill stragglers holding dev ports

Footguns worth knowing early

  • Remote ops need .env.local moved aside. Commands that target remote infra (e.g. seeding/migrating against staging) use the same dotenv injection, so rename .env.local.env.local.bak first, then restore. Destructive remote targets are additionally gated by ADR-014.
  • Native is a separate env pipeline. apps/native loads its own .env* via @expo/env, not the root layering.
  • Search is optional locally. Leave SEARCH_ENABLED=false unless you are working on search; the endpoints degrade gracefully.

Next

On this page