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

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

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 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:
Run one instance only. The outreach sender loop has no distributed lock; two instances would each send.

Scheduled jobs from outside

Under bun 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_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):
The LinkedIn search queue (/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).
If the log shows [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.