Administration
Users, roles, modules, audit log, backups.
6 user roles
Super admin
Manager
Employee
1099 Contractor
Vendor admin
Vendor member
members_can_log_time flag.7 modules — per-tenant feature bundles
Toggle on/off per tenant in Settings → Modules:
- ✓core — required, always on (time / clients / invoicing)
- ✓onboarding — magic-link invites for hires + vendors
- ✓compliance — HR docs + expiry + vendor matrix
- ✓contracts — author + send + sign agreements
- ✓vendor_staffing — vendors + outbound POs + invoices
- ✓attorney — LEDES + UTBMS + matters (requires compliance)
- ✓bank_sync — Plaid integration (separate cost)
requireModule(auth.modules, "X") AND the role check. Disabled modules return 404 (not 403) — endpoints behave as if they don't exist for that tenant. Matches the sidebar treatment where the item is simply absent.Audit log
Every mutation across the app writes an audit row capturing:
- ✓Tenant ID + user ID + action + entity type + entity ID
- ✓Metadata (before/after diff; type discriminator)
- ✓Request ID (correlation with structured logs)
- ✓IP address (redacted-prefix-only for privacy)
- ✓Timestamp
- ✓Searchable in Diagnostics → Audit Logs
Direct-edit escape hatch (super_admin)
When a time entry needs fixing after it's been submitted / approved / rejected, super_admin can PATCH it directly without the reject → auto-draft → edit → re-approve dance. The service accepts an adminOverride flag; the API route passes it when the caller is super_admin. Every override writes an audit row with metadata.type: "admin_direct_edit" + previousStatus so reviewers can find every one (and see what state the entry was in when the admin reached in). Status itself is NOT flipped by the override — admin edits the numbers on the approved record, doesn't reset workflow. Managers continue to use the standard reject flow; the escape hatch is super_admin-only.
Backups + diagnostics
SQLite backup
data/timetrackpro.sqlite. Stop service, copy file, restart — that's the backup. Schedule via Task Scheduler / cron.Diagnostics page
/app/data as a named volume. Backup the volume itself with docker volume backup or by tarring the mount point.