Demo mode. Sample data, Gmail sends are dry runs (the exact API request is recorded, nothing leaves), no login. The sample data resets once a day, or now with the button.

Install and deploy

The application is a single Python 3.12 service with a SQLite file. It runs on any Linux box, a Docker host, or a laptop. Nothing about it depends on the developer's accounts, servers or keys: every credential is yours and lives in your .env and your data folder.

Requirements: Docker 24+ with Compose.

git clone <your repository> deanreach
cd deanreach
cp .env.example .env
# edit .env: ADMIN_USER, ADMIN_PASSWORD, SECRET_KEY (see below), TRANSPORT=gmail
docker compose up -d --build

Two containers start: web (the admin on port 8000) and worker (the scheduler). Data lives in the ./data folder on the host: the SQLite database, the Gmail token, uploaded CVs and the OAuth client file. Back that folder up (see Backup and recovery).

Generate a SECRET_KEY:

python3 -c "import secrets; print(secrets.token_hex(32))"

Open http://<server>:8000, sign in with ADMIN_USER / ADMIN_PASSWORD, then connect Gmail from Settings (see Gmail and Google Cloud setup).

Option B: bare metal with systemd

sudo apt install python3.12 python3.12-venv
git clone <your repository> /opt/deanreach
cd /opt/deanreach
python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
cp .env.example .env   # then edit it
DATA_DIR=/opt/deanreach/data .venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000

A systemd unit (/etc/systemd/system/deanreach.service):

[Unit]
Description=DeanReach outreach admin
After=network-online.target

[Service]
User=deanreach
WorkingDirectory=/opt/deanreach
EnvironmentFile=/opt/deanreach/.env
Environment=SCHEDULER_ENABLED=1
ExecStart=/opt/deanreach/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000 --workers 1
Restart=always

[Install]
WantedBy=multi-user.target

Keep --workers 1 when the scheduler runs inside the web process (only one process may own the schedule). To scale the web side, set SCHEDULER_ENABLED=0 there and run python -m deanreach.worker as a second unit.

HTTPS and the public address

Put a reverse proxy with TLS in front (needed for the Google OAuth callback). Caddy does it in two lines:

outreach.example.com {
    reverse_proxy 127.0.0.1:8000
}

The OAuth redirect URI you register with Google is then https://outreach.example.com/settings/gmail/callback.

Environment variables

All of them are documented in .env.example. The ones that matter on day one:

Variable Meaning
ADMIN_USER, ADMIN_PASSWORD the dashboard login
SECRET_KEY signs the session cookie; change it and every session logs out
DATA_DIR folder for the database, token, uploads (default ./data)
DATABASE_URL leave unset for SQLite; set postgresql+psycopg://... for Postgres (install psycopg[binary])
TRANSPORT gmail to send, dryrun to build and log only
GMAIL_CLIENT_SECRETS path of the OAuth client JSON from Google Cloud
ANTHROPIC_API_KEY optional; enables the model fallback for unusual page layouts
SCHEDULER_ENABLED 1 to run the cron schedule in this process
LIVE_FETCH_ENABLED 0 to forbid fetching arbitrary URLs from the admin

First run checklist

  1. Sign in, open Settings, set the sender name, signature and the Gmail address.
  2. Connect Gmail (Settings) after completing the Google Cloud steps.
  3. Upload the CV (Settings), or keep the file at CV_PATH.
  4. Review Targets (titles, disciplines) and Templates.
  5. Add source pages (Source pages), crawl one, read the run trace.
  6. Set the caps and the schedule in Settings. Run outreach once by hand with TRANSPORT=dryrun to see what would go out, then switch to gmail.

Updating

cd /opt/deanreach && git pull
.venv/bin/pip install -r requirements.txt
sudo systemctl restart deanreach

Schema changes are applied automatically at start (create_all); columns are never dropped. Take a backup before updating anyway.

Running the tests

.venv/bin/python -m pytest -q