A self-hosted CRM + invoicing workspace for freelancers and small studios. Track clients, log activity, create and send invoices, get paid — all in one place, running on your own machine or server.
- Clients — contact details, status (lead/active/archived), and a running activity timeline (notes, calls, meetings, plus auto-logged invoice events)
- Invoices — line items, tax and discount, payment terms (Net 7/14/30/45/60, due on receipt, or custom) that auto-calculate the due date, multi-currency support, draft → sent → overdue → paid lifecycle, PDF export, and a no-login shareable link for clients to view and download their invoice
- Dashboard — outstanding balance, overdue amount, revenue this month, recent activity
- Settings — business profile, invoice numbering, default tax rate, and payment instructions
git clone https://github.com/richaferry/docket.git
cd docket
npm install
npm run devOpen http://localhost:3000. On first run you're sent
to /setup to create your admin account and business name.
Data lives in a local SQLite file at data/app.db, created and migrated
automatically on startup — nothing to run by hand. It's gitignored; back it up
however you like.
The project ships with Docker files for both production and local development. Requires Docker (or Docker Desktop).
cp .env.example .env
docker compose -f docker-compose.dev.yml up --buildOpen http://localhost:3000. Your host source is
mounted into the container, so edits hot-reload via next dev.
cp .env.example .env # then set PUBLIC_URL to your real URL
docker compose up -d --buildThe SQLite database is stored in a Docker named volume (docket-data,
mounted at /app/data), so it survives container restarts and rebuilds.
Back it up and restore it like any other file:
# backup
docker compose cp docket:/app/data/app.db ./app.db.backup
# restore
docker compose cp ./app.db.backup docket:/app/data/app.dbFor self-hosting, put a reverse proxy (Caddy, nginx, or Cloudflare Tunnel) in front of the container for TLS — don't expose the port directly.
Everything workspace-related (business profile, invoice defaults, email provider) is configured through the Settings page. One exception is the Public URL — where this app is reachable from the internet, used to build the client-facing invoice link in emails — which is deployment config, not workspace data, so it's set via an environment variable instead:
# .env
PUBLIC_URL=https://invoices.yourdomain.comSending invoices is disabled with a clear error until this is set. Restart the server after changing it.
Invoice email is sent through a shared sender configured by the superadmin
at /admin/email (MailAnvil key or SMTP + from-name/from-email). Every tenant
uses that sender; tenants don't enter mail-server credentials or a custom
From: address. Client replies are routed to the tenant's business email from
Settings → Business profile.
The shared sender also powers platform auth emails (verification, password
reset). Env vars (MAILANVIL_KEY, SMTP_*, PLATFORM_EMAIL) act as a fallback
when the DB config isn't set.
See docs/paid-email-sending.md for the
deliverability rationale and future options (domain verification or a locked
platform subdomain).
Next.js (App Router) · TypeScript · Tailwind CSS v4 · Drizzle ORM + Postgres ·
@react-pdf/renderer · Nodemailer · Server
Actions for all mutations. Containerised with Docker (Dockerfile,
Dockerfile.dev, docker-compose.yml).
npm run dev— start the dev servernpm run build/npm start— production build & servenpm run db:generate— generate a Drizzle migration after changingsrc/db/schema.tsnpm run db:migrate— apply pending migrationsnpm run admin:create -- you@docket.app— create/update a platform superadmin (accesses the/adminarea; prompts for a password)npm run db:studio— browse the local databasenpm test/npm run test:watch— run the test suite once / in watch modenpm run test:coverage— run tests with a coverage report
The project uses Vitest with the V8 coverage provider.
Core logic — invoicing math, payment terms, formatting utilities, the mailer,
and the client server actions — is unit-tested with the database, auth, and
next/navigation mocked out, so the suite runs without a database or server.
npm test # run all tests once
npm run test:watch # run tests in watch mode
npm run test:coverage # run tests + coverage reportRunning npm run test:coverage writes a browsable HTML report to coverage/
(gitignored) plus a machine-readable coverage-summary.json.
Coverage is enforced in CI — the test job fails if thresholds aren't met:
| Metric | Threshold |
|---|---|
| Statements | 100% |
| Functions | 100% |
| Lines | 100% |
| Branches | ≥95% |
Current measured coverage: 100% statements / functions / lines and 95.4%
branches across src/lib/mailer.ts, src/lib/invoices.ts, src/lib/utils.ts,
src/lib/payment-terms.ts, and src/actions/clients.ts (63 tests).
The coverage report is also uploaded as a downloadable coverage-report
artifact on every push/PR to main (GitHub Actions → the test job → summary).
Contributions are welcome — see CONTRIBUTING.md.
See SECURITY.md for how to report a vulnerability.