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:setuprefuses older servers. The schema is tested on 16, 17 and 18. gitandopenssl. 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
3
Create .env.local
.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
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:Scheduled jobs without the sidecar
Docker Compose runs thecron 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_SECRETendpoints take the secret as?secret=...or thex-tick-secretheader. The endpoint answers 401 when the variable is unset.CRON_SECRETendpoints takeAuthorization: Bearer $CRON_SECRET. With the secret the call serves every organization; a signed-in member calling the same route only runs their own organization.
.env.local, so define the variables at the top of the crontab (crontab -e):
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, notunauthorized:
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.