Skip to main content
Use this path for development, or when you manage the runtime and the database yourself. If you just want a working server, Docker Compose is simpler.
What you need
  • Bun 1.2+ (the repository’s tooling uses Bun) and Node.js 20.9+.
  • PostgreSQL 16 or newer, and an empty database for AgentSDR. db:setup refuses older servers. The schema is tested on 16, 17 and 18.
  • git and openssl. About 20 minutes.
  • A reverse proxy and a supervisor for production (below).

Overview

You run one process (next start) against one database. Background workers start inside it. The periodic jobs that must be called over HTTP are scheduled with your host’s cron: see Scheduled jobs without the sidecar.
1

Clone and install

2

Create an empty database

Or create it with your hosting provider’s console.
3

Create .env.local

Next.js and Bun both read .env.local. Set at least these (generate each secret with openssl rand -hex 32):
POSTGRES_PASSWORD and PORT in .env.example are for Docker Compose only. See Docker Compose (the “Generate the secrets” step) for what each secret does and the warning about INTEGRATION_CREDENTIALS_KEY. Every variable: configuration reference.
4

Create the schema

db:setup reads DATABASE_URL (and DATABASE_SSL) from the environment; Bun loads .env.local automatically for it. It applies db/schema.sql and db/seed.sql in one transaction, so a failure leaves the database untouched. It refuses to run if the database already contains tables. See Database.Optional, for local development only: bun run db:seed:demo creates a fictional demo organization. Sign in as [email protected] / demo-password-123 (override the password with DEMO_PASSWORD). It refuses to run with NODE_ENV=production or a DATABASE_URL that is not on localhost or 127.0.0.1 (unless you pass --allow-remote), because the data is fictional and must never land in a real database.
5

Build and start

For development use bun run dev instead of build and start. next start reads .env.local as well. The in-process outreach and WhatsApp campaign senders run only when NODE_ENV=production, which next start sets; they do not run under bun run dev.
6

Sign up

Open /sign-up, create the first account and your organization. See first sign-up.

Run it under a process manager

Put the process behind a reverse proxy (Caddy, nginx, a load balancer) that terminates TLS and forwards to port 3000. Run it under a supervisor (systemd, Docker, PM2) so it restarts on failure. A systemd unit, for example:
Run one instance only. The outreach sender loop has no distributed lock; two instances would each send.

Scheduled jobs without the sidecar

Docker Compose runs the cron sidecar for you. From source, schedule the same calls with the host’s cron. Every request is POST. Replace $APP with your public origin. The meaning of each job is in the jobs table.
  • OUTREACH_TICK_SECRET endpoints take the secret as ?secret=... or the x-tick-secret header. The endpoint answers 401 when the variable is unset.
  • CRON_SECRET endpoints take Authorization: Bearer $CRON_SECRET. With the secret the call serves every organization; a signed-in member calling the same route only runs their own organization.
Example crontab (UTC). Cron does not read your .env.local, so define the variables at the top of the crontab (crontab -e):
If you do not use LinkedIn, the CRON_SECRET jobs are not needed. If you do not use email outreach, the OUTREACH_TICK_SECRET jobs are not needed. The schedules for the four LinkedIn jobs are suggestions. Any reasonable frequency works: each run respects working hours and daily limits, and the search queue never runs twice at once. Do not schedule /api/outreach/tick: the in-process scheduler already does it, and scheduling it as well double-ticks.

Check that it works

Run one job by hand and look for JSON back, not unauthorized:
The call returns a JSON result (an unauthorized 401 means the secret does not match the app’s OUTREACH_TICK_SECRET), and /sign-in loads.

Next

Going to production

HTTPS, backups, upgrades and the jobs table.

Troubleshooting

db:setup refusals, pool errors, silent email.