Bondery Docs

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 :26632

On 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

ToolVersionNotes
Node.js26Declared in root package.json devEngines.runtime (pnpm downloads on install); .nvmrc is optional for nvm use / fnm use
npm≥ 11Bundled with Node.js
DockerlatestLocal 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:dev

This 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:postgres

Postgres 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:

CommandDescription
pnpm run db:migrate:deployApply pending Prisma migrations
pnpm run db:migrate:devAuthor new migrations interactively (requires shadow DB with PostGIS)
pnpm run db:studioOpen Prisma Studio against DATABASE_URL
pnpm run db:functionsApply raw SQL functions (included in dev bootstrap)
pnpm run provision:oauth-clientsUpsert 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 buckets

Optional 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:8333

Avatar 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:26634

Redis

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.

ModeBONDERY_PRIVATE_REDIS_URLWhen to use
Local Docker (required)redis://127.0.0.1:26636Normal local API work

From the repo root (Docker must be running):

pnpm run start:redis

Stop with pnpm run stop:redis. Verify: docker exec bondery-redis redis-cli pingPONG.

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)

VariablePurpose
BONDERY_PRIVATE_API_KEY_PEPPERServer 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 dev

The 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 dev

The 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 dev

The 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 dev

Open 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.

Tailscale gives your dev machine a stable 100.x.x.x address on your tailnet — works off Wi‑Fi and avoids public tunnel interstitials.

  1. Install Tailscale on your dev machine and phone; sign in to the same tailnet.

  2. On the dev machine, run tailscale ip -4 and note the address (e.g. 100.64.0.5).

  3. For phone testing only, temporarily set in root .env.local:

    BONDERY_PUBLIC_API_URL=http://100.64.0.5:26631
  4. Run pnpm run env and restart the API.

  5. Revert BONDERY_PUBLIC_API_URL to http://localhost:26631 when 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 lan

Register 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:mobile

Option B — mobile only (with Postgres and API already running):

cd apps/mobile
pnpm run dev

Then 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 device

Verify sync

  1. Sign in with GitHub or LinkedIn (same providers as the webapp).
  2. Wait for the initial sync banner to clear — contacts should match your account.
  3. 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:mobile

These 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/stripe

See 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-extension

Shortcuts: 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 compile

Lint the whole repo from the root: pnpm run lint (Biome).


Quick reference

Port registry: architecture.mdx.

AppSourceDev portStart command
Postgresdeploy/bondery/docker-compose.dev-db.yml54322pnpm run start:postgres
Redisdeploy/bondery/docker-compose.dev-redis.yml26636pnpm run start:redis
SeaweedFSdeploy/bondery/docker-compose.seaweedfs.yml8333pnpm run start:seaweedfs
apiapps/api26631pnpm run dev:api or pnpm run dev:webapp-api
webappapps/webapp26632pnpm run dev:webapp or pnpm run dev:webapp-api
websiteapps/website26630pnpm run dev
mobileapps/mobile26634 (Expo Metro)pnpm run dev:mobile
chrome-extensionapps/chrome-extension26633 (WXT HMR)pnpm exec wxt
@bondery/emails previewpackages/emails26639pnpm run preview

On this page