Purchase requisition workflow for K-12 school districts.
Self-hosted. Open source. Built for districts too small for Skyward or Tyler ERP but tired of paper forms and email chains.
ReqFlow turns purchase requests into a tracked, auditable, multi-stage approval workflow. Staff submit a requisition with line items, budget codes, and a vendor. The request routes through site approval, business manager review, optional board approval, and lands at the procurement step where a PO can be generated and items received against. Every step is signed, time-stamped, and tied to an audit log.
It's designed for the typical small-to-mid district setup: a few principals approving site-level requests, a business manager doing district approval and PO generation, and a regional finance system for actual general ledger accounting.
- Multi-stage approvals — Configurable site, district, and board approval stages with department-routed approvers and digital signatures
- Budget encumbrance ledger — Every transaction (allocation, encumbrance, release, expenditure, adjustment, transfer) recorded in a single canonical ledger with decimal-precision math
- Amazon PunchOut — Full cXML PunchOut integration. Browse Amazon Business catalogs from inside the app and bring carts back as line items
- ESIGN-compliant signatures — Drawn signatures captured with IP, user agent, content hash, consent text, and signer email per approval stage
- Scheduled reminders — Approvers get itemized digests on an escalating cadence (day 3, day 5, daily after day 7) so nothing sits too long
- Receiving and shipment tracking — Per-line receiving status with UPS/FedEx/USPS tracking integration
- Real-time collaboration — Comments, watchers, typing indicators, and shared requisition views via SSE
- PDF and Excel export — Branded POs and audit reports
- Granular permissions — Permission groups with per-action toggles on top of USER / BUSINESS_MANAGER / ADMIN roles
- Dark mode across the entire app
| Layer | Tools |
|---|---|
| Framework | Next.js 16 (App Router), React 19, TypeScript |
| Database | PostgreSQL 16, Prisma 7.8 |
| Auth | NextAuth.js v5 (Google OAuth) |
| UI | Tailwind CSS v4, Recharts, Lucide icons |
| Financial math | decimal.js |
| Nodemailer 8 (SMTP) | |
| Testing | Vitest, Testing Library, vitest-mock-extended |
| Deploy | Docker, supercronic (for in-container cron) |
| CI | GitHub Actions (lint, test, build) |
Prerequisites: Node.js 20+, PostgreSQL 14+, Google OAuth credentials.
# 1. Clone and install
git clone https://github.com/tgunn-dev/ReqFlow.git
cd ReqFlow
npm install
# 2. Configure environment
cp .env.example .env # if available, otherwise create manuallyCreate a .env file:
DATABASE_URL=postgresql://user:password@localhost:5432/reqflow
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET= # generate: openssl rand -base64 32# 3. Set up the database
npx prisma migrate deploy
npx prisma generate
# 4. Run the dev server
npm run devOpen http://localhost:3000. The first user to sign in is automatically promoted to Admin — sign in with your Google account before anyone else does.
ReqFlow ships with a multi-stage Dockerfile and two compose files. The production setup includes:
- App container running Next.js standalone
- PostgreSQL with persistent volume
- Internal network isolation (DB not exposed)
- Health checks, resource limits, memory caps
- Persistent volume for uploaded attachments
- In-container cron daemon (supercronic) for scheduled reminders
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -base64 32 # CRON_SECRET (enables scheduled reminders)POSTGRES_PASSWORD=strong-password-here
NEXTAUTH_URL=https://requisitions.yourdomain.com
NEXTAUTH_SECRET=<from openssl>
AUTH_GOOGLE_ID=<google-client-id>
AUTH_GOOGLE_SECRET=<google-client-secret>
CRON_SECRET=<from openssl>
# Optional
REMINDER_SCHEDULE=0 8 * * 1-5 # Default: weekdays at 8am
TZ=America/Chicago # Default
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=noreply@yourdomain.com
# Optional: Amazon PunchOut
AMAZON_PUNCHOUT_URL=
AMAZON_ORDER_URL=
AMAZON_FROM_IDENTITY=
AMAZON_TO_IDENTITY=
AMAZON_SHARED_SECRET=docker compose -f docker-compose.prod.yml --env-file .env.production up -d --buildThe container runs migrations on startup, then launches both the Next.js server and the supercronic cron daemon.
app/
actions/ Server Actions organized by domain (requisition, finance, admin, etc.)
api/ API routes (auth, PunchOut webhooks, cron, exports, v1 REST API)
admin/ Admin pages (role-protected by middleware and per-page checks)
dashboard/ User-facing pages
components/ React components (server and client)
data/ Read-side data fetching (Prisma queries)
services/ Business logic (budget operations, order processing)
lib/ Core utilities (Prisma client, audit, financial math, email)
prisma/ Schema, migrations
tests/ Unit and integration tests (mocked Prisma)
DRAFT ──► SUBMITTED ──► APPROVED_SITE ──► APPROVED_DISTRICT ──► ORDERED ──► RECEIVED ──► CLOSED
│ ▲
└─► READY_FOR_BOARD ─┘
◄── Return for Revision ──
✗ REJECTED
The flow is configurable: any of site, district, or board approval can be disabled per district policy. Self-approval can be allowed or blocked per district. The board approval threshold is a dollar amount.
Settings are managed through the admin UI at /admin/settings:
- Approval workflow — Which stages are required, the board approval threshold, self-approval policy, same-approver-multiple-levels policy
- Requisition rules — Required fields, line item limits, justification text requirements, over-budget blocking
- Email/SMTP — Server, port, credentials, from address (or use env vars)
- Notifications — Per-event email and in-app toggles
- Shipping carriers — UPS, FedEx, USPS API credentials for tracking
- Shipping budget code — Which code overhead (shipping, tax) gets encumbered against
Departments (/admin/org/departments) hold their own approver assignments — each department has a default site approver and business manager. Changing them notifies the new approver about any inherited pending requisitions.
A cron daemon inside the container hits /api/cron/reminders on the configured schedule. Default is weekdays at 8am Central. The endpoint sends three streams of notifications:
| Audience | Trigger |
|---|---|
| Approvers | Day 3, day 5, then daily once items pass 7 days |
| Business managers | When fully-approved requisitions are waiting for PO creation |
| Requesters | Day 7, then weekly once items pass 14 days |
Test manually:
curl -H "x-cron-secret: $CRON_SECRET" https://yourdomain.com/api/cron/reminders- ESIGN Act / UETA compliance: Each approval signature captures the signer's name, email, IP address, user agent, timestamp, consent text, and a SHA-256 hash of the requisition's material content at signing. The PDF export includes an audit certificate with these fields.
- Audit log: Every mutation runs through
lib/audit.tsand is recorded with actor, action, entity type/ID, change diff, IP, and user agent. - Soft delete: Requisitions are soft-deleted, not destroyed. Encumbered funds are released on delete and re-encumbered (via DRAFT reset) on restore.
- Permission system: Database-backed permissions with grouped templates. Permission cache invalidates on change.
- Rate limiting: Sensitive endpoints (file upload, etc.) are rate-limited per IP.
| Command | Description |
|---|---|
npm run dev |
Start dev server (turbopack) |
npm run build |
Production build |
npm run start |
Start production server |
npm run lint |
Run ESLint |
npm test |
Run tests in watch mode |
npm run test:run |
Run tests once |
npm run test:coverage |
Run tests with V8 coverage |
npm run db:seed |
Seed database with sample data |
npm run db:reset |
Drop and recreate the database |
Tests use Vitest with a deep-mocked Prisma client — no database required.
npm run test:run # one shot
npm run test:coverage # with coverage
npm run test:ui # interactive UICurrent coverage: 331 tests across 10 files, covering financial calculations, approval workflows, budget operations, and PunchOut order confirmation parsing.
Schema changes:
npx prisma migrate dev --name describe_the_change
npx prisma generateProduction deployments:
npx prisma migrate deployThe Docker entrypoint runs migrate deploy automatically on container start.
MIT — See LICENSE for details.