- TypeScript 98.4%
- CSS 0.6%
- JavaScript 0.5%
- Dockerfile 0.4%
|
|
||
|---|---|---|
| .claude/skills/deploy | ||
| backend | ||
| docker | ||
| frontend | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| AGENTS.md.bak | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| HOMELAB_INTEGRATION.md | ||
| MOBILE_API_TODO.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| zeitmark-prd.md | ||
| zeitmark.svg | ||
Zeitmark
Self-hosted, multi-tenant work-time tracker. Personal time entries, flexi balance, PTO, public holidays, monthly reports + Stundenzettel exports, client-side encrypted pay estimation. Built to deploy on a single VM with just two environment variables and configure the rest from the super-admin UI.
Stack
- Next.js 15 (App Router) + TypeScript
- Prisma 6 + PostgreSQL 16
- Auth.js v5 (credentials, Google, GitHub, generic OIDC, LDAP)
- Web Crypto API (AES-GCM 256 + PBKDF2-SHA256) for encrypted pay fields
- Tailwind v4 + shadcn/ui (new-york / neutral)
- pdf-lib (Stundenzettel)
- nodemailer (password reset, future notifications)
Repository layout
backend/ @zeitmark/db — Prisma schema, services, super-admin CLI
frontend/ @zeitmark/web — Next.js app, layouts, server actions
docker/ Dockerfile + entrypoint
docker-compose.yml
The two packages share a root package.json via npm workspaces.
Quick start (local dev)
docker compose up -d db
npm install
DATABASE_URL=postgresql://zeitmark:secret@localhost:5432/zeitmark \
npm run db:deploy --workspace=@zeitmark/db
DATABASE_URL=postgresql://zeitmark:secret@localhost:5432/zeitmark \
NEXTAUTH_SECRET=$(openssl rand -base64 32) \
npm run dev
Then create the first super-admin from the CLI:
DATABASE_URL=postgresql://zeitmark:secret@localhost:5432/zeitmark \
npm run create-super-admin -- --email you@example.com --password 'changeme'
Visit http://localhost:3000 and sign in.
Configuration
Only two environment variables ever:
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string. |
NEXTAUTH_SECRET |
Auth.js JWT signing secret. openssl rand -base64 32. |
Everything else — OAuth/OIDC client credentials, LDAP bind, SMTP, branding,
feature flags, default locale — lives in the InstanceConfig table and
is configured at /superadmin/instance after first login. Sensitive
sub-fields are encrypted at rest with AES-256-GCM (key derived from
NEXTAUTH_SECRET via HKDF).
Production deploy
docker compose up -d --build
Runs prisma migrate deploy from docker/entrypoint.sh, then serves on
port 3000. Drop a reverse proxy in front for TLS. Zeitmark uses Prisma
migrations, not Alembic.
Features
- Live clock-in/out with running timer and offline queue (IndexedDB + background sync)
- Manual entries (WORK / BREAK / ON_CALL / PTO / SICK / PUBLIC_HOLIDAY / FLEXI_TAKEN), per-day totals, overlap warning
- Full-day absences: PTO, sick days, and public holidays use date ranges only; stored times are derived from the workplace theoretical start time plus the configured workday duration
- Flexi balance: CARRY / PAYOUT / BOTH rollover modes, configurable cap, on-call multiplier, recorded payouts, 6-month history chart
- Public holidays: Nager.Date auto-populate per country, custom holiday add, per-user hide, dashboard auto-suggest "Book today as holiday"
- PTO allowance per (user, workplace, year) with used/remaining
- Calendar view: month grid with holidays + PTO/SICK/FLEXI bars
- Reports: per-day breakdown, totals, cross-workplace summary, CSV + PDF (Stundenzettel) export
- Pay encryption: passphrase-derived key (PBKDF2 310k), all pay/tax fields AES-GCM encrypted client-side. Server never sees plaintext. Unlock modes (SESSION / TIMEOUT / PER_VISIT), passphrase rotation, destructive reset.
- Pay estimation: client-side gross calculation per period (HOURLY / MONTHLY / ANNUAL), surfaced on dashboard + reports.
- Dashboard Time account live-updates while a WORK entry is running, without persisting that running time until clock-out.
- Auth providers (configurable from the super-admin UI):
- Email/password (bcrypt)
- Google OAuth, GitHub OAuth
- Generic OIDC (Authelia, Keycloak, Dex, Authentik, Ory…)
- LDAP (ldapts, service-bind + user-bind)
- Password reset over SMTP (nodemailer)
- Super-admin: instance config, user management (role / deactivate), audit log of every privileged action
- i18n: English + Deutsch, cookie-based locale switch, every UI string routes through the dictionary
Architectural rules
These are non-negotiable for contributors:
- Two env vars only. Anything else lives in
InstanceConfigand is reconfigurable at runtime. - Multi-tenant scope at the service layer. Every Prisma query
includes
userId(or scopes the workplace byuserId). *Encfields are client-side ciphertext. Never decrypted on the server, never logged, never SSR-embedded.- OAuth/LDAP/SMTP providers load from
InstanceConfigat request time. A super-admin edit takes effect on the next sign-in — no restart. - Super-admin is created via CLI, never via the web UI.
- Core time logic belongs in
backend/src. The frontend may display live timers and client-encrypted pay estimates, but should not reimplement time-account, absence, holiday, flexi, or report rules.
Mobile API Status
Most workflows currently use Next.js server actions, with a few API routes
for auth/offline/report helpers. A stable native mobile API should be added
under /api/v1; see MOBILE_API_TODO.md.
See AGENTS.md and zeitmark-prd.md for the full spec.
License
TBD — currently personal use.