Skip to content

Repository files navigation

Trestle

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.

Start here

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

Product

Roles

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.

Workflow

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.

Product boundaries

  • 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.

Quick start

Prerequisites

  • Node.js 22 or newer
  • pnpm 10
  • Docker with Docker Compose for the recommended local stack

Option A: Docker Compose

This is the simplest local demonstration. It uses local PostgreSQL and local private document storage.

cp .env.example .env
pnpm install
pnpm dev

Open:

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:seed

The 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 down

Delete the local database volume only when its data is disposable:

docker compose down -v

Option B: Direct local processes

Use this when PostgreSQL already runs outside Compose.

cp apps/server/.env.example apps/server/.env
cp apps/web/.env.example apps/web/.env
pnpm install

Configure 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:seed

Start the web/API processes and worker in separate terminals:

pnpm dev:local
pnpm --filter server worker

pnpm dev:local starts the web and API. It does not start the worker.

Managed-service setup

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.

1. Create the services

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.

2. Configure the server

cp apps/server/.env.example apps/server/.env

Set 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=true

The R2 endpoint is derived from R2_ACCOUNT_ID; no R2 secret belongs in apps/web/.env.

3. Bootstrap Neon and seed the demo

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:seed

If Mastra bootstrap hangs, verify that DATABASE_URL is direct/unpooled and inspect Neon for a stale advisory-lock holder before terminating it.

4. Start all local processes

pnpm dev:local
pnpm --filter server worker

Upload one of the PDFs from examples/invoices/, then follow the complete walkthrough in DEMO_GUIDE.md.

Demo modes and safety

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

Configuration by concern

Database and runtime

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

Document storage

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

Extraction

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.

Demo controls

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.

Development

Commands

# 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:studio

Review generated migration SQL before applying or committing it. Do not reuse production data for test databases.

Repository layout

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

Testing boundaries

  • 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=1 against a running stack.
  • Live model evaluation is manual and is not part of CI.

Architecture at a glance

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.

Deployment checklist

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=private for 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.

Known boundaries

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.

About

Human-governed document operations for supplier invoice processing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages