A self-hosted PWA for managing daily time allocations with checkout functionality. Perfect for managing screen time, homework time, or any time-based activities.
- Multiple Timers: Create timers for different people and activities
- Daily Allocations: Set daily time limits that reset at midnight
- Weekly Schedules: Configure different time allocations for each day of the week
- Expiration Times: Set daily expiration times after which timers become unavailable
- Checkout System: Check out a portion of time to use, returns unused time to the pool
- Admin PIN Protection: Simple PIN-based access control for management functions
- PWA Support: Install on iOS/Android home screens
- Offline Capable: Works offline with service worker caching
- Self-Hosted: Run on your own Debian/Proxmox LXC container
Deploy to a Proxmox LXC container in 2 steps:
# 1. Create Debian LXC container (on Proxmox host)
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/debian.sh)"
# 2. Install Timer App (inside container - use 'pct enter CONTAINER_ID')
bash -c "$(curl -fsSL https://raw.githubusercontent.com/debugthings/timer-app/master/install.sh)"Access at http://YOUR_CONTAINER_IP:3001
Full deployment instructions →
- Frontend: React 18, TypeScript, TailwindCSS, Vite, React Query
- Backend: Node.js, Express, Prisma ORM
- Database: SQLite (file-based, no setup required)
- Testing: Vitest (backend API tests), Playwright (E2E tests)
- Deployment: Single Node.js service serves both API and frontend
- Node.js 20 LTS or higher
- npm or yarn
# Install all dependencies
npm run install:all
# Or manually:
cd backend && npm install
cd ../frontend && npm installcd backend
cp .env.example .env
# Edit .env if needed
npx prisma generate
npx prisma migrate devTerminal 1 - Backend:
cd backend
npm run devTerminal 2 - Frontend:
cd frontend
npm run devThe frontend will be available at http://localhost:5173 and will proxy API requests to the backend at http://localhost:3001.
# Run all backend tests
npm run test:backend
# Or from backend directory
cd backend
npm test
# Watch mode
cd backend
npm run test:watchThe backend tests cover:
- Admin API (settings, PIN management)
- People CRUD operations
- Timer CRUD operations and expiration
- Checkout lifecycle (create, start, pause, stop, cancel)
- Transactional integrity and concurrent operation handling
# Run E2E tests (requires both servers running)
npm run test:e2e
# Run with visible browser
npm run test:e2e:headed
# Run with Playwright UI
npm run test:e2e:uiThe E2E tests cover:
- First-time setup flow
- Admin panel authentication
- Timer management
- Checkout flow (start, pause, resume, stop)
- Timer expiration behavior
npm run test:allcd frontend
npm run build# Windows (PowerShell)
Copy-Item -Path frontend/dist/* -Destination backend/public -Recurse -Force
# Linux/Mac
cp -r frontend/dist/* backend/public/cd backend
npm run buildcd backend
npx prisma migrate deploycd backend
npm startThe app will be available at http://localhost:3001.
From your Proxmox host:
# Create a Debian 12 LXC container using community scripts
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/debian.sh)"
# Note the container ID (e.g., 100)Enter the container and run the automated installation:
# Enter the container (replace 100 with your container ID)
pct enter 100
# Download and run the installation script
bash -c "$(curl -fsSL https://raw.githubusercontent.com/debugthings/timer-app/master/install.sh)"The script will:
- Install Node.js 20 LTS and build tools
- Clone the repository from GitHub
- Build the frontend and backend
- Set up the database
- Create a system user and directories
- Install update scripts (
deploy.shandupdate.sh) - Install and start the systemd service
The app will be available at http://YOUR_CONTAINER_IP:3001
If you prefer manual installation or need to customize the setup:
# On Proxmox host
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/debian.sh)"
# Enter the container (replace 100 with your container ID)
pct enter 100# Update system
apt update && apt upgrade -y
# Install build tools and git
apt install -y build-essential git curl sqlite3
# Install Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs# Clone repository
git clone https://github.com/debugthings/timer-app.git /tmp/timer-app
cd /tmp/timer-app
# Install and build frontend
cd frontend
npm install
npm run build
# Install and build backend
cd ../backend
npm install
npm run build
# Copy built frontend to backend public folder
mkdir -p public
cp -r ../frontend/dist/* public/# Create system user
useradd -r -s /bin/false timer-app
# Create application directory
mkdir -p /opt/timer-app
mkdir -p /opt/timer-app/data
# Copy built backend
cp -r /tmp/timer-app/backend/dist /opt/timer-app/
cp -r /tmp/timer-app/backend/public /opt/timer-app/
cp -r /tmp/timer-app/backend/node_modules /opt/timer-app/
cp -r /tmp/timer-app/backend/prisma /opt/timer-app/
cp /tmp/timer-app/backend/package*.json /opt/timer-app/
# Create production .env file
cat > /opt/timer-app/.env << 'ENVEOF'
DATABASE_URL="file:/opt/timer-app/data/timer.db"
PORT=3001
NODE_ENV=production
ENVEOF
# Setup database (before setting permissions)
cd /opt/timer-app
export DATABASE_URL="file:/opt/timer-app/data/timer.db"
npx prisma generate
npx prisma migrate deploy
# Set permissions AFTER database is created
chown -R timer-app:timer-app /opt/timer-app# Copy the deployment and update scripts
cp /tmp/timer-app/deploy.sh /opt/timer-app/
cp /tmp/timer-app/update.sh /opt/timer-app/
chmod +x /opt/timer-app/deploy.sh
chmod +x /opt/timer-app/update.sh
chown root:root /opt/timer-app/deploy.sh
chown root:root /opt/timer-app/update.sh# Create service file
cat > /etc/systemd/system/timer-app.service << 'EOF'
[Unit]
Description=Timer App
After=network.target
[Service]
Type=simple
User=timer-app
WorkingDirectory=/opt/timer-app
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3001
Environment=DATABASE_URL=file:/opt/timer-app/data/timer.db
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd
systemctl daemon-reload
# Enable and start service
systemctl enable timer-app
systemctl start timer-app
# Check status
systemctl status timer-app# Check if service is running
systemctl status timer-app
# Check logs
journalctl -u timer-app -f
# Test API
curl http://localhost:3001/api/admin/settingsIf you want to access the app through a domain name or NGINX proxy:
Add this location block to your NGINX configuration:
location / {
proxy_pass http://10.x.x.x:3001; # Your LXC IP
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}Reload NGINX:
sudo nginx -t
sudo systemctl reload nginxSimple update command:
# Run the update script
sudo /opt/timer-app/update.shThe update script will:
- ✅ Stop the service
- ✅ Backup the database (timestamped)
- ✅ Clone latest code from GitHub
- ✅ Build frontend and backend
- ✅ Deploy new files
- ✅ Run database migrations
- ✅ Restart the service
Manual update (alternative method):
# Stop service and backup
sudo systemctl stop timer-app
sudo cp /opt/timer-app/data/timer.db /opt/timer-app/data/timer.db.backup.$(date +%Y%m%d)
# Run deployment
sudo touch /opt/timer-app/.auto-update-enabled # Enable deployment
sudo /opt/timer-app/deploy.sh
sudo rm /opt/timer-app/.auto-update-enabled # Disable again
# Restart service
sudo systemctl start timer-appView update logs:
# Watch service logs during/after update
sudo journalctl -u timer-app -fThe update script automatically backs up your database. For manual backups:
# Manual backup
sudo cp /opt/timer-app/data/timer.db /opt/timer-app/data/timer.db.backup.$(date +%Y%m%d)
# Restore from backup
sudo systemctl stop timer-app
sudo cp /opt/timer-app/data/timer.db.backup.YYYYMMDD /opt/timer-app/data/timer.db
sudo systemctl start timer-app- Visit the app URL
- You'll be prompted to set an admin PIN
- Enter a PIN (minimum 4 characters)
- Click "Admin Panel" on the dashboard
- Add people (e.g., "John", "Sarah")
- Add timers for each person (e.g., "Screen Time", "Homework")
- Set default daily time allocations (e.g., 2 hours)
- Optionally configure weekly schedules with different times per day
- Optionally set expiration times for each day
Timers can have custom schedules for each day of the week:
- Set different time allocations per day (e.g., 2 hours on weekdays, 4 hours on weekends)
- Set expiration times per day (e.g., timer expires at 8:00 PM)
- Days without custom schedules use the default daily allocation
When a timer has an expiration time set:
- The timer becomes unavailable after the expiration time
- Active checkouts are automatically force-stopped and cancelled
- The timer becomes available again the next day
- Users see a clear "Expired" indicator
- View all timers on the dashboard
- Click a timer to see details
- Click "Checkout Time" to reserve a portion of time
- Start the timer to begin counting down
- Pause/Stop as needed
- Unused time is returned to the daily pool
GET /api/admin/settings- Get settings (timezone, PIN status)POST /api/admin/verify-pin- Verify PINPOST /api/admin/set-pin- Set/change PINPUT /api/admin/settings- Update settings (admin)
GET /api/people- List all people with their timersGET /api/people/:id- Get person by IDPOST /api/people- Create person (admin)PUT /api/people/:id- Update person (admin)DELETE /api/people/:id- Delete person (admin)
GET /api/timers- List all timers with today's allocationsGET /api/timers/:id- Get timer with today's allocationGET /api/timers/:id/expiration- Check if timer is expiredGET /api/timers/:id/allocation- Get allocation for specific datePOST /api/timers- Create timer (admin)PUT /api/timers/:id- Update timer (admin)DELETE /api/timers/:id- Delete timer (admin)
POST /api/checkouts- Create checkoutGET /api/checkouts/:id- Get checkout statusPOST /api/checkouts/:id/start- Start timerPOST /api/checkouts/:id/pause- Pause timerPOST /api/checkouts/:id/stop- Stop and return unused timePOST /api/checkouts/:id/cancel- Cancel checkout
The application uses several techniques to ensure data integrity:
- Prisma Transactions: All multi-step database operations are wrapped in transactions to ensure atomicity
- Race Condition Prevention: Frontend uses
useRefflags to prevent double-clicks and concurrent API calls - Auto-pause Protection: Active timers that complete automatically are protected against multiple pause attempts
# Systemd logs
sudo journalctl -u timer-app -f
# Application logs
cd /opt/timer-app
# Logs are output to stdout/stderrOption 1: Reset PIN via SQLite (Preserves Data)
# Open the database with SQLite
sudo sqlite3 /opt/timer-app/data/timer.db
# Reset the PIN (sets it to NULL, triggering first-time setup)
UPDATE Settings SET adminPinHash = NULL WHERE id = 1;
# Verify the change
SELECT id, adminPinHash, timezone FROM Settings;
# Exit SQLite
.quit
# Restart the service
sudo systemctl restart timer-app
# You'll be prompted to set a new PIN on first visitOption 2: Delete Database (WARNING: Deletes All Data)
# Stop the service
sudo systemctl stop timer-app
# Delete the database (WARNING: This deletes all data)
sudo rm /opt/timer-app/data/timer.db
# Restart the service
sudo systemctl start timer-app
# You'll be prompted to set a new PIN on first visit# Backup
sudo cp /opt/timer-app/data/timer.db /opt/timer-app/data/timer.db.backup
# Restore
sudo cp /opt/timer-app/data/timer.db.backup /opt/timer-app/data/timer.db
sudo systemctl restart timer-apptimer-app/
├── backend/
│ ├── prisma/
│ │ ├── schema.prisma # Database schema
│ │ └── migrations/ # Database migrations
│ ├── src/
│ │ ├── index.ts # Express app entry point
│ │ ├── routes/ # API route handlers
│ │ │ ├── admin.ts
│ │ │ ├── checkouts.ts
│ │ │ ├── people.ts
│ │ │ └── timers.ts
│ │ ├── middleware/ # Express middleware
│ │ │ └── adminAuth.ts
│ │ ├── utils/ # Utility functions
│ │ │ ├── dateTime.ts # Date/time helpers
│ │ │ └── timerExpiration.ts
│ │ └── tests/ # Backend tests
│ └── public/ # Built frontend (production)
├── frontend/
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── contexts/ # React contexts
│ │ ├── hooks/ # Custom hooks
│ │ ├── pages/ # Page components
│ │ ├── services/ # API client
│ │ ├── types/ # TypeScript types
│ │ └── utils/ # Utility functions
│ └── public/ # Static assets
├── e2e/ # Playwright E2E tests
├── playwright.config.ts # Playwright configuration
└── package.json # Root package.json with scripts
MIT