Pulse Clock
Multi-tenant time clock and workforce management system for hotel/hospitality environments. Employees punch in/out and take breaks via a kiosk interface; administrators monitor time cards, detect scheduling issues, and export payroll data.
Overview
pulse-clock provides:
- Employee Kiosk: Web-based punch interface with webcam capture
- Admin Dashboard: Time grid view with issue detection
- Management: Hotels and employees CRUD
- Payroll Export: TSV download with validation checks
- Validation Engine: State machine enforcing valid punch sequences
Prerequisites
- Node.js (v20.9 or higher)
- npm (package manager)
- SQLite (accessed through Drizzle and the libSQL client)
Installation
cd pulse-clock
npm install
Copy the example environment file and configure:
cp .env.example .env
Edit .env with your configuration. Key variables:
| Variable | Description | Default |
|---|---|---|
PULSE_SECRET |
Login secret; use 32+ random chars | — |
PULSE_RATE_LIMIT |
API rate limit (requests per window) | 60 |
PULSE_RATE_WINDOW |
Rate limit window (seconds) | 60 |
DATABASE_URL |
SQLite database path | ./data/pulse-clock.db |
Usage
Development Server
npm run dev
Open http://localhost:3000 to access the application.
Build for Production
npm run build
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Pulse Clock │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Kiosk Mode │ │ Admin Dashboard │ │
│ │ /kiosk/* │ │ /admin/* │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ API Routes │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ /punch │ │ /time- │ │ /payroll │ │ │
│ │ │ │ │ entries │ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ time-entry-service.ts │ │
│ │ (createValidatedTimeEntry) │ │
│ │ Punch State Machine │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQLite Database │ │
│ │ companies | employees | time_entries | audit_log │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Data Flow
┌─────────────────────────────────────────────────────────────────┐
│ Kiosk Punch Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Employee Selects → Webcam Capture → Punch Button Click │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ POST /api/punch │ │
│ │ { employeeId, type }│ │
│ └────────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ Punch State Machine │ │
│ │ validateNewEntry... │ │
│ └────────────┬───────────┘ │
│ │ │
│ ┌─────────────────────┼──────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌──────────┐
│ │ Valid Punch │ │ Invalid Punch │ │ Duplicate│
│ │ → Save Entry │ │ → Reject │ │ → Ignore │
│ └───────────────┘ └───────────────┘ └──────────┘
│ │
└─────────────────────────────────────────────────────────────────┘
Punch State Machine
Valid Transitions:
OUT ──────▶ IN (clock in)
IN ──────▶ OUT (clock out)
IN ──────▶ BREAK_OUT (start break)
BREAK_OUT ─▶ BREAK_IN (end break)
All other transitions are rejected unless forceOverride=true
Key Features
Employee Kiosk
- Live employee cards showing current status: Off Duty / On Shift / On Break
- Webcam capture on each punch (base64 stored in DB)
- Punch modal with Clock In, Clock Out, Start Break, End Break buttons
- 5-second duplicate-punch guard on kiosk clicks
- Auto-refreshes employee status every 5 seconds
- Elapsed shift time shown on employee cards (net of break time)
Admin Dashboard
- Date range selector with Today/Yesterday/Last 7/Last 14/Last 30 presets
- Grid view: employees (rows) × calendar days (columns)
- Red cell highlighting for validation issues
- "Issues Only" filter toggle
- Detail drawer per employee/day: full punch timeline
- Timestamp editing through authenticated admin sessions
- Add manual punch entries
Validation Engine
Detects per employee per day:
- First entry is not Clock In
- Consecutive duplicate entries (e.g., two INs in a row)
- Break started without prior Clock In
- Break ended without break start
- Break started but never ended
- Still clocked in at day end
- Missing clock out
Payroll Export
- Date range presets including Pay Period 1-15 and Pay Period 16-31
- Preview table: Employee | Date | In | Out | Break (min) | Net Hours | Notes
- Copy to clipboard (TSV) and Download .tsv buttons
- Issues column flags validation problems per day row
- Summary strip with total hours and error counts
Public Hosting Security
- All pages redirect to
/loginunless thepulse_authcookie is valid. - All API routes except
/api/authorizeand/api/healthrequire the auth cookie. PULSE_SECRETis required and must be at least 32 characters.- The auth cookie is
HttpOnly,SameSite=Lax,Securein production, and valid for 1 year. - Login attempts and authenticated API calls are rate limited by IP.
- Mutating API requests require same-origin/CSRF protection.
Multi-Tenancy
All data is scoped by companyId:
- Employees belong to a company
- Time entries scoped to company + employee
- Audit logs scoped to company
Pages Reference
| Path | Description |
|---|---|
/ |
Landing page with company grid |
/login |
Secret-based login page |
/kiosk |
Kiosk company selector |
/kiosk/[companyId] |
Employee kiosk interface |
/admin |
Admin time grid dashboard |
/admin/manage |
Hotels and employees CRUD |
/admin/payroll |
Payroll export with TSV download |
API Reference
| Endpoint | Method | Description |
|---|---|---|
/api/companies |
GET, POST | Company CRUD |
/api/employees |
GET, POST | Employee CRUD |
/api/employees/with-status |
GET | Employee list with live status |
/api/punch |
POST | Kiosk punch (state machine) |
/api/time-entries |
GET, POST, PATCH, DELETE | Time entry CRUD + manual edits |
/api/payroll |
GET | Payroll TSV generation |
/api/authorize |
GET, POST | Cookie auth check/login |
/api/health |
GET | Unauthenticated healthcheck |
Authentication
Open /login and enter PULSE_SECRET. Successful login sets the pulse_auth cookie.
API calls from the browser use that cookie automatically. For non-browser scripts, first authenticate and store the cookie:
curl -c cookies.txt \
-H "Content-Type: application/json" \
-H "x-pulse-csrf: 1" \
-X POST https://localhost:3000/api/authorize \
-d '{"secret":"your-32-character-minimum-secret"}'
curl -b cookies.txt \
-H "Content-Type: application/json" \
-H "x-pulse-csrf: 1" \
-X PATCH https://localhost:3000/api/time-entries \
-d '{"id":123,"timestamp":1747507200}'
Database Schema
companies (id, name, created_at)
employees (id, company_id, name, is_active, created_at)
time_entries (id, employee_id, company_id, type, timestamp, photo_base64, is_deleted)
audit_log (id, employee_id, company_id, admin_id, action, detail, created_at)
Project Structure
pulse-clock/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # Landing page
│ │ ├── kiosk/
│ │ │ └── [companyId]/ # Kiosk UI
│ │ ├── admin/
│ │ │ ├── page.tsx # Time grid dashboard
│ │ │ ├── manage/page.tsx # Hotels & employees CRUD
│ │ │ └── payroll/page.tsx # Payroll export
│ │ └── api/ # API routes
│ ├── lib/ # Shared business logic
│ │ ├── api.ts # fetchApi wrapper
│ │ ├── validation.ts # State machine
│ │ ├── calculations.ts # Time calculations
│ │ ├── time.ts # Date utilities
│ │ └── time-entry-service.ts
│ ├── db/
│ │ ├── schema.ts # Drizzle schema
│ │ └── index.ts # Drizzle client and migrations
│ └── components/ # React components
├── drizzle/ # SQL migrations
└── package.json
Docker / Coolify Deployment
The app includes a production Dockerfile for Coolify.
Recommended Coolify settings:
| Setting | Value |
|---|---|
| Build Pack | Dockerfile |
| Dockerfile Location | Dockerfile |
| Port | 3000 |
| Persistent Storage | /app/data |
Set these environment variables in Coolify:
PULSE_SECRET=replace-with-at-least-32-random-characters
DATABASE_URL=/app/data/pulse-clock.db
PULSE_RATE_LIMIT=60
PULSE_RATE_WINDOW=60
Generate a deployment secret with:
openssl rand -base64 32
For local Docker testing:
docker build -t pulse-clock .
docker run --rm -p 3000:3000 -v pulse-clock-data:/app/data --env-file .env pulse-clock
SQLite migrations run automatically on startup, and the database is stored in /app/data when deployed with the Docker defaults.
Run one app replica per SQLite volume so startup migrations are not executed concurrently.