Skip to content

cpatpa/PIP

 
 

Repository files navigation

PIP

PIP is an internal legal AI assistant for Piper Alderman. It lets fee earners upload legal documents, chat about them with AI models, propose and accept tracked-change edits, run reusable workflows, and extract structured data into spreadsheet-style tabular reviews.

Forked from the upstream "Mike" project and re-architected for internal self-hosting at PA.

Stack

  • Frontend Next.js 16, React 19, Tailwind 4, Auth.js v5
  • Backend Express + TypeScript on Node 20
  • Database PostgreSQL 16 (plain pg; no ORM, no Supabase)
  • Auth Auth.js (Credentials + Microsoft Entra OIDC), HS256 JWT shared with the backend
  • Storage local filesystem with AES-256-GCM at rest (default), S3-compatible (R2 / MinIO) as opt-in
  • AI Anthropic, Gemini, OpenAI as external providers; local Ollama (or any OpenAI-compatible endpoint) for sovereign matters, with an organisation switch to disable external providers entirely
  • Deploy Docker Compose with host-mounted persistent volumes, Caddy reverse proxy, nightly pg_dump sidecar

The full architecture map is at docs/developer/01-architecture.md.

Repository layout

PIP/
├── Instructions.md                    project conventions and decisions log
├── CHANGELOG.md                       running record of every change
├── docs/                              audience-grouped documentation
│   ├── developer/                     architecture, schema, migrations, auth
│   ├── security/                      threat model and cryptography
│   ├── operator/                      deployment and ops (Phase 7+)
│   ├── admin/                         admin-facing guides (Phase 4+)
│   └── user/                          end-user guides
├── backend/                           Express + pg
│   ├── migrations/                    SQL migrations applied in order
│   └── src/
└── frontend/                          Next.js + Auth.js

Deploying

The guided installer takes a clean Ubuntu Server 22.04+ or Debian 12+ from nothing to a running PIP deployment in one command:

sudo apt update && sudo apt install -y git
sudo git clone <repo-url> /opt/pip
cd /opt/pip
sudo bash install.sh

The installer:

  1. Detects the OS and installs whiptail, curl, openssl, gnupg, ufw, then Docker Engine + the Compose plugin.
  2. Runs a TUI wizard collecting:
    • Public hostname (any DNS name or LAN address works)
    • TLS mode: Caddy internal CA (self-signed, default, works without DNS), Let's Encrypt (public DNS required), or HTTP only (LAN testing).
    • Bootstrap admin email (first user signing up with this email becomes admin automatically).
    • Data root (default /srv/pip).
    • Postgres password (auto-generated by default).
    • Email (Resend) - skipable.
    • External AI policy and per-provider keys.
    • Microsoft Entra OIDC - skipable.
    • Ollama mode: bundled (run Ollama as a container in this stack), remote (point at an existing Ollama server URL), or none. The bundled service is gated by a Docker Compose profile so a remote / none install never starts the local container.
    • Ollama starter model (bundled mode only).
  3. Generates AUTH_SECRET, USER_API_KEYS_ENCRYPTION_SECRET, DOWNLOAD_SIGNING_SECRET, and STORAGE_ENCRYPTION_KEY automatically.
  4. Provisions ${DATA_ROOT}/{postgres,storage,ollama,caddy,backups} with the right ownership for the backend container (uid 10001) and the backup sidecar (uid 70).
  5. Writes .env.compose (mode 0600), the runtime ${DATA_ROOT}/caddy/Caddyfile, and a secrets backup at ${DATA_ROOT}/secrets-backup.txt (mode 0400). Copy the secrets backup off the server - losing STORAGE_ENCRYPTION_KEY makes every uploaded document permanently unreadable.
  6. Opens UFW ports 80/443 if UFW is active.
  7. Builds the backend + frontend images, brings up the stack, waits for /ready, optionally pulls the chosen Ollama model.

Idempotent. Re-run sudo bash install.sh at any time to change domain, TLS mode, AI providers, or any other setting.

Upgrade. After a git pull or any code change:

sudo bash update.sh

Manual one-shot backup. The compose stack runs a nightly pg_dump sidecar; for an ad-hoc dump:

sudo bash bin/backup-now.sh

Health check / control panel. One command prints service status, container resource usage (CPU, memory, network I/O), host disk usage per data subdir, public-URL probe, and (when run interactively) drops into a menu for restart / log / reset-admin actions:

sudo bash bin/pip-status.sh              # status + interactive menu
sudo bash bin/pip-status.sh --status     # one-shot, no menu
sudo bash bin/pip-status.sh --restart backend
sudo bash bin/pip-status.sh --restart-all
sudo bash bin/pip-status.sh --watch      # refresh every 2 seconds

Reset the bootstrap admin password. Locked out, or rotating after first sign-in:

sudo bash bin/pip-reset-admin.sh             # interactive
sudo bash bin/pip-reset-admin.sh --generate  # auto-generate, print once
sudo bash bin/pip-reset-admin.sh --password '<your-new-12+-char-password>'

The script hashes the new password via the backend container's bcrypt (cost 12, same as the application), UPDATEs the bootstrap user's password_hash, clears any pending reset tokens, mirrors the new value into ${DATA_ROOT}/secrets-backup.txt + .env.compose, and writes a user.password.reset audit event.

Nuke a test deployment. Tears down the stack, deletes ${DATA_ROOT} (postgres, storage, ollama, caddy, backups, and the secrets backup), removes .env.compose, and optionally drops the built images. Irreversible. Intended for test hosts and pre-go-live rebuilds, not for production:

sudo bash bin/pip-uninstall.sh                # interactive, prompts before each destructive step
sudo bash bin/pip-uninstall.sh --yes          # non-interactive (scripted runs)
sudo bash bin/pip-uninstall.sh --keep-data    # tear down containers but keep ${DATA_ROOT}
sudo bash bin/pip-uninstall.sh --keep-images  # keep built backend/frontend images
sudo bash bin/pip-uninstall.sh --prune        # also `docker system prune -af --volumes`

After running, sudo bash install.sh does a fresh end-to-end install.

The compose services are postgres, backend, frontend, ollama (local AI), caddy (reverse proxy + TLS), and backup. Every persistent volume mounts under ${DATA_ROOT} on the host so containers can be replaced without losing data. The backend entrypoint runs pending SQL migrations on every boot.

Local development

If you'd rather run the apps directly on your laptop without Docker:

Prerequisites

  • Node.js 20 or newer
  • npm
  • git
  • PostgreSQL 16 with a database PIP can own
  • A Resend API key (for password-reset email; optional in development)
  • LibreOffice on the host PATH if you want DOCX -> PDF conversion
  • Optional: an S3-compatible bucket (R2 or MinIO) if you prefer object storage to the AES-encrypted local filesystem driver

1. Database

createdb pip
createuser pip --pwprompt
psql -c "GRANT ALL PRIVILEGES ON DATABASE pip TO pip; ALTER ROLE pip BYPASSRLS;"

BYPASSRLS is required because every backend-owned table has FORCE ROW LEVEL SECURITY enabled as a defence-in-depth measure. The application is the access-control boundary. See docs/developer/08-migrations.md.

2. Environment files

Create backend/.env:

PORT=3001
FRONTEND_URL=http://localhost:3000

DATABASE_URL=postgres://pip:your-password@localhost:5432/pip

# Generate secrets with: openssl rand -hex 32
AUTH_SECRET=...                              # shared with frontend
USER_API_KEYS_ENCRYPTION_SECRET=...          # required
DOWNLOAD_SIGNING_SECRET=...                  # required

# Optional: bootstraps the admin role on this email at first sign-in
BOOTSTRAP_ADMIN_EMAIL=you@piperalderman.com.au

# Model providers (only those you intend to use)
ANTHROPIC_API_KEY=...
GEMINI_API_KEY=...
OPENAI_API_KEY=...

# Outbound email (password reset). Required in production.
RESEND_API_KEY=...
# RESEND_FROM="PIP <no-reply@piperalderman.com.au>"

# Storage. The default driver writes AES-256-GCM encrypted files to
# STORAGE_LOCAL_PATH; STORAGE_ENCRYPTION_KEY is required (32+ chars).
STORAGE_DRIVER=local
STORAGE_LOCAL_PATH=./local-storage
STORAGE_ENCRYPTION_KEY=...

# Opt-in S3-compatible mode (R2 / MinIO) instead of the local driver
# STORAGE_DRIVER=s3
# R2_ENDPOINT_URL=...
# R2_ACCESS_KEY_ID=...
# R2_SECRET_ACCESS_KEY=...
# R2_BUCKET_NAME=pip

# Optional Entra OIDC (omit to use Credentials only)
# AUTH_MICROSOFT_ENTRA_ID_ID=...
# AUTH_MICROSOFT_ENTRA_ID_SECRET=...
# AUTH_MICROSOFT_ENTRA_ID_TENANT_ID=...

Create frontend/.env.local:

NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
AUTH_SECRET=...                              # same value as backend
# AUTH_URL=http://localhost:3000             # required in production

# Entra (same three values as backend; both sides need them)
# AUTH_MICROSOFT_ENTRA_ID_ID=...
# AUTH_MICROSOFT_ENTRA_ID_SECRET=...
# AUTH_MICROSOFT_ENTRA_ID_TENANT_ID=...

The frontend env file holds no privileged secrets other than AUTH_SECRET, which is also held by the backend.

3. Install and migrate

npm install --prefix backend
npm install --prefix frontend
npm run migrate --prefix backend

The migration runner applies every file in backend/migrations/ in order and records progress in a pip_migrations tracking table. See docs/developer/08-migrations.md.

4. Run

npm run dev --prefix backend
npm run dev --prefix frontend

Open http://localhost:3000, register the BOOTSTRAP_ADMIN_EMAIL account, complete the onboarding wizard, and you're in.

First-run notes

  • The first user with the email matching BOOTSTRAP_ADMIN_EMAIL is promoted to admin on registration. Any other email matching the organisation's allowed_email_domains policy (default piperalderman.com.au) signs up as a regular user.
  • The onboarding wizard collects display name, office, jurisdictions, practice services, sectors, and an AI-disclaimer acknowledgement.
  • The default org system prompt is preconfigured with AU English, AGLC4 citation conventions, and hallucination guards. Admins can edit this in the admin AI Policy panel (Phase 4).
  • Account > Custom Instructions accepts additive personal notes that the assistant follows on top of the organisation's instructions.

Troubleshooting

AUTH_SECRET is required on boot. Generate a 32-byte hex value with openssl rand -hex 32 and put the same value in both backend/.env and frontend/.env.local.

USER_API_KEYS_ENCRYPTION_SECRET is required and must be at least 32 characters. Same fix; this protects user-supplied model provider keys at rest.

Migrations apply but queries return zero rows. The Postgres role PIP connects with must have BYPASSRLS (RLS is enabled defence-in- depth on every table). Run ALTER ROLE pip BYPASSRLS; as a superuser.

Password-reset email never arrives. In development without RESEND_API_KEY, the email body is written to the backend log; copy the link from stdout. In production set RESEND_API_KEY.

DOC or DOCX conversion fails. Install LibreOffice locally and restart the backend so soffice is on the process path. From Phase 7 this runs in a sandboxed converter container.

The model picker shows a missing-key warning. Add a key for that provider in Account > Models & API Keys, or configure it in backend/.env and restart.

Useful checks

npm run build --prefix backend
npm run build --prefix frontend
npm run lint --prefix frontend

Roadmap

The phased build plan. Phases are tackled in order; items move from "Outstanding" to "Done" as each phase completes.

Done

Phase 0 - Security quick wins and housekeeping

Foundation docs, threat model, PR template, audit-finding fixes that don't need architectural change: dead-secret removal, JSON DoS cap, LibreOffice timeout, DOCX zip/XML bomb guards, CORS allowlist, strict CSP, multer 2.x, generic error responses, production log redaction.

Phase 1 - Rebrand to PIP

Replaced "Mike" everywhere (UI copy, system prompts, defaults, page titles, file names) and ran an AU English copy sweep at the same time. Brand tokens (pip-red, pip-navy) wired into the Tailwind theme. Placeholder logo pending the official PA asset.

Phase 2 - Postgres + Auth.js

The largest single change. All of:

  • pg connection pool and in-house SQL migration runner with a pip_migrations tracking table; no ORM.
  • Full Phase 2 schema: 19 application tables defined entirely by migrations. Every user_id is a uuid foreign key to public.users with ON DELETE CASCADE. Row-level security enabled (forced) on every backend-owned table.
  • org_settings seeded with the canonical PA org system prompt (AU English, AGLC4 citations, no-fabrication and flag-uncertainty guards, AI reminder), 10 AU jurisdictions, the 17 PA services and 16 PA sectors from piperalderman.com.au, the piperalderman.com.au email-domain allowlist, and a 7-year retention default.
  • audit_events append-only audit trail with ON DELETE SET NULL on the actor so history survives user deletion.
  • Auth.js v5 on the frontend with two providers wired:
    • Credentials (email + bcrypt at cost 12, minimum 12-char passwords).
    • Microsoft Entra ID OIDC, enabled when the three AUTH_MICROSOFT_ENTRA_ID_* env vars are set. Conflict and domain-allowlist rejection both 401.
  • HS256 JWT shared between frontend and backend via AUTH_SECRET. Auth.js session cookie stays httpOnly; the browser obtains a five-minute backend bearer via GET /api/session-token.
  • Backend requireAuth middleware verifies the JWT via jose. New requireAdmin checks role.
  • BOOTSTRAP_ADMIN_EMAIL env auto-promotes the first match.
  • Onboarding wizard (display name, office, jurisdictions, services, sectors, AI-disclaimer acknowledgement). Mandatory after first sign-in; middleware redirects until completed.
  • Account > Custom Instructions panel with a 4096-character cap. Additive only; never overrides the organisation's instructions.
  • Password reset flow via Resend with a one-hour single-use HMAC token (SHA-256 of the raw value stored).
  • Every backend route migrated off the Supabase JS client onto plain pg with parameterised SQL. @supabase/supabase-js uninstalled from both apps.
  • Account deletion enforces re-auth (password for Credentials, email-typing for Entra-only) and tears down storage objects before the cascade runs.

Audit findings closed: C3, H1, H2, H3, H4 (timeout), H5, H7, M1, M3, M4, M5, M6, L1, L5.

Phase 3 - Workspaces and roles

Workspaces sit above users and below the organisation. Every user gets an auto-provisioned Personal workspace via the ensure_personal_workspace trigger; the partial unique index pins each user to exactly one. Owner / admin / member role ladder with last-owner protection, Personal-workspace protection, and an email-to-user-id invite path. AI policy fields (instructions, allow_external_models, retention_days) on the workspace override the org defaults when set.

Every workspace-scoped resource (projects, chats, tabular_reviews, workflows) carries workspace_id. Chats and reviews inherit the parent project's workspace when project-scoped. GET /projects?workspace_id= scopes both own and shared branches.

The legacy projects.shared_with and tabular_reviews.shared_with JSONB email lists are replaced by project_members and review_members joined on user_id. Backfill loops migrated every existing email to its matching active user; unmatched emails are dropped and reported as NOTICE. The legacy JSONB columns were later dropped in migration 0018 once the members tables had proven stable in production paths.

Phase 4 - Admin backend

Backend admin API at /admin/*, gated by requireAdmin:

  • GET / PATCH / DELETE /admin/users (last-admin protection, self-mutation blocked).
  • GET / PATCH /admin/org-settings (typed validation per field, metadata recorded in audit).
  • GET /admin/audit (paginated, optional action and user_id filters). Frontend admin pages land separately; the API is complete.

Phase 5 - Storage driver + at-rest encryption + download token hardening

New StorageDriver selector keyed on STORAGE_DRIVER:

  • local (default for Docker): writes files to ${STORAGE_LOCAL_PATH} with AES-256-GCM. Each file on disk is [12-byte IV][16-byte GCM tag][ciphertext]; tampering is detected on read. Path-traversal defence: keys are resolved against the configured root and refused if they escape.
  • s3: existing R2 / MinIO behaviour, opt-in via the R2 env vars.

STORAGE_ENCRYPTION_KEY is required at runtime for the local driver (minimum 32 chars). The download-token format gains userId and exp claims; /download/:token refuses tokens that belong to a different user or have passed their expiry (default 24 h, tunable via DOWNLOAD_TOKEN_TTL_SECONDS). Closes audit finding C1.

Phase 6 - Local LLM provider + EXTERNAL_AI_DISABLED

New local provider in the LLM adapter (local/<model-id> routes to any OpenAI-compatible Chat Completions endpoint, default http://ollama:11434/v1). completeText and streamChatWithTools both dispatch. Tool support was added in Phase 9.

EXTERNAL_AI_DISABLED=true (env) refuses every dispatch to Anthropic, Gemini, and OpenAI at the runtime layer. Local models always pass. The same gate also honours org_settings.allow_external_models and the per-workspace override.

Phase 7 - Docker and deployment

Multi-stage Dockerfiles for backend and frontend. Next.js builds with output: 'standalone'. docker-compose.yml services: postgres, backend, frontend, ollama, caddy (TLS + reverse proxy), backup (nightly pg_dump + retention sweep). Every persistent volume mounts under ${DATA_ROOT}. Backend entrypoint runs migrations on boot. /health and /ready endpoints are in place (the latter pings Postgres). Closes audit finding H4 (LibreOffice sandboxing follow-up still parked, see below).

Phase 8 - Final security and polish

Per-user rate limits: chat, chat-create, and upload limiters now key on res.locals.userId so one staff member's burst doesn't drain the bucket for the whole firm sitting behind a corporate NAT. Pre-auth requests still fall back to IP. Closes audit finding H6.

structure_tree titles extracted from uploaded PDFs and DOCX are sanitised (control chars stripped, angle brackets escaped, length capped) before storage and render. Closes audit finding M2.

Phase 9 - Memory (persistent user facts)

Per-user durable facts ("I work in commercial litigation, NSW") injected into the system prompt between user custom instructions and the chat message. Migration 0022_user_memories.sql. Soft cap of 50 entries per user enforced inside a single SELECT FOR UPDATE transaction so concurrent saves can't bypass the LRU eviction.

LLM tool add_memory(content) for model-driven capture with an in-chat confirmation when a memory is saved. Account > Memories tab supports review, edit, pin, delete, export. org_settings.allow_memory master switch; default off until reviewed. Audit events for create, update, delete, clear-all.

Shipped alongside the layered system prompt in backend/src/lib/promptAssembly.ts. Compose order: org prompt, workspace override, user custom instructions, memory block, operational footer. org_settings.use_layered_prompt is the emergency rollback to the legacy hardcoded assembly.

Phase 10 - Web search

Admin-gated retrieval from the open web. Pluggable provider interface; default is Brave Search (clean ToS, single env var), with SearXNG self-hosted as the sovereign alternative (Docker Compose profile web-search). Migrations 0023_web_search.sql and 0025_web_search_credentials.sql; admin-saved API keys are encrypted at rest with STORAGE_ENCRYPTION_KEY.

LLM tools web_search(query) and fetch_url(url) with a 24-hour fetch cache, size and timeout caps, content-type allowlist, and an SSRF-safe undici fetcher that resolves the host, validates the IP isn't in a private range, and pins the connection to that exact IP. Per-chat toggle and an external-AI-style banner when active. Audit events for every search and fetch.

org_settings.allow_web_search master switch; per-workspace override. Domain blocklist as the primary filter, seeded with paste sites, unmoderated forums, and *.onion. Optional allowlist for tightly scoped configurations. Citations rendered with the existing cite-button.

Phase 11 - Groups and granular permissions

RBAC on top of the Phase 3 workspace model. Migration 0024_groups.sql: groups, group_members, capability set in permissions, group_permissions. project_members and review_members extended to accept either a user id or a group id.

Auto-managed system groups ("All members", "Admins") maintained by Postgres triggers that read a session-local GUC (pip.system_group_sync) so legitimate sync writes pass while direct UI mutations of a system group are refused.

Capability resolver computes effective permissions in a single CTE query (workspace role, group memberships, direct grants) and caches per request. Admin UI: group CRUD, member management, capability editor. Migration 0026_owner_locked_caps.sql introduces the owner_locked column for capabilities only an owner may grant (e.g. admin.policy.write), enforced via a trigger reading the session-local pip.actor_role GUC.

Consumer routes migrated from inline role checks to req.can(), returning 403 with { code: 'forbidden', capability } on refusal. Hot-path indexes added in migration 0027_phase11_indexes.sql.

Audit follow-ups

Two post-Phase-11 sweeps. Tier A (6f9836a, 730a7e3): hot-path indexes, owner-locked capabilities, consumer route migration, SIGTERM handling, migration advisory lock, citation memoisation, Caddy security headers, update.sh waits for /ready, bin/pip-acceptance.sh (21-probe end-to-end check). Tier B (d4f70b0, 1e3cb08, 00b993b): shared /me context, React.memo on chat, permissionsByWorkspace N+1 collapsed, formatApiError + ApiError class, empty-state copy, aria-labels, modal focus trap (useFocusTrap), form labels, bulk accept/reject two-step confirm, loading skeletons, tabular pending-cell spinner.

Designed, not started

Each phase has a complete design doc under docs/developer/. Pick up with the same pre-implementation audit pattern Phases 9-11 used.

Phase 12 - Multi-model side-by-side

New chat mode with up to three models running in parallel against the same message history. Backend fan-out, per-model rate-limit accounting and cost capture, split-pane UI. Admin policy caps parallelism. Design: docs/developer/phase-12-multi-model.md.

Phase 13 - Vector RAG with embeddings (recommended next)

The biggest single quality lift on the roadmap. pgvector, document_chunks and document_embeddings, embedding worker on upload, hybrid vector + tsvector search, local embeddings via Ollama by default. Reuses the existing cite-button per chunk. Design: docs/developer/phase-13-vector-rag.md.

Phase 14 - Knowledge collections

UX layer on top of Phase 13: # autocomplete to scope retrieval to a named collection, project, or document. Depends on Phase 13. Design: docs/developer/phase-14-knowledge-collections.md.

Outstanding (Tier C maintenance backlog)

Real but not on the critical path. Pick up when adjacent work is in scope. Tracked in docs/developer/00-roadmap.md.

  • Structured logging with request-id correlation. Replace the printf-style backend/src/lib/logger.ts with a pino-style emitter; add req.id = uuid() middleware so an SSE error can be traced back to its chat.
  • Refactor backend/src/lib/chatTools.ts (4053 LOC) into chatTools/{memory,webSearch,documents,workflows}.ts. Pure code hygiene; no behaviour change.
  • Prometheus /metrics endpoint. Retention counts, web-search rate-limit hits, chat token usage are log lines today.
  • ProjectsOverview virtualisation (react-window). Only matters above ~500 projects per user.

Parked (post-MVP)

  • Workflow audit. The upstream ships 60+ built-in workflows; most are US-centric. A future workstream replaces them with AU and PA-relevant content, authored with practice-area-lead input.
  • LibreOffice sandboxing. Today soffice runs in the backend container under a non-root user with a hard timeout. A separate pip-converter container with stricter limits is a follow-up.
  • Automated tests. The codebase currently has no test suite. Adding one is a real gap for a legal-sector tool but is out of scope for the MVP cut.
  • Voice and dictation. Local-Whisper-only dictation may return as a small phase if user demand materialises after Phase 14. External STT/TTS rejected.
  • Image generation, user-authored plugins, anonymous chat. All considered and rejected. See docs/developer/00-roadmap.md.

License

AGPL-3.0-only. PIP is intended for internal Piper Alderman deployment. Upstream attribution is preserved in LICENSE and NOTICE.

About

OSS AI Legal Platform

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 95.0%
  • Shell 2.9%
  • PLpgSQL 1.5%
  • Other 0.6%