- TypeScript 97.8%
- JavaScript 1%
- Shell 0.5%
- CSS 0.5%
- Dockerfile 0.1%
Toont "Vinculum v2.0.0 (build 0) | Gebouwd op 16 juli 2026 | Ontwikkeld door Je-Ma ICT Beheer" onder elke pagina, in zowel de v1- als v2-layout. Buildnummer is de patch-component (v1.1.12 = build 12). Een oplopend nummer of commit-SHA kan niet: .git staat in .dockerignore. Build-datum via nieuwe build-info.json, geschreven tijdens docker build. Niet hergebruikt uit security-report.json omdat die bij een falende npm audit terugvalt op een stub met lege generatedAt — dan zou de datum verdwijnen terwijl de build gelukt is. Stub-eerst-patroon wel overgenomen zodat een falend script de COPY in de runner-stage niet breekt. Ontwikkelaar-naam hardcoded, niet via getBranding(): die vendorName is het bedrijf dat de app draait, dus een klant-deploy zou "Ontwikkeld door Acme BV" tonen. In de v2-layout verhuist de pb-24 van main naar de footer, anders valt die op mobiel achter de fixed BottomTabV2. |
||
|---|---|---|
| deploy | ||
| docs | ||
| mobile | ||
| ops | ||
| prisma | ||
| public | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| DEPLOY.md | ||
| docker-compose.override.yml | ||
| docker-compose.yaml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| next.config.ts | ||
| package.json | ||
| postcss.config.mjs | ||
| prisma.config.ts | ||
| README.md | ||
| tailwind.config.ts | ||
| tsconfig.json | ||
Vinculum
CRM and billing for a small IT service shop, in one Docker container on your own server. Customers, quotes with digital signatures, invoices with iDEAL payment links, bookkeeping with receipt OCR, domain and DNS management, mail hosting, a service desk pipeline that drafts DNS changes from tickets, and offsite backups. No SaaS fees, your data stays in your own PostgreSQL.
Built by Je-Ma ICT Beheer for daily use in production at
the host configured via APP_URL. Current version: see CHANGELOG.md.
Contents
- What it does
- Quick start
- Stack
- Configuration
- Local development
- Deployment
- Scheduled jobs
- Backups and restore
- Security
- Documentation
What it does
The full path from lead to paid invoice, plus the operational side of running an IT shop:
Lead > Deal > Quote (DocuSeal) > Project > Hours > Invoice > Mollie > Paid
Contracts . Files . Vault . Assets . DNS . Mail
Sales and customers. Customers with contacts, tags, notes, attachments and
VAT/KvK data. Lookup against the Dutch trade register fills address details
automatically. A Kanban pipeline for deals, an AI summary per customer, asset
and license tracking with expiry reminders, an NPS survey with a public link,
and a credential vault per customer (encrypted with AES-256-GCM, every
reveal audited with IP and browser).
Quotes and contracts. Line items, VAT, currencies, versioning and a product catalog. An AI generator drafts quote lines or a complete Dutch contract from a short description. Signing runs through DocuSeal: for quotes above 5000 euro ex VAT and for all contracts, every partner flagged as signer plus the customer signs in order. When the last signature lands, the invoice is created, a Mollie payment link is attached and the mail goes out automatically.
Invoices and bookkeeping. Multiple currencies, PDF with logo and a payment QR that opens Mollie checkout, automatic reminders, recurring invoices, UBL export, bank statement import with automatic matching, and a quarterly VAT report. Purchases come in by photo or PDF and Claude reads supplier, date, amounts and VAT off the receipt. Above a threshold an approval flow kicks in: every approver gets a magic link by mail and the purchase is only booked when everyone has signed off. A monthly ZIP with all invoices, receipts and CSV exports goes to the accountant by mail. Incoming supplier invoices can also be ingested straight from an IMAP mailbox.
Domains, DNS and mail. Domains are linked to customers and their records are managed live against ClouDNS or your own WHM server. Openprovider handles registration and transfers, including availability checks and per customer contact handles. Mailcow integration activates mail for a domain in one click: mail domain, DKIM key and the matching DNS records (MX, SPF, DKIM) are created together. Every record change triggers a notification mail and lands in the audit log.
Service desk automation. A cron polls Jitbit tickets and lets Claude classify them: DNS change, new mailbox, domain registration or link request. Each proposal lands in an approval queue with a confidence score, the parsed change and a prepared reply. An operator approves, and the change is applied and confirmed to the customer on the ticket. When information is missing, the system mails the customer a clarification question written by Claude and picks the thread back up when they reply. Unknown senders can be offered a secure onboarding form: the customer fills in company details that are encrypted in the browser before they ever reach the server.
WordPress and hosting operations. Sites per customer with a cPanel connection, traffic reports generated from AWStats data with AI written commentary, published to a reports subdomain and mailed to the customer. A maintenance log, file sharing per site, and a CMDB for hardware with QR labels and depreciation tracking.
Telephony. Rinkel integration: click to call from any customer page, call history synced into the activity feed, and a popup for incoming calls.
Productivity. Cmd+K palette, keyboard shortcuts, dark mode, installable as a PWA with a receipt scan shortcut, and an audit log of every write.
All AI features are optional and run on the Anthropic API. Every external integration switches itself off when its environment variables are empty; the core CRM keeps working.
Quick start
git clone https://code.vanmullemholding.eu/jesse-a/Vinculum.git crm
cd crm
cp .env.example .env # fill in at least AUTH_SECRET and DATABASE_URL
docker compose up --build # builds and starts app + postgres
Open http://localhost:3000. On first boot the container creates an admin
account from SEED_ADMIN_EMAIL and SEED_ADMIN_PASSWORD. Leave the password
empty and a strong random one is generated and printed once in the container
log, so there is never a publicly known default.
Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16, App Router, server actions, Turbopack |
| Language | TypeScript |
| Database | PostgreSQL with Prisma 6 |
| Cache / locks | Redis, optional (rate limits, webhook replay cache, cron locks) |
| Styling | Tailwind CSS |
| pdf-lib | |
| nodemailer over SMTP | |
| Auth | JWT sessions via jose, bcryptjs, TOTP per RFC 6238 |
| AI | Anthropic SDK (Claude) |
| Payments | Mollie |
| Signing | DocuSeal, HMAC verified webhooks |
| DNS | ClouDNS API and WHM API, behind one provider abstraction |
| Domains | Openprovider REST API |
| Mail hosting | Mailcow API |
| Helpdesk | Jitbit API |
| Telephony | Rinkel API |
| Storage | local volume or any S3 compatible bucket |
The container entrypoint runs prisma db push on boot, creates the admin
account if the database is empty, and starts the standalone Next.js build on
port 3000. There are no manual migration steps.
Configuration
Everything is configured through environment variables. The minimum to boot:
DATABASE_URL="postgresql://user:pass@db:5432/crm"
AUTH_SECRET="a random string of at least 32 characters"
APP_URL="https://crm.example.com"
SEED_ADMIN_EMAIL="you@example.com"
SEED_ADMIN_PASSWORD="" # empty = generated and shown once in the log
COMPANY_NAME="Your Company"
.env.example documents every variable, grouped per integration: SMTP, Mollie, Anthropic, DocuSeal, ClouDNS, WHM, Openprovider, Mailcow, Jitbit, Rinkel, IMAP intake, KvK lookup, S3 uploads and S3 backups. A feature without its variables simply stays hidden in the UI.
Note on AUTH_SECRET: it is not just the session signing key. Encryption keys
for the credential vault, TOTP secrets and API tokens are derived from it with
HKDF. Store it in your password manager as well; a database backup is useless
without it.
Local development
nvm use 24
npm install
docker compose up -d db # postgres only
npm run dev # next dev server on :3000
Schema changes go through npx prisma db push during development, and
npx prisma studio gives you a data browser. Seed data lives in
prisma/seed.ts and runs with npx prisma db seed.
Deployment
The reference setup is Coolify v4 with Traefik in front. Short version:
- Create a PostgreSQL database in Coolify (or use a managed one).
- New application, build pack Dockerfile, port 3000, attach your domain.
- Set the environment variables.
- Add a persistent volume on
/app/data(or setSTORAGE_BACKEND=s3). - Deploy. Pushes to the linked branch deploy automatically.
DEPLOY.md is the full walkthrough, including the health check,
TLS hardening (modern ciphers only, HSTS, post quantum key exchange via
X25519MLKEM768), offsite backups and the dependency update workflow.
Scheduled jobs
All cron endpoints accept only POST with an Authorization: Bearer $CRON_SECRET
header and are safe to retrigger: every job takes a distributed lock and the
work itself is idempotent. Set them up as scheduled tasks in Coolify:
| Endpoint | Suggested schedule | What it does |
|---|---|---|
/api/cron/payment-reminders |
daily 09:00 | first and second invoice reminder |
/api/cron/recurring-invoices |
daily 07:00 | create and send subscription invoices |
/api/cron/backup |
daily 03:15 | dump database and uploads to S3 |
/api/cron/security-check |
daily 06:00 | npm audit, mails admins on HIGH or CRITICAL |
/api/cron/jitbit-dns-prep |
every 5 to 10 min | classify new tickets into change proposals |
/api/cron/inkoop-mail-import |
every 15 min | import supplier invoices from the IMAP mailbox |
/api/cron/openprovider-expiry |
daily | sync domain expiry dates and nameservers |
/api/cron/rinkel-cdr-import |
every 15 min | pull call history into activity feeds |
/api/cron/wordpress-healthcheck |
hourly | check linked WordPress sites |
Webhooks live under /api/webhooks/docuseal (HMAC required),
/api/webhooks/mollie (status is refetched from Mollie, the body is never
trusted) and /api/webhooks/rinkel/<token>.
Backups and restore
One button under settings, or the cron endpoint, streams a pg_dump and a tar
of the uploads directly to any S3 compatible bucket (DigitalOcean Spaces,
Backblaze B2, Cloudflare R2, AWS). No temp files on disk. Status and the last
result are visible on the settings page.
Restore: stop the app, restore the SQL dump with psql, unpack the uploads
tar into the volume, start the app. The exact commands are in DEPLOY.md, step
9. Test a restore once before you need it.
If you run on a managed database (the production setup uses DigitalOcean Managed PostgreSQL with automatic backups and point in time recovery), the builtin database backup becomes a second line rather than the primary one.
Security
Security has had more attention than any other part of this codebase: four external review rounds plus an Internet.nl pass, 45 findings fixed and the trail documented. A white box source review of v1.0.0 plus an external probe of the live deployment (June 2026) rated the posture Strong, with zero critical and zero high findings. The full report is in SECURITY-ASSESSMENT-2026-06.md.
The short list of what is in place:
- 2FA with TOTP is mandatory for every user, with recovery codes and replay protection. Sessions are validated against the database on every request and can be revoked instantly, per session or everywhere at once.
- Secrets at rest (vault entries, TOTP secrets, API tokens) use
AES-256-GCMwith per purpose keys derived fromAUTH_SECRETthrough HKDF. Reveals are audited; some secrets are write only and can never be read back. - A strict Content Security Policy with a fresh nonce per request and no
unsafe-inline, plus HSTS, frame denial and the rest of the modern header set. - Outbound requests to user influenced hosts go through an SSRF defense layer: string checks, DNS resolution with private IP rejection, and a fetch wrapper that refuses redirects.
- Webhooks verify HMAC signatures with constant time comparison and a freshness window. Cron endpoints require a bearer secret. Public links use random tokens of 160 bits or more, never sequential IDs.
- Password changes, TOTP changes and email changes require the current password again, with a rate limit.
Reports from the community are welcome at security.txt on the production
host.
Documentation
| Document | What |
|---|---|
| DEPLOY.md | step by step deployment on Coolify, TLS, backups, updates |
| docs/user-guide.md | guide for daily users, no technical background needed |
| SECURITY-ASSESSMENT-2026-06.md | the June 2026 security assessment report |
| docs/openprovider-research.md | API notes behind the Openprovider integration |
| docs/rinkel-research.md | API notes behind the Rinkel integration |
| CHANGELOG.md | every change, per commit, in Dutch |
License
No open source license. It runs a real company, so the code is published as reference. Want to use it commercially? Ask first.
Made with a great many git push origin main.