Local development setup
This guide walks you through setting up the full Bondery development environment. Follow the dependency chain: Postgres → Redis → (optional) SeaweedFS → API → apps.
Environment variables: edit root .env.local once, then sync — see Environment configuration.
pnpm install
pnpm run setup:dev
# edit .env.local (OAuth / optional integrations)
pnpm run start:postgres
pnpm run start:redis
pnpm run start:seaweedfs # required for avatar / logo uploads
pnpm run env
pnpm run dev:webapp-api # API :26631 + webapp :26632On first API start in development, runDevelopmentBootstrap() runs the same pipeline as production api.pre_start (migrations, SQL functions, OAuth clients, platform admins, storage buckets) unless you set BONDERY_DEV_SKIP_RELEASE_MIGRATE or BONDERY_DEV_SKIP_STORAGE_BUCKETS. You do not need to run db:migrate:deploy and provision:oauth-clients manually before every pnpm run dev.
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Node.js | 26 | Declared in root package.json devEngines.runtime (pnpm downloads on install); .nvmrc is optional for nvm use / fnm use |
| npm | ≥ 11 | Bundled with Node.js |
| Docker | latest | Local Postgres, Redis, and SeaweedFS |
For mobile development you also need Expo tooling (installed via root pnpm install) and a native toolchain: iOS — Xcode + Simulator or a physical device; Android — Android Studio + emulator or USB debugging.
Clone and install
git clone https://github.com/usebondery/bondery.git
cd bondery
nvm use # optional — reads .nvmrc (Node 26); pnpm also installs Node from devEngines.runtime
pnpm install
pnpm run setup:devThis installs dependencies for all apps and packages in the monorepo, then creates root .env.local from the example.
Postgres database
The local Postgres instance is the foundation everything else connects to. Start it before running the API.
Environment variables
Set BONDERY_PRIVATE_POSTGRES_PASSWORD in root .env.local (pnpm run env derives DATABASE_URL into api/db). See .env.local.example. OAuth client ids/secrets for Better Auth live under BONDERY_PRIVATE_AUTH_*. After editing, run pnpm run env to sync app env files.
No OAuth credentials yet? The stack will still start — email-based flows may be limited, but you can develop against the API. Social login buttons need GitHub/LinkedIn app credentials.
Start Postgres
From the repo root (reads BONDERY_PRIVATE_POSTGRES_PASSWORD from .env.local):
pnpm run start:postgresPostgres listens on host port 54322. pnpm run env writes DATABASE_URL to api/db using BONDERY_PRIVATE_POSTGRES_PASSWORD from root .env.local.
Stop with pnpm run stop:postgres.
Migrations (manual / one-off)
The API dev server runs migrations on boot. Use these when you author schema changes or need a clean reset:
| Command | Description |
|---|---|
pnpm run db:migrate:deploy | Apply pending Prisma migrations |
pnpm run db:migrate:dev | Author new migrations interactively (requires shadow DB with PostGIS) |
pnpm run db:studio | Open Prisma Studio against DATABASE_URL |
pnpm run db:functions | Apply raw SQL functions (included in dev bootstrap) |
pnpm run provision:oauth-clients | Upsert first-party OAuth clients (included in dev bootstrap) |
For a full auth walkthrough, see Authentication.
Mobile sync (Postgres changelog)
Mobile offline sync uses custom pull/bootstrap endpoints on the API (GET /api/sync/bootstrap, GET /api/sync/pull) backed by sync_change_log. No separate sync service is required beyond Postgres + API.
Object storage (SeaweedFS)
Contact avatars and cached LinkedIn logos use the S3-compatible SeaweedFS gateway. Credentials live only in root .env.local (BONDERY_PRIVATE_S3_ACCESS_KEY_ID / SECRET); the seaweedfs-s3 container renders its config from those vars at startup.
pnpm run start:seaweedfs # S3 gateway on http://127.0.0.1:8333
pnpm run setup:seaweedfs # optional — API dev boot also ensures bucketsOptional liveness smoke: curl -s http://127.0.0.1:8333/status
S3-ready (unsigned ListBuckets): curl -sS -D- http://127.0.0.1:8333/ — expect HTTP 403 and S3 Error XML. Empty 200 on /status is not sufficient.
Stop with pnpm run stop:seaweedfs.
Set in .env.local (see .env.local.example):
BONDERY_PUBLIC_STORAGE_URL=http://127.0.0.1:8333
BONDERY_PRIVATE_S3_ENDPOINT=http://127.0.0.1:8333Avatar previews need public bucket policy. Setup applies anonymous GetObject on avatars. If uploads succeed but images 404/403 in the browser, run pnpm run setup:seaweedfs or restart the API (dev bootstrap).
linkedin-logos bucket: stores company/school logos fetched during LinkedIn import ({userId}/{linkedinId}.jpg). Created alongside avatars by ensure-storage-buckets on API startup.
API server (apps/api)
Environment variables
Prefer root .env.local + pnpm run env (see Environment configuration). That writes apps/api/.env.development.local.
Required secrets include BONDERY_PRIVATE_BETTER_AUTH_SECRETS (format 1:<secret>, each secret ≥32 characters) and BONDERY_PRIVATE_API_KEY_PEPPER — see API keys below.
For mobile sync, set CORS for Expo web if needed:
BONDERY_PUBLIC_EXTRA_ALLOWED_ORIGINS=http://localhost:26634Redis
Redis powers rate limiting, Better Auth secondary storage (bondery:auth:*), mobile sync wake (pub/sub), and WebSocket ticket storage in the API. Required for local API development.
| Mode | BONDERY_PRIVATE_REDIS_URL | When to use |
|---|---|---|
| Local Docker (required) | redis://127.0.0.1:26636 | Normal local API work |
From the repo root (Docker must be running):
pnpm run start:redisStop with pnpm run stop:redis. Verify: docker exec bondery-redis redis-cli ping → PONG.
Wire the API in apps/api/.env.development.local:
BONDERY_PRIVATE_REDIS_URL="redis://127.0.0.1:26636"BONDERY_PRIVATE_REDIS_URL is required in development and production. See Installation.
API keys (long-lived integration tokens)
| Variable | Purpose |
|---|---|
BONDERY_PRIVATE_API_KEY_PEPPER | Server secret mixed into API key hashes before storage. Use a random value at least 32 characters (e.g. openssl rand -hex 32). |
Validated API keys resolve directly to a userId in application code — no JWT minting step. See API authentication.
Keep BONDERY_PRIVATE_API_KEY_PEPPER stable. If you change it after creating keys, existing keys stop working — create new keys after rotating the pepper.
Start
cd apps/api
pnpm run devThe API server starts on port 26631 with hot reload enabled.
Web application (apps/webapp)
Environment variables
Filled by pnpm run env into apps/webapp/.env.development.local — see Environment configuration.
The webapp is itself a confidential OAuth client of the API's authorization server. See Authentication for env vars, bootstrap commands, and troubleshooting.
Start
cd apps/webapp
pnpm run devThe webapp starts on port 26632. Open http://localhost:26632 in your browser.
Or from the repo root: pnpm run dev:webapp-api (API + webapp together).
Marketing website (apps/website)
Uses BONDERY_PUBLIC_WEBAPP_URL / BONDERY_PUBLIC_WEBSITE_URL from env sync.
Start
cd apps/website
pnpm run devThe website starts on port 26630. Open http://localhost:26630 in your browser.
Chrome extension (apps/chrome-extension)
Environment variables
Filled by pnpm run env. See Authentication for BONDERY_PUBLIC_OAUTH_CLIENT_ID, BONDERY_INFRA_CHROME_EXTENSION_ID, and provisioning steps.
See Chrome Extension OAuth Setup when you see redirect URI mismatch errors (common after prisma migrate reset or on a new machine).
Start
cd apps/chrome-extension
pnpm exec wxt prepare # only needed once after install
pnpm exec wxt # or: pnpm run devOpen chrome://extensions in Chrome, enable Developer mode, click Load unpacked, and select the generated dist/chrome-mv3-dev folder.
Mobile application (apps/mobile)
Run the mobile app against your local API and Postgres stack. Requires Postgres migrations and a running API first.
Environment variables
Filled by pnpm run env into apps/mobile/.env.local (BONDERY_PUBLIC_*). Expo loads those into app.config.ts extra.
Physical device on the same Wi‑Fi
In root .env.local, replace 127.0.0.1 / localhost with your machine's LAN IP (e.g. 192.168.1.42) for BONDERY_PUBLIC_API_URL, then pnpm run env. The phone cannot reach your laptop's loopback interface.
Ensure the API is reachable on the LAN and your firewall allows inbound connections on port 26631.
Physical device over Tailscale (recommended)
Tailscale gives your dev machine a stable 100.x.x.x address on your tailnet — works off Wi‑Fi and avoids public tunnel interstitials.
-
Install Tailscale on your dev machine and phone; sign in to the same tailnet.
-
On the dev machine, run
tailscale ip -4and note the address (e.g.100.64.0.5). -
For phone testing only, temporarily set in root
.env.local:BONDERY_PUBLIC_API_URL=http://100.64.0.5:26631 -
Run
pnpm run envand restart the API. -
Revert
BONDERY_PUBLIC_API_URLtohttp://localhost:26631when you return to laptop web dev.
Start Metro with LAN binding so the phone can load the JS bundle over Tailscale:
cd apps/mobile
pnpm exec expo start --host lanRegister http://<tailscale-ip>:26631/auth/callback/github in your GitHub OAuth app when testing social login on a physical device.
WSL2: Ensure the API listens on 0.0.0.0 (not only 127.0.0.1). Tailscale on the Windows host routes to WSL forwarded ports when using WSL2 networking.
Android emulator note
On Android emulators, 127.0.0.1 and localhost in env vars are automatically rewritten to 10.0.2.2 (the host loopback from the emulator).
Start
Option A — API + mobile from repo root:
pnpm run dev:mobileOption B — mobile only (with Postgres and API already running):
cd apps/mobile
pnpm run devThen press i for iOS Simulator, a for Android emulator, or scan the QR code with Expo Go / a dev client.
Native dev builds
For SQLite sync and background tasks, use a development build instead of Expo Go:
cd apps/mobile
pnpm run dev:prebuild # regenerates android/ and ios/ — only when native deps change
pnpm run android:dev # USB device or emulator
pnpm run ios # iOS Simulator or deviceVerify sync
- Sign in with GitHub or LinkedIn (same providers as the webapp).
- Wait for the initial sync banner to clear — contacts should match your account.
- Toggle airplane mode, edit a contact, then reconnect — changes should drain from the outbox and appear on web.
If sync requests return 426, protocol or SQLite schema versions are mismatched — rebuild the app after pulling API/mobile changes.
If bootstrap or pull return 401, confirm the mobile session token matches your local API and Better Auth configuration.
See also Sync architecture (mobile) and apps/mobile/README.md.
Running everything at once
Before using these commands, complete environment setup in the steps above.
From the repo root:
# 1. Infrastructure
pnpm run start:postgres
pnpm run start:redis
pnpm run start:seaweedfs
# 2. App dev
pnpm run env
pnpm run dev:webapp-api
# Or: api + webapp + website + chrome-extension
pnpm run dev:extension
# Or: api + mobile
pnpm run dev:mobileThese scripts are defined in the root package.json and orchestrated by Turborepo (turbo run). Persistent app processes (next dev, tsx watch) stay up; package tsc --watch runs beside them. Do not wrap those servers in turbo watch — that kills Next on any tracked file write.
Stripe billing (optional)
Inbound Stripe events (portal cancel, payment failure, trial-ending email) do not reach 127.0.0.1. After Stripe CLI is installed:
stripe login # once per machine — not run by our scripts
pnpm run setup:stripe # write CLI webhook secret → .env.local → pnpm run env
# restart the API
pnpm run dev:stripe # second terminal: forward to http://127.0.0.1:26631/webhooks/stripeSee Environment configuration.
Production builds
pnpm exec turbo build --filter=api
pnpm exec turbo build --filter=webapp
pnpm exec turbo build --filter=website
pnpm exec turbo build --filter=chrome-extensionShortcuts: pnpm run build, pnpm run build:api, build:webapp, build:website.
Local dev watch
pnpm run dev # turbo run dev — all apps with a dev script
pnpm run dev:webapp-api # webapp + api (usual local stack)
pnpm run dev:api # api only
pnpm run dev:webapp # webapp only
pnpm run dev:mobile # api + mobile
pnpm run dev:emails # React Email preview only (port 26639)
pnpm run build:packages # one-shot package compileLint the whole repo from the root: pnpm run lint (Biome).
Quick reference
Port registry: architecture.mdx.
| App | Source | Dev port | Start command |
|---|---|---|---|
| Postgres | deploy/bondery/docker-compose.dev-db.yml | 54322 | pnpm run start:postgres |
| Redis | deploy/bondery/docker-compose.dev-redis.yml | 26636 | pnpm run start:redis |
| SeaweedFS | deploy/bondery/docker-compose.seaweedfs.yml | 8333 | pnpm run start:seaweedfs |
api | apps/api | 26631 | pnpm run dev:api or pnpm run dev:webapp-api |
webapp | apps/webapp | 26632 | pnpm run dev:webapp or pnpm run dev:webapp-api |
website | apps/website | 26630 | pnpm run dev |
mobile | apps/mobile | 26634 (Expo Metro) | pnpm run dev:mobile |
chrome-extension | apps/chrome-extension | 26633 (WXT HMR) | pnpm exec wxt |
@bondery/emails preview | packages/emails | 26639 | pnpm run preview |