No description
  • TypeScript 98.4%
  • CSS 0.6%
  • JavaScript 0.5%
  • Dockerfile 0.4%
Find a file
Stefan Tanta 70875ef8e1 Docs: capture current Zeitmark behavior
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 14:14:06 +02:00
.claude/skills/deploy UI polish, Stundenzettel PDF fixes, holiday weekend toggle, deploy skill 2026-04-25 13:40:18 +02:00
backend Absences: derive full-day times from workplace start 2026-06-12 13:09:34 +02:00
docker Docker: include workspace dependencies in image 2026-06-12 09:59:59 +02:00
frontend Absences: derive full-day times from workplace start 2026-06-12 13:09:34 +02:00
.dockerignore Bring up the Zeitmark MVP + Phase 2 in one drop 2026-04-25 09:58:08 +02:00
.gitignore Expose Postgres on host port 5431 + ignore *.lock 2026-04-28 18:31:43 +02:00
AGENTS.md Docs: capture current Zeitmark behavior 2026-06-12 14:14:06 +02:00
AGENTS.md.bak Docs: capture current Zeitmark behavior 2026-06-12 14:14:06 +02:00
CLAUDE.md Initial commit from Create Next App 2026-04-24 19:25:41 +02:00
docker-compose.yml Expose Postgres on host port 5431 + ignore *.lock 2026-04-28 18:31:43 +02:00
HOMELAB_INTEGRATION.md Bring up the Zeitmark MVP + Phase 2 in one drop 2026-04-25 09:58:08 +02:00
MOBILE_API_TODO.md Docs: capture current Zeitmark behavior 2026-06-12 14:14:06 +02:00
package-lock.json Harden time tracking and mobile API readiness 2026-06-12 09:45:13 +02:00
package.json Harden time tracking and mobile API readiness 2026-06-12 09:45:13 +02:00
README.md Docs: capture current Zeitmark behavior 2026-06-12 14:14:06 +02:00
zeitmark-prd.md Docs: capture current Zeitmark behavior 2026-06-12 14:14:06 +02:00
zeitmark.svg Bring up the Zeitmark MVP + Phase 2 in one drop 2026-04-25 09:58:08 +02:00

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:

  1. Two env vars only. Anything else lives in InstanceConfig and is reconfigurable at runtime.
  2. Multi-tenant scope at the service layer. Every Prisma query includes userId (or scopes the workplace by userId).
  3. *Enc fields are client-side ciphertext. Never decrypted on the server, never logged, never SSR-embedded.
  4. OAuth/LDAP/SMTP providers load from InstanceConfig at request time. A super-admin edit takes effect on the next sign-in — no restart.
  5. Super-admin is created via CLI, never via the web UI.
  6. 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.