Trestle is a human-governed document operations application for supplier invoice processing. AI extracts an Invoice Draft and Field Evidence; deterministic rules validate financial facts; authorized people control correction, confirmation, Finance approval, rejection, and posting.
Trestle is a synthetic-data MVP and portfolio demonstration. Do not upload confidential or production documents.
Choose the document that matches what you need:
| Need | Guide |
|---|---|
| Run the application locally | Quick start |
| Walk through every user-facing scenario | DEMO_GUIDE.md |
| Pick an invoice for a test | examples/invoices/README.md |
| Understand the product and boundaries | Product |
| Configure Neon, R2, or Gemini | Managed-service setup |
| Run tests and builds | Development |
| Prepare a hosted synthetic demo | Deployment checklist |
| Role | Responsibilities |
|---|---|
| AP Specialist | Upload, review, correct, resolve findings, confirm, reject, and post invoices |
| Finance Approver | Approve, return, or reject cases requiring Finance review |
| Administrator | Configure Approval Policy, inspect reference data, and reset synthetic data |
Pre-created accounts:
ap.specialist@trestle.demo
finance.approver@trestle.demo
administrator@trestle.demo
All three use the password supplied through SEED_USER_PASSWORD. The password
is never committed.
Received → Processing → Awaiting AP Review
→ Extraction Failed → Processing | Rejected
Awaiting AP Review → Ready to Post
→ Awaiting Finance Approval
→ Returned for Correction | Rejected
Awaiting Finance Approval → Ready to Post
→ Returned for Correction | Rejected
Ready to Post → Posting → Posted
→ Posting Failed → Posting | Returned for Correction | Rejected
Posted and Rejected are terminal states. Every case requires AP
Confirmation. Finance approval is required only when current findings and the
Approval Policy require it.
- PostgreSQL is authoritative for financial state, permissions, corrections, decisions, reference data, and posting records.
- AI extracts values and evidence. It does not authorize, match authoritative records, validate financial rules, approve, reject, or post.
- Human corrections create revisions and invalidate decisions tied to older revisions.
- Source Documents are private and stored outside PostgreSQL. Document hashes are used for exact duplicate detection; object keys are opaque per-upload identifiers.
- Public demonstrations must use synthetic documents and bounded extraction quotas.
- Node.js 22 or newer
- pnpm 10
- Docker with Docker Compose for the recommended local stack
This is the simplest local demonstration. It uses local PostgreSQL and local private document storage.
cp .env.example .env
pnpm install
pnpm devOpen:
- Web: http://localhost:3001
- API health: http://localhost:3000/health
- OpenAPI schema: http://localhost:3000/doc
- API reference: http://localhost:3000/reference
In a second terminal, seed the accounts and reference data inside the server container:
docker compose exec -e SEED_USER_PASSWORD='local-only-password' server pnpm --filter server db:seedThe server startup applies Drizzle, Graphile Worker, and Mastra bootstrap
steps. The Compose stack includes the worker. Then follow
DEMO_GUIDE.md.
Stop without deleting data:
docker compose downDelete the local database volume only when its data is disposable:
docker compose down -vUse this when PostgreSQL already runs outside Compose.
cp apps/server/.env.example apps/server/.env
cp apps/web/.env.example apps/web/.env
pnpm installConfigure the server database, then run the setup steps explicitly:
pnpm --filter server db:migrate
pnpm --filter server queue:migrate
pnpm --filter server mastra:bootstrap
SEED_USER_PASSWORD='local-only-password' pnpm --filter server db:seedStart the web/API processes and worker in separate terminals:
pnpm dev:local
pnpm --filter server workerpnpm dev:local starts the web and API. It does not start the worker.
Use this path to run the web, API, and worker locally while Neon stores PostgreSQL data, Cloudflare R2 stores private documents, and Google AI Studio performs extraction.
Prepare:
- A Neon database and its direct/unpooled connection string.
- A private R2 bucket and an API token limited to that bucket.
- A Google AI Studio API key with an appropriate spending limit.
Do not use a Neon hostname containing -pooler for Trestle's
DATABASE_URL. Mastra bootstrap uses session-level PostgreSQL advisory locks;
transaction pooling does not preserve the required session.
cp apps/server/.env.example apps/server/.envSet these server-only values:
DATABASE_URL=<direct Neon connection string>
R2_ACCOUNT_ID=<Cloudflare account ID>
R2_ACCESS_KEY_ID=<R2 access key>
R2_SECRET_ACCESS_KEY=<R2 secret>
R2_BUCKET_NAME=<private bucket name>
GOOGLE_AI_STUDIO_API_KEY=<server-only key>
CORS_ORIGINS=http://localhost:3001
DEMO_MODE=private
DEMO_RESET_ENABLED=trueThe R2 endpoint is derived from R2_ACCOUNT_ID; no R2 secret belongs in
apps/web/.env.
pnpm --filter server db:migrate
pnpm --filter server queue:migrate
pnpm --filter server mastra:bootstrap
SEED_USER_PASSWORD='local-only-password' pnpm --filter server db:seedIf Mastra bootstrap hangs, verify that DATABASE_URL is direct/unpooled and
inspect Neon for a stale advisory-lock holder before terminating it.
pnpm dev:local
pnpm --filter server workerUpload one of the PDFs from examples/invoices/, then follow the complete
walkthrough in DEMO_GUIDE.md.
| Mode | Intake | Reset | Intended use |
|---|---|---|---|
private |
Arbitrary authenticated PDF uploads | Requires DEMO_RESET_ENABLED=true |
Local or access-controlled hosted walkthrough |
public |
Server-known fixture IDs only at the API boundary | Enabled for the public demo | Synthetic public deployment after a fixture-picker client is available |
Current browser intake is file-upload based. Keep DEMO_MODE=private for the
hosted browser walkthrough unless the deployment provides a fixture-picker
client for the public fixture API.
Recommended safety settings:
DEMO_EXTRACTION_QUOTA_PER_ACCOUNT=20
DEMO_EXTRACTION_QUOTA_PER_IP=5
DEMO_QUOTA_PERIOD_HOURS=24
DEMO_INTAKE_RATE_LIMIT_MAX=20| Variable | Purpose |
|---|---|
DATABASE_URL |
Full PostgreSQL connection string; use direct Neon URL for Mastra |
DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, DATABASE_DB |
Components used to construct a URL when DATABASE_URL is absent |
HOST, PORT |
API bind address and port |
CORS_ORIGINS |
Exact browser origins allowed to call the API |
DATABASE_POOL_MAX |
PostgreSQL pool size |
| Variable | Purpose |
|---|---|
DOCUMENT_STORAGE_ROOT |
Local private document root |
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME |
Enable private Cloudflare R2 storage |
DOCUMENT_MAX_BYTES |
Maximum document size; default 10 MiB |
DOCUMENT_MAX_PAGES |
Maximum PDF pages; default 5 |
| Variable | Purpose |
|---|---|
GOOGLE_AI_STUDIO_API_KEY |
Preferred direct Gemini provider; server/worker only |
GOOGLE_AI_STUDIO_MODEL |
Gemini model; default gemini-2.5-flash |
GOOGLE_AI_STUDIO_TIMEOUT_MS |
Gemini timeout; default 60 seconds |
OPENROUTER_API_KEY |
Optional alternate provider |
OPENROUTER_PRIMARY_MODEL, OPENROUTER_FALLBACK_MODEL |
OpenRouter model chain |
Without a provider key, processing uses the deterministic extractor path used for offline development and tests.
| Variable | Purpose |
|---|---|
DEMO_MODE |
private for arbitrary authenticated uploads; public for fixture-only API intake |
DEMO_RESET_ENABLED |
Allows the authenticated Administrator reset in private mode |
DEMO_EXTRACTION_QUOTA_PER_ACCOUNT |
Per-account extraction quota |
DEMO_EXTRACTION_QUOTA_PER_IP |
Per-IP extraction quota |
DEMO_QUOTA_PERIOD_HOURS |
Quota period |
Only VITE_ values may enter the web bundle. Keep all database, R2, and model
credentials in the server/worker environment.
# Run applications
pnpm dev # Docker Compose stack
pnpm dev:local # Direct web and API processes
pnpm dev:web
pnpm dev:server
pnpm --filter server worker
# Quality gates
pnpm lint
pnpm typecheck
pnpm test -- --run
pnpm build
# Focused checks
pnpm test:web -- --run
pnpm test:server -- --run
E2E=1 pnpm test:e2e
# Database
pnpm --filter server db:generate
pnpm db:migrate
pnpm db:migrate:test
pnpm db:push # Disposable local exploration only
pnpm db:studioReview generated migration SQL before applying or committing it. Do not reuse production data for test databases.
trestle/
├── apps/server/ # Hono API, worker, Drizzle, repositories, tests
├── apps/web/ # React SPA, routes, features, and UI
├── examples/invoices/ # Synthetic PDFs for the demo walkthrough
├── config/ # Shared TypeScript configuration
├── docker-compose.yml # Local development stack
├── docker-compose.prod.yml # Production-shaped template
├── DEMO_GUIDE.md # Click-by-click scenario runbook
└── README.md # Project overview and setup
- Web tests use Testing Library and MSW at the network boundary.
- Server route tests use Hono clients and fresh in-memory adapters.
- PostgreSQL, queue, workflow, and storage semantics require integration tests.
- End-to-end browser scenarios run with
E2E=1against a running stack. - Live model evaluation is manual and is not part of CI.
Browser SPA
│
▼
Hono API ───────► PostgreSQL (authoritative business state)
│ ▲
▼ │
Private document store │
│
Node worker ─────────────┘
│
├── extraction provider
├── Mastra workflow checkpoints
├── Graphile Worker jobs
└── simulated Accounting System
The API and worker are separate processes from the same server codebase. The worker prepares documents, extracts untrusted model output, validates it with deterministic rules, resumes suspended workflows, and performs idempotent posting. Mastra checkpoints execution; PostgreSQL owns financial authority.
Before publishing a synthetic demo:
- Use a separate managed database and private document bucket.
- Use HTTPS and set exact production
CORS_ORIGINS. - Use a direct/unpooled Neon URL for migrations and Mastra bootstrap.
- Pass provider keys only to API/worker server environments.
- Keep
DEMO_MODE=privatefor arbitrary uploads, or provide a fixture picker before switching to public fixture-only mode. - Set extraction quotas and a provider spending limit.
- Verify Administrator reset, AP posting, Finance approval, duplicate handling,
and rejection using
DEMO_GUIDE.md. - Never expose database credentials, R2 secrets, model keys, sessions, signed URLs, or real documents.
Trestle does not include registration, multi-organization tenancy, email ingestion, batch upload, vendor or Purchase Order creation, three-way matching, currency conversion, real ERP integration, payments, billing, mobile apps, or autonomous financial decisions.