Local development
Everything runs on your Mac for v1. No VPS, no Vercel deploy, no DNS.
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Node | 22.x (LTS) | Managed by nvm is fine |
| npm | 10+ | Comes with Node |
| Docker Desktop | recent | Hosts the Supabase OSS stack |
| Self-hosted Supabase | running | At /Users/angelom/apps/supabase-local/, see Phase 1 of the self-hosted Supabase plan |
| Supabase CLI | latest | brew install supabase/tap/supabase (only needed for db diff, optional for v1) |
Confirm Supabase is running:
docker ps --filter "name=supabase" --format "{{.Names}}"
You should see supabase-kong, supabase-db, supabase-auth, supabase-realtime, and friends.
First-time setup
git clone git@github.com:amarasa/href-crm.git
cd href-crm
npm install
cp .env.example .env.local # then fill in keys (see below)
.env.local needs:
| Variable | Source |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | http://localhost:8000 for the local Mac stack |
NEXT_PUBLIC_SUPABASE_ANON_KEY | From /Users/angelom/apps/supabase-local/.secrets-backup.txt (ANON_KEY) |
SUPABASE_SERVICE_ROLE_KEY | Same file (SERVICE_ROLE_KEY) |
RESEND_API_KEY | The hrefcreative.com Resend account's key |
RESEND_FROM_EMAIL | crm@hrefcreative.com |
RESEND_REPLY_TO | support@hrefcreative.com |
NEXT_PUBLIC_APP_DOMAIN | localtest.me:3001 for local |
NEXT_PUBLIC_APP_PROTOCOL | http for local |
Apply the schema
The Supabase CLI's db push is designed for the cloud workflow. For our self-hosted stack, apply migrations directly via psql inside the Postgres container:
docker exec -i supabase-db psql -U postgres -d postgres < supabase/migrations/0001_initial_schema.sql
Re-running an already-applied migration will throw "already exists" errors; that's fine, the existing tables stay put. To start fresh, drop the schema first:
docker exec -i supabase-db psql -U postgres -d postgres -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
(Be careful: that wipes everything in public.)
Provision a test tenant
npm run provision-tenant -- acme "Acme Family Law" you@example.com
The output prints the tenant ID, the one-time API key, the splash URL, the admin invite URL, and a curl example for testing the intake API. Save the API key somewhere; it cannot be retrieved later.
Run the dev server
npm run dev
Pinned to port 3001 (HQ owns 3000). On first run, Next downloads compilers, installs Turbopack, etc. Subsequent starts are sub-second.
Visit:
http://acme.localtest.me:3001/— Acme's branded splashhttp://acme.localtest.me:3001/invite/<token>— the invite acceptance page (URL printed byprovision-tenant)http://localtest.me:3001/docs— the docs site (this page lives there)
Common commands
npm run dev # dev server (port 3001, Turbopack)
npm run typecheck # tsc --noEmit
npm run test # Vitest (unit)
npm run test:e2e # Playwright (e2e)
npm run provision-tenant -- <slug> "<name>" <admin-email> # create a tenant
Reconfiguring Supabase Auth SMTP
The CRM relies on the local Supabase Auth sending magic-link emails through Resend. The relevant settings live in /Users/angelom/apps/supabase-local/docker/.env:
SITE_URL=http://localtest.me:3001
ADDITIONAL_REDIRECT_URLS=http://*.localtest.me:3000,http://*.localtest.me:3001,...
SMTP_ADMIN_EMAIL=crm@hrefcreative.com
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASS=<resend-api-key>
SMTP_SENDER_NAME=HREF CRM
After editing, restart the auth container:
cd /Users/angelom/apps/supabase-local/docker
docker compose up -d auth
(up -d auth recreates the auth container with the new env vars; restart auth would not pick them up.)
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Splash 404s with no banner | Subdomain does not resolve to a tenant | provision-tenant first, or check the slug spelling |
| "This account is not a member of this workspace" | User signed up but no tenant_members row | Use the invite link, do not just sign up |
| Magic link email never arrives | Supabase Auth SMTP misconfigured | Check docker logs supabase-auth for SMTP errors |
| Dev server picks port 3002 | Port 3001 already in use | pkill -f "next dev" and start fresh |
localtest.me does not resolve | Some VPNs/DNS hijack *.me | Add 127.0.0.1 localtest.me acme.localtest.me to /etc/hosts as a fallback |
unstable_cache returns stale tenant | Cached value from lookupTenant is held for 60 seconds | Wait, or call revalidateTag("tenants") after mutations |
Resetting state
To wipe the CRM data without nuking the whole Supabase OSS stack:
TRUNCATE tenants RESTART IDENTITY CASCADE;
Run that via Supabase Studio (http://localhost:8000) or docker exec -i supabase-db psql -U postgres -d postgres. Cascades through every related table.