This document provides a high-level overview of the KeepContracts (KC) codebase to help humans and agents understand how the application is structured.
What KeepContracts is: A document signing engine (white-labeled Documenso), running at keepcontracts.com. It is invisible infrastructure. End users never interact with it directly. Signup is disabled and only DataThink admins have accounts.
An envelope is a single signing transaction. It holds a document, a list of recipients, and tracks status.
- 1 contractor signing an NDA → 1 envelope
- 1 teacher sending a permission slip to 30 parents → 30 envelopes (one per parent)
1. DataThink apps (i.e. Ladderly or KeepHours) calls KC API using a service account key
2. KC creates the envelope and emails the recipient a magic link
3. Recipient clicks the link and signs (no KC account needed)
4. KC webhooks back to the DataThink app on completion
5. DataThink app downloads and permanently stores the signed PDF
6. KC is done. The DataThink app owns the record
KC doesn't do this, the DataThink app does. When KC creates an envelope it returns an envelope_id. Your app stores that ID linked to the relevant person in your own database:
| contractor_id | envelope_id | document | sent_at |
|---|---|---|---|
| 99 | kc-envelope-abc | NDA | Jan 15 |
| 99 | kc-envelope-xyz | W9 Form | Feb 15 |
WHERE contractor_id = 99 → full history, regardless of how many envelopes.
Ladderly: Teacher clicks "send" in Ladderly → Ladderly calls KC API → parent receives magic link → signs → KC webhooks to Ladderly → Ladderly stores signed PDF. Teacher sees all their documents in Ladderly filtered by teacher_id. Teacher never touches keepcontracts.com.
KeepHours: DataThink sends NDA to contractor via KC API → contractor signs via magic link → KC webhooks to KeepHours → KeepHours stores signed PDF. Contractor logs into KeepHours and sees only their own documents — scoped by contractor_id in KeepHours' DB.
This section covers the technical architecture of the KeepContracts codebase (forked from Documenso).
Documenso is an open-source document signing platform built as a monorepo using npm workspaces and Turborepo. The application enables users to create, send, and sign documents electronically.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Remix App (Hono Server) │
│ apps/remix │
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────────────┤
│ /api/v1/* │ /api/v2/* │ /api/trpc/* │ /api/jobs/* │ React Router UI │
│ (ts-rest) │ (tRPC) │ (tRPC) │ (Jobs API) │ │
├─────────────┴─────────────┴─────────────┴─────────────┴─────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ @api │ │ @trpc │ │ @lib │ │ @email │ │ @signing │ │
│ │ (REST) │ │ (RPC) │ │ (CORE) │ │ │ │ │ │
│ └─────────┘ └─────────┘ └────┬────┘ └─────────┘ └─────────────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐ │
│ │ Storage │ │ Jobs │ │ PDF │ │
│ │Provider │ │ Provider │ │ Signing │ │
│ └────┬────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
└──────────────┼──────────────────┼──────────────────┼────────────────────────┘
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ Database │ │ Inngest/ │ │ Google KMS/ │
│ S3 │ │ Local │ │ Local │
└─────────────┘ └─────────────┘ └─────────────┘
| Package | Description | Port |
|---|---|---|
@documenso/remix |
Main application - React Router (Remix) with Hono server | 3000 |
@documenso/documentation |
Documentation site (Next.js + Nextra) | 3002 |
@documenso/openpage-api |
Public analytics API | 3003 |
| Package | Description |
|---|---|
@documenso/lib |
Core business logic (server-only, client-only, universal) |
@documenso/trpc |
tRPC API layer with OpenAPI support (API V2) |
@documenso/api |
REST API layer using ts-rest (API V1) |
@documenso/prisma |
Database layer (Prisma ORM + Kysely) |
@documenso/ui |
UI component library (Shadcn + Radix + Tailwind) |
@documenso/email |
Email templates and mailer (React Email) |
@documenso/auth |
Authentication (OAuth via Arctic, WebAuthn/Passkeys) |
@documenso/signing |
PDF signing (Local P12, Google Cloud KMS) |
@documenso/ee |
Enterprise Edition features |
@documenso/assets |
Static assets |
| Package | Description |
|---|---|
@documenso/app-tests |
E2E tests (Playwright) |
@documenso/tailwind-config |
Shared Tailwind config |
@documenso/tsconfig |
Shared TypeScript configs |
| Category | Technology |
|---|---|
| Frontend | React 18, React Router v7 (Remix) |
| Server | Hono |
| Database | PostgreSQL 15, Prisma, Kysely |
| API | tRPC, ts-rest, OpenAPI |
| Styling | Tailwind CSS, Radix UI, Shadcn UI |
| Auth | Arctic (OAuth), WebAuthn/Passkeys |
| React Email, Nodemailer | |
| Jobs | Inngest / Local |
| Storage | S3-compatible / Database |
| @libpdf/core, pdfjs-dist | |
| i18n | Lingui |
| Build | Turborepo, Vite |
| Testing | Playwright |
- Location:
packages/api/v1/ - Framework: ts-rest (contract-based REST)
- Mount:
/api/v1/* - Auth: API Token (Bearer header)
- Status: Deprecated but maintained
Routes (RESTful pattern):
GET/POST/DELETE /api/v1/documents/*GET/POST/DELETE /api/v1/templates/*- Recipients and fields nested under documents
- Location:
packages/trpc/server/ - Framework: tRPC with trpc-to-openapi
- Mount:
/api/v2/*,/api/v2-beta/* - Auth: API Token or Session Cookie
- Status: Active
Routes (action-based pattern):
GET/POST /api/v2/document/*- Document operationsGET/POST /api/v2/template/*- Template operationsGET/POST /api/v2/envelope/*- Envelope operations (multi-document)GET/POST /api/v2/folder/*- Folder management
Route Organization:
packages/trpc/server/
├── document-router/
│ ├── get-document.ts
│ ├── get-document.types.ts
│ └── ...
├── template-router/
├── envelope-router/
├── recipient-router/
├── field-router/
└── ...
- Mount:
/api/trpc/* - Usage: Frontend-to-backend communication
- Auth: Session-based
Jobs handle async operations like email sending, document sealing, and webhooks.
┌─────────────────┐ ┌───────────────────────────────────────┐
│ triggerJob() │────▶│ Job Provider │
│ │ │ ┌─────────────┬─────────────────┐ │
│ - name │ │ │ Inngest │ Local │ │
│ - payload │ │ │ (Cloud) │ (Database) │ │
└─────────────────┘ │ └─────────────┴─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Job Handler │ │
│ │ (async processing) │ │
│ └─────────────────────┘ │
└───────────────────────────────────────┘
packages/lib/jobs/client/- Provider implementationspackages/lib/jobs/definitions/- Job definitions
Email Jobs:
send.signing.requested.email- Signing invitationsend-confirmation-email- Email verificationsend-recipient-signed-email- Notify on signaturesend-rejection-emails- Rejection notificationssend-document-cancelled-emails- Cancellation notices
Internal Jobs:
internal.seal-document- Finalize signed documentsinternal.bulk-send-template- Bulk document sendinginternal.execute-webhook- External webhook calls
The codebase uses a strategy pattern with ts-pattern for provider selection via environment variables.
Handles file uploads and downloads.
| Provider | Description | Env Value |
|---|---|---|
| Database | Store files as Base64 in DB | database |
| S3 | S3-compatible storage (+ CloudFront) | s3 |
Config: NEXT_PUBLIC_UPLOAD_TRANSPORT
Location: packages/lib/universal/upload/
Cryptographically signs PDF documents.
| Provider | Description | Env Value |
|---|---|---|
| Local | P12 certificate file | local |
| Google Cloud HSM | Google Cloud KMS | gcloud-hsm |
Config: NEXT_PRIVATE_SIGNING_TRANSPORT
Location: packages/signing/
Sends transactional emails.
| Provider | Description | Env Value |
|---|---|---|
| SMTP Auth | Standard SMTP with credentials | smtp-auth |
| SMTP API | SMTP with API key | smtp-api |
| Resend | Resend API | resend |
| MailChannels | MailChannels API | mailchannels |
Config: NEXT_PRIVATE_SMTP_TRANSPORT
Location: packages/email/mailer.ts
Processes async jobs.
| Provider | Description | Env Value |
|---|---|---|
| Local | Database-backed queue | local (default) |
| BullMQ | Redis-backed queue | bullmq |
| Inngest | Managed cloud service | inngest |
Config: NEXT_PRIVATE_JOBS_PROVIDER
Location: packages/lib/jobs/client/
Browser
│
▼
Hono Server (apps/remix/server/)
│
├──▶ /api/v1/* ──▶ ts-rest handlers (packages/api/)
│
├──▶ /api/v2/* ──▶ tRPC OpenAPI handlers (packages/trpc/)
│
├──▶ /api/trpc/* ──▶ tRPC handlers (packages/trpc/)
│
├──▶ /api/jobs/* ──▶ Job handlers (packages/lib/jobs/)
│
└──▶ /* ──▶ React Router (apps/remix/app/routes/)
│
▼
React Components (packages/ui/)
1. Upload Document ──▶ Storage Provider (DB/S3)
│
2. Add Recipients ────────────────┤
│
3. Add Fields ────────────────────┤
│
4. Send Document ─────────────────┤
│ │
▼ │
Email Job ──▶ Email Provider |
│ |
5. Recipient Signs ───────────────┤
│ │
▼ │
seal-document Job │
│ │
▼ │
Signing Provider ◀─────────────┘
│
▼
Signed PDF ──▶ Storage Provider
documenso/
├── apps/
│ └── remix/
│ ├── app/
│ │ └── routes/ # React Router routes
│ │ ├── _authenticated+/ # Protected routes
│ │ ├── _unauthenticated+/ # Public routes
│ │ └── _recipient+/ # Signing routes
│ └── server/
│ ├── router.ts # Hono route mounting
│ └── main.js # Entry point
├── packages/
│ ├── api/v1/ # API V1 (ts-rest)
│ ├── trpc/server/ # API V2 + Internal (tRPC)
│ ├── lib/
│ │ ├── server-only/ # Server business logic
│ │ ├── client-only/ # Client utilities
│ │ ├── universal/ # Shared code
│ │ └── jobs/ # Background jobs
│ ├── prisma/ # Database schema & client
│ ├── signing/ # PDF signing
│ ├── email/ # Email templates
│ └── ui/ # Component library
└── docker/ # Docker configs
# Full setup (install, docker, migrate, seed, dev)
npm run d
# Start development server
npm run dev
# Database GUI
npm run prisma:studio
# Type checking (faster than build)
npx tsc --noEmit
# E2E tests
npm run test:e2e| Service | Port |
|---|---|
| PostgreSQL | 54320 |
| Inbucket (Mail) | 9000 |
| MinIO (S3) | 9001, 9002 |
| Variable | Purpose | Options |
|---|---|---|
NEXT_PUBLIC_UPLOAD_TRANSPORT |
Storage provider | database, s3 |
NEXT_PRIVATE_SIGNING_TRANSPORT |
Signing provider | local, gcloud-hsm |
NEXT_PRIVATE_SMTP_TRANSPORT |
Email provider | smtp-auth, smtp-api, resend, mailchannels |
NEXT_PRIVATE_JOBS_PROVIDER |
Jobs provider | local, inngest |
See .env.example for the complete list of configuration options.