An open-source, self-hosted invoice & payslip generator.
WolfSlip produces invoices and payslips on infrastructure you control. There is no hosted tier and no third party in the middle - you run the app, you own the Postgres database, and your billing and payroll records never leave your environment.
WolfSlip is not tied to any one country. Currency, locale, tax-identifier label, payment details and the fiscal-year boundary are all configured from Settings. The design canvas uses an Indian business as its worked example, and those values are samples, nothing more.
- License: MIT - see
LICENSE - Status: early development, built in phases. Invoices work end to end except for PDF rendering and email delivery, which are next.
| Area | State |
|---|---|
| Monorepo, CI, local Supabase stack | Working |
| Authentication (email + password) | Working |
| Design system and application shell | Working |
| Settings (company, numbering, recipients, SMTP status) | Working |
| Clients, employees, PDF templates | Working |
| Invoices - draft, issue, void, numbering | Working |
| PDF rendering | Not started |
| Email delivery | Not started |
| Payslips and pay runs | Not started |
Signup is deliberately closed. Every authenticated user can read and write the whole ledger on a single-org deployment, so accounts are created by an operator
- see Creating accounts.
| Document | What it covers |
|---|---|
docs/ARCHITECTURE.md |
How data is fetched, written and secured; the layer map |
docs/DESIGN_SYSTEM.md |
Tokens, globals.css, and how to add UI without breaking the theme |
docs/DATABASE.md |
Schema layout, RLS conventions, document numbering, migrations |
docs/DECISIONS.md |
Decisions taken so far and why, including the rejected alternatives |
docs/CONTRIBUTING.md |
Commands, scripts, conventions, the verification gate |
AGENTS.md |
Rules an automated contributor must follow |
TROUBLESHOOTING.md |
Failures that have actually happened here, and their causes |
| Concern | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Cache Components) |
| Language | TypeScript |
| Database & Auth | Supabase (Postgres + RLS + GoTrue) |
| Auth SSR | @supabase/ssr |
| Server Actions | next-safe-action + Zod |
| Client data fetching | TanStack Query |
| UI | shadcn/ui on Radix UI |
| Styling | Tailwind CSS v4, tokens in globals.css |
| Forms | React Hook Form + Zod resolvers |
| PDF (planned) | @pdfme/generator, Node runtime |
| Email (planned) | nodemailer over SMTP |
| Lint / Format | oxlint + oxfmt |
| Unit tests | Vitest |
| Database tests | pgTAP |
| E2E tests | Playwright |
| Monorepo | Turborepo + pnpm workspaces |
Requirements: Node 24+, pnpm 11+, Docker (for the local Supabase stack).
./setup.sh
pnpm devsetup.sh installs dependencies, creates the .env files from their examples,
starts Supabase, syncs the local keys into your env, creates the storage
buckets, and generates database types.
The app runs at http://localhost:3000, Supabase Studio at http://localhost:54323, and the local mail catcher at http://localhost:54324.
Sign in with the account seeded into the local database:
staff@wolfslip.test / Wolf-Test-1!
That account is created by seed.sql, which runs only against the local stack -
supabase db push applies migrations, never seeds - so it cannot reach a
deployed database.
wolfslip/
├── apps/
│ ├── web/ # Next.js application
│ │ └── src/
│ │ ├── app/
│ │ │ ├── (marketing)/ # Public pages
│ │ │ ├── (auth)/ # Sign in, reset, callback routes
│ │ │ └── (app)/ # Authenticated app
│ │ ├── components/ui/ # shadcn/ui - edited source, not a dependency
│ │ ├── data/ # Server Actions (every write)
│ │ ├── lib/
│ │ │ ├── db/ # Select strings, row -> view-model mappers
│ │ │ ├── queries/ # Isomorphic reads; loaders.ts is server-only
│ │ │ ├── query/ # TanStack client and provider
│ │ │ ├── schemas/ # Zod schemas, shared by forms and actions
│ │ │ ├── format/ # Money, amount-in-words, periods, numbering
│ │ │ ├── supabase/ # client / server / route / admin / proxy
│ │ │ └── types.ts # View models - the only types components import
│ │ └── styles/globals.css # The design system
│ └── database/
│ └── supabase/
│ ├── schemas/ # Declarative schema (source of truth)
│ ├── migrations/ # Generated - never hand-edited
│ ├── tests/ # pgTAP
│ └── seed.sql # Local fixtures and the dev account
├── docs/ # Architecture, design system, decisions
├── packages/typescript-config/ # Shared tsconfig base
└── scripts/ # Maintenance CLIs, run through tsx
Run from the repo root.
| Command | Does |
|---|---|
pnpm dev |
Run every app in parallel |
pnpm web#dev |
Run only the Next.js app |
pnpm build |
Production build |
pnpm lint |
oxlint over apps/web/src and scripts/ |
pnpm typecheck |
tsc --noEmit for the app and for scripts/ |
pnpm test |
Vitest unit tests |
pnpm test-db |
pgTAP tests against the local stack |
pnpm test:e2e |
Playwright end-to-end tests |
pnpm database#start / #stop / #status |
Local Supabase stack |
pnpm gen-types-local |
Regenerate database.types.ts from the local DB |
pnpm gen-types |
Regenerate types from the linked remote project |
pnpm supabase:sync-env |
Copy local Supabase keys into your env files |
pnpm user:create <email> [password] |
Create a staff account |
pnpm storage:bootstrap |
Create the storage buckets (they are DML, so migrations cannot) |
pnpm shadcn:add <component> |
Add a shadcn component without reverting this project's tokens |
Never run shadcn add directly - see
docs/DESIGN_SYSTEM.md.
The declarative schema under apps/database/supabase/schemas/ is the source of
truth, split by concern and applied in the order declared under
[db.migrations] schema_paths in config.toml. Never hand-write a migration.
# 1. Edit the relevant file in apps/database/supabase/schemas/
# 2. Generate the migration
cd apps/database && pnpm supabase db diff -f add_payslips
# 3. Apply locally, regenerate types, re-run the database tests
pnpm supabase db reset
cd ../.. && pnpm gen-types-local && pnpm test-dbEvery table needs Row Level Security policies and explicit GRANTs - RLS
alone makes a table unreachable through PostgREST, and a grant without RLS is a
leak. New tables also inherit a GRANT ALL … TO anon default privilege from the
Supabase platform baseline, which the migration must revoke. pnpm test-db
asserts all of this generically, so a table that forgets fails there rather than
in production. Full detail in docs/DATABASE.md.
Public signup is disabled (enable_signup = false in
apps/database/supabase/config.toml) because every authenticated user can read
the whole ledger. Create accounts deliberately:
pnpm user:create finance@example.com # generates and prints a password
pnpm user:create finance@example.com 'secret' # or set oneThis calls the Auth Admin API with the service-role key, which bypasses the
signup gate. It needs SUPABASE_SERVICE_ROLE_KEY in the environment.
WolfSlip is a Next.js app plus a Postgres database, so it runs anywhere Node 24+ runs. You need:
- A Postgres database with the Supabase extensions - from supabase.com or a self-hosted Supabase.
- The migrations in
apps/database/supabase/migrationsapplied to it. - The storage buckets created (
pnpm storage:bootstrap) - bucket creation is DML, so migrations cannot do it. - These environment variables on the web app:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
NEXT_PUBLIC_SITE_URL=
SUPABASE_SERVICE_ROLE_KEY= # server-only; never prefix this NEXT_PUBLIC_
Email delivery, once built, adds SMTP_HOST, SMTP_PORT, SMTP_USER,
SMTP_PASSWORD, SMTP_FROM and SMTP_REQUIRE_TLS. The SMTP password lives in
the environment and never in Postgres, so a database dump carries no mail
credentials.
Then pnpm build && pnpm --filter web start.
Before opening a PR:
pnpm lint && pnpm typecheck && pnpm test && pnpm test-db && pnpm buildSee docs/CONTRIBUTING.md for conventions, and
docs/DECISIONS.md before proposing a change to an
architectural choice - the reasoning, including what was rejected, is recorded
there.
MIT. See LICENSE.