Architecture

How the system is structured and how its components interact.

Frontend

Built with Next.js (App Router) and Tailwind CSS. Pages are split into a server component that fetches data and a Page client component that handles interactivity. User identity is managed in a React context (UserContext) backed by localStorage — no session tokens or cookies are used.

The header renders two navigation rows: one for all users and a second admin-only row that is conditionally shown based on the current user's role.

API Layer

All mutations and data reads go through Next.js Route Handlers under src/app/api/. Key routes:

  • PATCH /api/expenses/[id]Updates a single expense field: manager approval, approved-by user, manual approval verdict, final reviewer, or core fields.
  • POST /api/batch-analyzeAccepts an array of expense IDs, fetches each from the database, and dispatches them to the Python compliance agent one at a time. Writes the resulting analysis back to expense_analyses.
  • POST /api/analyze-expenseSingle-expense compliance entrypoint used for ad-hoc analysis. Also handles receipt parsing via a Claude vision call.
  • GET/POST /api/usersLooks up a user by login ID or creates a new account.
  • GET/POST /api/policiesLists policy documents or uploads a new one and triggers re-indexing.

Database

PostgreSQL hosted on Neon (serverless). The schema has four main tables:

  • usersAccounts with roles: active, inactive, or admin.
  • expensesSubmitted expense reports. Tracks amounts, category, vendor, manager approval, approved-by user, final reviewer, and manual approval decision.
  • expense_analysesOne analysis row per expense. Stores the AI verdict (APPROVED / FLAGGED / DENIED), reasoning, policy citations, confidence score, and the model used.
  • promptsEditable prompt templates. Only one row per named prompt may be active at a time.

The app connects via a tagged-template SQL helper (executeSql) that parameterises all queries.

Compliance Agent

Written in Python (src/scripts/compliance.py). The Next.js API spawns a short-lived Python subprocess per expense and reads the JSON result from stdout.
Update: this has all been transcoded to typescript

  1. 1
    RAG retrieval

    The expense details are used as a query against a ChromaDB vector store to retrieve the most relevant policy excerpts. Only chunks from the currently active policy version are considered.

  2. 2
    Prompt construction

    Retrieved excerpts are injected into the active compliance prompt template alongside the expense fields (amount, category, vendor, description, client charge flag, manager approval flag).

  3. 3
    Claude analysis

    The prompt is sent to Claude (claude-opus-4-8) with adaptive thinking enabled. The model returns a JSON object with verdict, reasoning, policy citations, and a confidence score.

  4. 4
    Result storage

    The API layer parses the JSON response and writes a row to expense_analyses. Flagged expenses surface in the Needs Manual Review queue for admin decision.

Policy Indexing

Policy documents are uploaded as plain text, chunked, and embedded into ChromaDB Cloud. Each chunk is stored with a version number so the compliance agent can target only the current policy version during retrieval. Uploading a new document increments the version.