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 ReachCrew.
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 and the scheduled jobs start inside it, so no cron is needed. If you would rather drive the jobs from your host’s cron, see Scheduled jobs from outside.
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):CRON_SECRET and OUTREACH_TICK_SECRET are optional: set them only to call the job endpoints from outside (see below).POSTGRES_PASSWORD and PORT in .env.example are for Docker Compose only. See Docker Compose (the “Set your 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 demo@example.com / 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 and the scheduled jobs 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 from outside
Underbun run start the app runs its own scheduled jobs (daily rollover, LinkedIn outreach, webhook replay, pruning). To drive them from the host’s cron instead, set INTERNAL_SCHEDULER=false and set the secrets you need in .env.local. Every request is POST. Replace $APP with your public origin. The meaning of each job, and how the endpoints avoid repeating the scheduler’s work (?force=1 to skip that), is in the jobs section.
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):
/api/linkedin/jobs/run-search-queue) is not part of the schedule; it runs when someone clicks Run, and can be called the same way if you want it on a timer. 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.
Do not schedule /api/outreach/tick: the in-process scheduler already does it, and scheduling it as well double-ticks.
Check that it works
/sign-in loads, and the app log shows [scheduler] ... lines as jobs run (the first LinkedIn outreach run comes at the next :00 or :30 UTC).[scheduler] Table scheduled_job_runs is missing, see Troubleshooting.
Next
Going to production
HTTPS, backups, upgrades and the jobs table.
Troubleshooting
db:setup refusals, pool errors, silent email.