Instruction manual
AbdelftahZowail/Quickly instruction manual
MIT-licensed, self-hosted cold-email application with campaign sequencing, inbox rotation, reply processing, analytics, REST APIs, webhooks, an n8n community node, and a remote MCP server that exposes lead-related tools; it can serve Claude Code only as a separately deployed MCP tool surface by inference, because supplied files document Cursor and generic MCP clients but no Claude Code-specific setup.
1. Purpose, architecture, and Claude Code classification
Quickly is an MIT-licensed, self-hosted cold-email platform for sales teams, agencies, and independent operators. It combines a React 18/Vite/Tailwind interface, a Python 3.12 FastAPI backend, async SQLAlchemy, PostgreSQL 15, APScheduler, and optional Caddy HTTPS. It sends through Gmail API, Microsoft Graph, or generic SMTP; Gmail and Microsoft use OAuth 2.0. Data lives in the operator’s PostgreSQL database.
The documented product surface includes multi-step campaigns, inbox rotation, warm-up and jitter, A/B variants, CSV personalization, reply synchronization and classification, tracking, analytics, webhooks, REST automation, an n8n node, backups, and an MCP endpoint. The first account becomes the administrator; registration then closes, and administrators invite further users.
**Claude Code classification: unclassified.** The supplied first-party files document an MCP server at `/api/mcp`, leads tools, and connection through `mcp-remote` from Cursor or “other clients.” They do not name Claude Code, provide Claude Code configuration, or demonstrate a Claude-specific plugin, hook, skill, command, or tested MCP setup. Quickly is clearly a standalone web application and exposes an agent-facing tool surface, but the evidence is insufficient to classify a reliable Claude Code extension mechanism.
The repository metadata identifies `AbdelftahZowail/Quickly`, default branch `main`, Python as the primary language, and the MIT license. Repository claims such as deliverability comparisons, deployment time, provider count, and endpoint count are project statements, not independently established guarantees.
2. Production deployment and essential configuration
Choose one documented deployment: Railway/PaaS, a VPS without in-stack Caddy, the all-in-one Caddy stack, or the host-Caddy layout. Railway uses image `azowail/quickly:latest`, a PostgreSQL plugin, and `BASE_URL` matching the generated HTTPS domain.
For the recommended Beacon-only VPS layout, download the release files, edit `.env`, terminate HTTPS with your own reverse proxy, and start:
mkdir quickly && cd quickly
curl -LO https://github.com/azowail/quickly/releases/latest/download/docker-compose.no-caddy.yml
curl -LO https://github.com/azowail/quickly/releases/latest/download/.env.example
mv .env.example .env
docker compose -f docker-compose.no-caddy.yml up -dThis publishes Quickly on port 8000 and creates its PostgreSQL volume through Compose. Set `BASE_URL`, a stable `QUICKLY_SECRET_KEY`, and provider credentials as needed. For the all-in-one stack, first create its required external volume, then download Caddy resources:
docker volume create quickly_pgdata
mkdir quickly && cd quickly
curl -LO https://github.com/azowail/quickly/releases/latest/download/docker-compose.yml
curl -LO https://github.com/azowail/quickly/releases/latest/download/Caddyfile
curl -LO https://github.com/azowail/quickly/releases/latest/download/.env.example
mv .env.example .env
docker compose up -dSet `CADDY_HOST=yourdomain.com`, `BASE_URL=https://yourdomain.com`, and `QUICKLY_SECRET_KEY`. Caddy needs public ports 80 and 443 and automatically manages the main-domain certificate. Generate a signing secret with:
python -c "import secrets; print(secrets.token_urlsafe(64))"Other documented variables are `DATABASE_URL`, Google and Office 365 client credentials, `OFFICE365_TENANT_ID`, `CORS_ORIGINS`, backup controls, and tracking-UI flags. If the signing key changes or is omitted, existing JWT sessions are invalidated after restart.
3. First login and sending-account setup
Open the deployed URL and create the initial administrator. The files describe built-in username/password authentication and Google/Microsoft sign-in; OAuth sign-in requires the relevant provider credentials first. Generate automation keys under **Settings → API Keys** and save each key immediately because it is displayed only once. A separately connected personal Google or Microsoft account can send user notifications; it is not an outbound campaign inbox.
For Gmail, create a Google Cloud project, enable Gmail API and Cloud Pub/Sub API, and create a Web application OAuth client. Register both callbacks:
https://yourdomain.com/oauth/app/google/callback
https://yourdomain.com/oauth/google/callbackSet `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`, restart the deployment, configure the consent screen with `https://mail.google.com/`, and add test users while the app remains in testing. Optional real-time Unibox detection uses a Pub/Sub push subscription targeting `https://yourdomain.com/api/unibox/gmail/push`; grant `gmail-api-push@system.gserviceaccount.com` publisher access and enter the full topic name under **Settings → Gmail Sync**. Connect each account through **Inboxes → Add Inbox → Connect Gmail Account**.
For Microsoft 365, Office 365, or Outlook.com, register an Azure/Entra application with these callbacks:
https://yourdomain.com/oauth/office365/callback
https://yourdomain.com/oauth/app/office365/callbackGrant delegated `Mail.ReadWrite`, `Mail.Send`, `User.Read`, and `offline_access`; create a client secret; set the client ID, secret, and `OFFICE365_TENANT_ID=common` or a specific tenant; then use **Connect Office 365 Account**.
Generic SMTP is also supported, optionally with IMAP reply sync. For Amazon SES, use regional SES SMTP credentials, a verified sender, host `email-smtp.<region>.amazonaws.com`, and port 587/STARTTLS or 465/SSL. SES itself has no inbox, so leave IMAP empty; match Quickly’s limits to SES quotas.
4. Campaigns, personalization, scheduling, replies, and analytics
Create a campaign under **Campaigns → New Campaign**, name it, optionally choose a timezone, and assign one or more Gmail, Microsoft, or SMTP inboxes. Quickly rotates sends while respecting inbox limits, sending windows, business-day schedules, cooldowns, warm-up ramps, and configurable random jitter. Provider matching can inspect a lead domain’s DNS MX records and route Google or Microsoft recipients through matching inbox types.
Add unlimited sequence steps. Each step may use HTML or plain text, wait days, and reply threading. Personalize subjects and bodies with fields such as `{{name}}`, `{{email}}`, `{{company}}`, or any extra CSV column. A CSV must contain `email`; all other columns become template variables. Add multiple subject/body variants to a step for random selection and per-variant open, click, and reply measurements. Start the campaign to reserve queue slots and begin scheduled delivery. Campaign ordering supports drag-to-prioritize, with priority-first or round-robin strategy.
Tracking uses pixels and wrapped links. Analytics include campaign totals, step performance, timelines, opens, clicks, replies, bounces, and unsubscribes. Test mode can simulate sends, opens, and clicks without delivering mail. Permanent failures stop affected leads. Verification can check addresses through mailtester.ninja or another HTTP provider.
Unibox combines reply detection, threaded viewing, compose, and reply across connected accounts. AI classification assigns one of `interested`, `not_interested`, `out_of_office`, `wrong_person`, `auto_reply`, or `unsubscribed`. Configure it under **Settings → AI Features** by selecting a provider, credential or Ollama endpoint, model, and enablement state. The project documents 19 providers and local Ollama operation. The health dashboard reports inbox-token health, OAuth scopes, tracking reachability, AI connectivity, and synchronization mode. Email notifications are configurable by user and event with an hourly rate limit.
5. Tracking hostnames, REST API, webhooks, MCP, and n8n
For branded tracking links, the recommended path is Quickly Beacon: run Beacon at the desired HTTPS origin, set `BEACON_PUBLIC_BASE_URL` when needed, copy its initial token-bearing setup URL, and paste it under **Inboxes → Connect Beacon**. One Beacon hostname can serve multiple inboxes, but connect each inbox so it receives separate registrations and webhook secrets. The prebuilt image hides legacy CNAME controls. Advanced host-Caddy deployments can expose them with `QUICKLY_TRACKING_CNAME_UI=1` or `QUICKLY_PREBUILT_IMAGE=0`; that legacy path requires on-demand TLS and `/api/caddy/ask`.
The REST API has JWT and API-key authentication. Source-supported examples are:
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "yourpassword"}'
curl http://localhost:8000/api/campaigns \
-H "Authorization: Bearer <access_token>"
curl -X POST http://localhost:8000/api/campaigns/1/leads \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '[{"email": "alice@example.com", "name": "Alice"}]'Webhooks can filter 15 documented events, including sends, opens, clicks, bounces, reply classes, status changes, limits, and token expiry; endpoints support Bearer authentication. MCP is a remote Streamable HTTP server at `/api/mcp` for leads tools. The supplied files do not include its exact client configuration, so none is added here.
The n8n package exposes Account, Campaign, Sequence, Sequence Variant, Lead, Campaign Lead, Inbox, Schedule, Status, Settings, Webhook, Notification, Unibox, and OAuth-account operations. It intentionally excludes browser redirects, tracking responses, and MCP streaming. Build it only with the documented commands:
cd n8n-node
npm install
npm run buildPoint `N8N_CUSTOM_EXTENSIONS` at the whole package folder containing `package.json`, `dist`, and `node_modules`. Credentials accept an API key via `X-API-Key` or JWT via Bearer auth; the test calls `GET /api/auth/me`. CSV export returns binary data; import reads a binary property named `data` by default. Manual Unibox thread IDs use `inbox_id||thread_id`; Campaign Reorder expects JSON such as `[3,1,2]`. **Continue On Fail** records per-item errors.
6. Backups, updates, operational checks, and troubleshooting
Settings can create `.qbk` backups containing a manifest and PostgreSQL custom-format dump. Encrypted backups expose only a small masked manifest preview; the dump remains encrypted, and losing the password makes it unrecoverable. Older `.qbk` formats are rejected. Automatic encrypted backups reuse the saved password. Local disk backup requires `QUICKLY_LOCAL_DISK_BACKUPS=1` and writable persistent `backups/`; repository Compose files provide this and retain the newest 10 files. PaaS deployments without persistent storage should use webhook delivery. Host-run backends need `pg_dump` and `pg_restore`; multi-worker restores need a shared `QUICKLY_RESTORE_STAGING_DIR`.
Update the matching stack without changing deployment variants:
docker compose pull
docker compose up -ddocker compose -f docker-compose.no-caddy.yml pull
docker compose -f docker-compose.no-caddy.yml up -ddocker compose -f docker-compose-not-host.yml pull
docker compose -f docker-compose-not-host.yml up -dSchema changes apply at startup, and PostgreSQL volumes preserve data. For the standard stack, inspect services and application logs with:
docker compose ps
docker compose logs app
docker compose logs caddyTLS failures usually mean DNS does not point at the server, ports 80/443 are blocked, or `CADDY_HOST` does not exactly match the host. App failures call for checking service state, app logs, `.env`, and `BASE_URL`. In Compose, a custom database URL must use Docker service hostname `db`, not `localhost`. OAuth failures commonly indicate absent credentials, a callback mismatch, disabled Gmail API, missing Google test-user status, absent Microsoft delegated permissions, or the wrong tenant. Set a stable signing key to prevent restart-driven session expiry. HTTP-only operation leaves `CADDY_HOST` unset and starts the documented Compose stack normally.
7. Local development, frontend build, and tests
Local development requires Python 3.12+, Node.js 18+, and PostgreSQL 15+. The Docker development stack is the shortest documented path:
git clone https://github.com/azowail/quickly.git
cd quickly
cp .env.example .env
# Edit .env and set: BASE_URL=http://localhost:8000
docker compose -f docker-compose.dev.yml upThe UI is then at `http://localhost:5173`, with frontend and backend reload. A no-Caddy development variant is also documented as `docker compose -f docker-compose.no-caddy.dev.yml up`; it uses `.env_dev`, PostgreSQL 5435, API 8002, Vite 5175, and prebuilt-style tracking controls.
For manual backend work:
git clone https://github.com/azowail/quickly.git
cd quickly
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reloadSet `BASE_URL=http://localhost:8000` and `DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost/quickly`. A documented disposable database is:
docker run -d --name quickly-db \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=quickly \
-p 5432:5432 \
postgres:15-alpineRun the frontend separately:
cd frontend
npm install
npm run devThe frontend package also documents `npm run build` and `npm run preview`. Run fast SQLite-backed tests with `pytest`. For PostgreSQL-backed tests on macOS/Linux, export `TEST_DATABASE_URL` and run `pytest`; Windows uses `set TEST_DATABASE_URL=...`. The n8n package additionally documents `npm run watch` for rebuilding on TypeScript changes.
8. Security utilities, constraints, and safe operation
The documented security module provides encryption, SSRF checks, target validation, SQLAlchemy encryption, and response headers. `init_encryption()` initializes a module-level Fernet object from an argument or `QUICKLY_ENCRYPTION_KEY`; without a key, sensitive values—including SMTP/IMAP passwords—fall back to plaintext. `_derive_key()` hashes an arbitrary passphrase with SHA-256 into Fernet-compatible form. `generate_encryption_key()` creates a URL-safe random value. `encrypt()` and `decrypt()` transform values when encryption is active; both pass through empty values, and decryption also returns the original value when token decoding fails. `is_encrypted()` uses the `gAAAAA` Fernet prefix as a heuristic. `EncryptedText` applies those operations automatically when SQLAlchemy writes or reads a column.
`is_private_ip()` blocks unparseable, private, loopback, link-local, documentation, benchmarking, multicast, reserved, broadcast, and corresponding IPv6 addresses. `resolve_and_check()` rejects named metadata hosts, checks raw addresses, resolves DNS, and rejects a hostname if any result is blocked. `validate_url_not_ssrf()` requires a hostname and delegates that check. `validate_webhook_url()` permits only HTTP/HTTPS, requires HTTPS in production, and allows HTTP localhost only when enabled; its subsequent private-address check still applies. `is_valid_hostname()` accepts public-domain-shaped hostnames up to 253 characters.
`SecurityHeadersMiddleware` adds content-type sniffing protection, frame denial, disabled legacy XSS filtering, strict-origin referrer policy, disabled camera/microphone/geolocation permissions, and a restrictive Content Security Policy. It adds two-year HSTS with subdomains when direct or forwarded HTTPS is detected.
Operational limits remain: SMTP providers and AI services have their own terms and quotas; SES delayed SNS bounces are not integrated; JWTs expire; API calls can return 401, 404, 422, or 429; tracking and classification process potentially sensitive outreach data. The software is self-hosted, but configured AI providers, OAuth providers, webhooks, verification services, MCP clients, and n8n workflows can transmit data outside the server. Review credentials, endpoint reachability, consent obligations, and applicable anti-spam law before real campaigns.