DocRequest
Free, self-hosted client portal for collecting documents securely
- Role
- Creator & Sole Developer (Open Source)
- Dates
- Nov 2026
- Status
- Live
- Next.js
- NestJS
- PostgreSQL
- BullMQ
- TypeScript
- Prisma
- Redis
- Zod
- Tailwind CSS
- shadcn/ui
- ClamAV
- sharp
- Docker
- Terraform
- AWS (S3, KMS, RDS)
- Vitest
Summary
Version 1 is finished and live as a public demo. Every file is checked by its content, encrypted before it is stored, virus-scanned on a queue and deleted on schedule. 79 API tests run against real PostgreSQL and Redis in CI.
The problem
Accountants, solicitors, recruiters, letting agents and care agencies need passports, proof of address, payslips and bank statements from their clients. Today these come in over email and WhatsApp. That is insecure, because passports sit in inboxes. It is messy, because files arrive with random names and half are missing. And it is slow, because staff spend hours chasing. Paid tools solve this for a monthly fee, but then clients' documents live on someone else's servers. DocRequest is a free, open-source alternative that a business runs itself, so the files stay on its own server.
My role
Solo project. I own the whole project. I wrote the PRD and the six-phase build plan, set the stack and project rules, and built and shipped it phase by phase to a live demo.
What I built
- Wrote the PRD and a six-phase build plan with the data model, API endpoints and pages.
- Built staff sign-in with a password plus a 6-digit emailed code, rotating refresh tokens, workspaces and Owner, Admin and Staff roles.
- Built templates, requests and secure client links, plus a mobile-first upload page that needs no client account.
- Built the file pipeline: type check by content, per-file AES-256-GCM encryption, ClamAV scan, HEIC to JPEG, and GPS and EXIF removal.
- Built review with retakes, zip export with clean names, reminders, link expiry, and retention that deletes each file's key.
- Added demo mode, Docker Compose self-hosting and AWS Terraform, all checked in CI.
Architecture
A pnpm and Turborepo monorepo. The web app only shows data; the API makes every decision. - apps/web: Next.js with Tailwind and shadcn/ui: the landing page, the staff app and the client upload page. /api/* is proxied to the API so cookies stay first-party. - apps/api: NestJS with Prisma on PostgreSQL. All logic, auth, permissions and the audit log. A separate worker runs the BullMQ queues: mail, virus scan, image conversion, zip export and scheduled sweeps. - packages/shared: Zod schemas, types and the five built-in checklists, used by both apps. - Redis holds sign-in codes, client sessions, rate-limit counters and the job queues. - Storage adapter: local disk by default, or any S3-compatible service. Each file's key is wrapped by a master key from the environment or AWS KMS. - ClamAV scans uploads in the worker. It can be switched off on small servers. - infra/aws: Terraform for one ARM EC2 server running the same Docker Compose stack, with private RDS PostgreSQL, private S3 and a KMS key.
Key decisions and trade-offs
Chose: to encrypt every upload with its own random AES-256-GCM key before anything is written, with that key wrapped by a master key from the environment or AWS KMS. Because: storage then holds only ciphertext under random names, and deleting a file's wrapped key makes any leftover copy useless. Chose: a storage adapter with local disk as the default and any S3-compatible service as an option. Because: a self-hoster can start on one small server and move to S3, R2 or MinIO by changing settings, not code. Chose: to run virus scanning and image conversion on a BullMQ queue in a separate worker, and never serve a file until it is scanned. Because: ClamAV and HEIC conversion are slow. The upload request returns quickly, while unscanned or infected files stay locked.
Hardest problem and how I solved it
Client links are 32 random bytes, and the server stores only their SHA-256 hash. That keeps links safe if the database leaks, but it means the server can't put an existing link into a later email. Reminders, retake requests and "send a new link" all need a working link. So every email that needs a link makes a fresh one, and older links stop working. An old email shows a clear message telling the client to open the latest one. The client also needs a 6-digit code from their inbox, which works for 10 minutes and locks after 5 wrong tries. Sessions are kept separate from links. Each session is bound to one request, lasts 2 hours and renews while the client is active, so a client already on the page isn't thrown out when a reminder replaces their link. Link expiry is set when the request is sent and isn't extended by reminders or retakes.
Testing and quality
79 API tests in 9 files (Vitest and Supertest) run against a real PostgreSQL database and Redis, with no mocks. They cover sign-in with the emailed code, refresh rotation and reuse detection, rate limits, role rules and workspace isolation, client links and code lockout, and file type checks by content. They also check that only ciphertext is stored, that a wrong key or tampering fails, that infected files are removed, that GPS data is stripped from photos and that unscanned files are never served. Reminders sending once, retention and demo reset safety are covered too. CI on GitHub Actions has three jobs: typecheck, lint, tests, build and pnpm audit against PostgreSQL 18 and Redis 8; a Docker build that starts the whole stack; and a Terraform format and validate check.
Results
- 79 API tests in CI against real PostgreSQL and Redis
- 6 of 6 build phases done, from sign-in to Docker and AWS
- 5 ready-made document checklists
Version 1 is complete, open source under the AGPL-3.0 licence, and CI passes on main. It can be self-hosted with one docker compose up, or deployed to AWS with the included Terraform. Try the live demo: press "Try the demo as Priya", who runs a made-up letting agency. No email or password is needed. Please don't upload real documents.
Key features
- Five ready-made checklists, or write your own.
- One secure link per client, confirmed with a 6-digit emailed code, and no client account.
- Mobile-first upload page with a progress bar.
- Preview, accept or ask for a retake with a reason.
- Automatic reminders at 2, 5 and 7 days and before the due date, sent only in the daytime.
- One zip with clean file names, like smith-jane_passport.pdf.
- Owner, Admin and Staff roles with fully separate workspaces.
- Per-file encryption, virus scanning, GPS removal and scheduled deletion.
- Append-only audit log with no client details in it.
Out of scope
Version 1 leaves out e-signatures, client accounts, payments, OCR or AI checks on documents, integrations such as Xero and Google Drive, custom domains and native apps.
