Approved consulting time becomes utilization, WIP dollars, a draft milestone or contract invoice, and project margin. A draft invoice does not consume WIP; time and expenses stay unbilled until the invoice is issued.
Permit rule: Realization numbers come from approved time, WIP, and contracts — not from a model. Humans operate this kernel. Same instinct: SafetyMP.
- Kimai is a time tracker. Hours, approvals, and timesheets are its product.
- This repo is realization. Approved time becomes utilization, WIP dollars, a draft milestone or contract invoice, and project margin.
- Postgres RLS is the tenancy model. Organization isolation is enforced in the database, not only in application middleware.
- Multi-tenant organizations with role-based access (Admin, Manager, Consultant)
- Clients & projects — tasks, team members, budgets, billing models (T&M, fixed fee, retainer, milestone)
- Time & expenses — approval workflows, categories, receipt uploads, bulk approval
- Resources — profiles, allocations, utilization reporting
- Billing — WIP and contract/progress invoicing, milestone billing, PDF export, journal CSV
- Accounting — chart of accounts, Xero / QuickBooks OAuth, push journals from invoices
- Reporting — profitability with contract/milestone columns, expense breakdown by category
- Row-level security — PostgreSQL RLS enforces organization isolation
| Dashboard | Invoices | Profitability | Accounting |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
- Next.js 16 (App Router) · React 19 · TypeScript 6
- PostgreSQL 16 · Prisma 7 · Auth.js / NextAuth v5
- Tailwind CSS 4 · Vitest
- Node.js 22+ (see
.nvmrc) - Docker (for local Postgres) or an existing PostgreSQL 16 instance
- npm
The one job without Postgres is realization. A draft keeps approved time as unbilled WIP (an ASC 606 contract asset). Issuing the invoice is what marks it invoiced, and voiding a draft puts it back.
npm install
npm run realizegit clone https://github.com/SafetyMP/Professional-Service-Automation.git
cd Professional-Service-Automation
cp .env.example .env
# Edit .env: set AUTH_SECRET (e.g. openssl rand -base64 32)
docker compose up -d
npm install
npm run db:migrate
npm run db:seed
npm run dev -- -p 3005
# http://localhost:3005| Field | Value |
|---|---|
| Organization | demo-firm |
| Admin | admin@demo.com / password123 |
| Manager | manager@demo.com / password123 |
Do not use demo credentials in production. Change passwords and rotate AUTH_SECRET before deploying.
See .env.example:
| Variable | Purpose |
|---|---|
DATABASE_URL |
App database URL (RLS role psa_app) |
DIRECT_URL |
Direct Postgres URL for migrations (postgres superuser) |
AUTH_SECRET |
Session signing secret (required) |
AUTH_URL |
Public app URL (e.g. http://localhost:3005) |
XERO_CLIENT_ID / XERO_CLIENT_SECRET |
Optional — Xero OAuth |
QUICKBOOKS_CLIENT_ID / QUICKBOOKS_CLIENT_SECRET |
Optional — QuickBooks OAuth |
QUICKBOOKS_ENV |
sandbox (default) or production |
Local Docker Postgres listens on port 5440 (mapped from container 5432).
For production or a one-command demo stack (Postgres + app in Docker):
export AUTH_SECRET="$(openssl rand -base64 32)"
docker compose -f docker-compose.stack.yml up --build -d
# http://localhost:3000 — demo-firm / admin@demo.com / password123See docs/deploy.md for Railway, Fly.io, manual Node deployment, and the production checklist.
| Command | Description |
|---|---|
npm run dev |
Prisma generate + Next.js dev server |
npm run build |
Production build |
npm run lint |
ESLint |
npm run typecheck |
TypeScript check |
npm run test |
Vitest unit tests |
npm run realize |
Draft holds WIP; issue realizes it; void refuses an issued invoice |
npm run tenancy |
psa_app sees no password hashes until an org or login email is set |
npm run db:migrate |
Apply Prisma migrations |
npm run db:seed |
Reset and seed demo data |
npm run check:boundaries |
Module import boundary lint |
./scripts/verify.sh |
Full local verification gate |
npm run screenshots |
Capture README screenshots and demo GIF (requires running server) |
lib/ Domain services (clients, projects, time, billing, …)
src/app/ Next.js App Router pages and API routes
prisma/ Schema, migrations, RLS policies
specs/ Domain rules and product scope
tests/ Unit tests
scripts/ Seed, verify, boundary checks
Domain logic lives in lib/<domain>/service.ts. Cross-domain imports must go through public service files only (enforced by scripts/check-boundaries.ts).
Contributions are welcome. See CONTRIBUTING.md for setup, conventions, and the pull request checklist.
Phase 1 (v0.1.x) and Phase 2 (v0.2.0 – v0.5.0) are shipped. Scope is in specs/product/mvp-scope.md. Phase 3+ (tax, retainer auto-draft, client portal, approval email, public API, native mobile) is out of scope.
Release history: CHANGELOG.md.
GitHub Actions on every push and pull request:
| Workflow | Purpose |
|---|---|
| CI | Lint, typecheck, unit tests, boundaries, Prisma validate, production build, E2E smoke tests |
| CodeQL | Security analysis (weekly + on PR) |
| Release | Publish Docker image to GHCR on release |
| Screenshots | Regenerate README images on release |
Dependabot opens weekly PRs for npm, GitHub Actions, and Docker base images.
Local E2E: npm run test:e2e (requires running server + seeded DB on port 3005).
docs/DESIGN-PIVOT.md— realization-kernel positioning (why not Kimai / generic PSA)docs/deploy.md— Docker, Railway, Fly.io, and production checklistdocs/development.md— local setup, architecture, testing, and common tasksAGENTS.md— community agent/developer contractdocs/factory-overlay.md— factory/site overlay (moved out of root AGENTS.md)specs/domain/billing-rules.md— billing model rulesspecs/domain/profitability-rules.md— profitability calculations
See SECURITY.md for reporting vulnerabilities.
docs/README.md lists the repository layout. Community policy files are the root CONTRIBUTING.md, CODE_OF_CONDUCT.md, and SECURITY.md.




