Precision time tracking for freelancers — punch in, punch out, get paid accurately.
PunchIn is a mobile-first, offline-capable Progressive Web App (PWA) for freelancers and independent contractors who need fast, no-friction time tracking. No accounts. No subscriptions. Your data stays on your device by default — nothing leaves it unless you opt in to sync.
Most time tracking tools are bloated, require an account, or bill you monthly for basic features. PunchIn is the opposite:
- Instant — open the app, tap Punch In, you're tracking
- Private — all data stored locally in your browser (IndexedDB); nothing leaves your device unless you opt in to sync
- Installable — works as a PWA; add it to your home screen and use it like a native app
- Offline-first — works without an internet connection, always
trackmytime.today — open it in any browser, add it to your home screen, and start tracking. No sign-up required.
Timer · Jobs · Labor Types · Daily Sheet · Weekly Sheet · Analytics · Settings
PunchIn adapts from pocket to desktop without a separate codebase, with a three-tier navigation shell: on phones, a top brand header + bottom tab bar; on tablets, that bottom bar gives way to a compact left icon rail; on desktop, to a full labelled sidebar carrying the brand and a live "On the clock" status. The Timer screen adds a right-hand overview rail on wide screens — last session, quick-punch shortcuts for your most-recent jobs, and a this-week total with a per-job breakdown. When installed as a PWA, it detects the host OS and applies platform-native behaviors automatically — iOS safe-area insets, Apple-style bottom sheets with swipe-to-dismiss and Taptic Engine feedback, and Material Design 3 sheets on Android with hardware back-button support.
Live Timer Dashboard
Start one or more timers across different jobs simultaneously. Each running timer shows the job, labor type, start time, optional notes, and a live elapsed-time counter updated every second. Tap Stop to end a timer. When no timers are active, the most recently completed session is shown as a "Last Session" card so your previous work is always visible.
Job & Labor Type Management
Organize your work into jobs (client projects) and labor types (billable categories like Design, Development, Consulting). Both support color-coded badges for fast visual identification. Archiving a job or labor type hides it from active dropdowns and groups it under a collapsible Archived section at the bottom of the list — searchable and restorable any time. Historical entries are never broken; archived records are always preserved.
Timesheets
Review your logged time by day or week. The weekly view shows a per-job breakdown with proportional bars so you can see at a glance where your time went. Full-text search filters entries by job name, client, labor type, or notes. Navigate between periods with arrows, log past entries manually, and edit or delete any record.
Export the current period as a CSV spreadsheet or a Print / PDF via the system print dialog — both available directly from the Timesheets toolbar.
Analytics
Charts powered by Recharts give you a visual overview of your workload over the last 7 or 30 days:
- Daily bar chart — hours logged per day
- Hours by job — horizontal bar chart sorted by volume
- Labor type donut — proportion of time by category
Invoice Generator
Set hourly rates per labor type on each job (Jobs tab → edit a job → Hourly rates), then open the Invoice modal from the Timesheets toolbar. Pick a single job or a whole client and a period, and PunchIn builds a line-item invoice — hours, rate, and amount for every entry, each line priced at that job's own rate.
The invoice carries your billing profile (Settings → Billing): a Billed from / Billed to band with your name, business, email, phone, and address — plus an optional business logo — and the chosen client as "Billed to". Amounts format in your default currency, clock times follow your 12/24-hour preference, and each entry can be rounded in your favour to a 15- or 30-minute increment. An optional, editable invoice number (prefix + auto-incrementing counter, or a custom alphanumeric code) prints on each invoice. Export as a formatted CSV or use Print / PDF to send it directly to a client.
Cross-Device Sync
Sync your data across devices using your existing cloud storage — no PunchIn account required. Choose one of three free providers:
- GitHub Gist (private) — ideal for developers; uses a private Gist in your account. Requires a GitHub OAuth App and a Cloudflare Worker secret to exchange the auth code server-side.
- Google Drive — stores a single hidden file in the app-specific
appDataFolder; never appears in your Drive file list. - OneDrive — stores a single file in your OneDrive App Folder.
Sync is a pull-then-push snapshot: PunchIn pulls the remote snapshot, merges any new entries from other devices using the same smart deduplication as JSON import, then pushes the unified state back. Google and OneDrive tokens expire after ~1 hour; PunchIn detects expiry and prompts you to reconnect.
Provider buttons only appear when the app is deployed with the corresponding VITE_* OAuth client ID. See .env.example for setup instructions.
Account-Free Device Transfer
Move your data between devices with no account and no cloud provider. In Settings → Transfer, tap Create share link to snapshot your database into a compressed link plus a scannable QR code — then open the link or scan the QR on the other device. Import from a link lets you paste a link or code directly. Imports show a confirmation with a summary ("Includes N jobs / M entries") before merging, and reuse the same smart deduplication as cloud sync so nothing is duplicated. Histories too large for a QR/URL fall back to a copyable link with a clear note. Everything happens client-side — no server is involved.
Reminders
Opt-in local notifications keep you on top of your tracking — no account or server required. In Settings → Reminders, enable the master toggle (which requests notification permission) and choose any combination of:
- Long-running timer — alerts when an active timer exceeds a configurable number of minutes
- No timer running — nudges you by a chosen time of day if nothing is tracking
- Timer still running — alerts at a chosen time if a timer is still going
- Daily / Weekly timesheet — reminders to log or review your time
Because there's no backend, reminders fire only while PunchIn is open or installed and running in the background — the Settings copy makes this clear and points iPhone/iPad users to add the app to their Home Screen first.
Settings & Data Portability
- Concurrent timers — toggle on or off; when off, starting a new timer automatically stops any running one
- Week start — choose whether your week starts on Monday or Sunday
- Time format — show clock times as 12-hour, 24-hour, or Auto (matches your device's preference) across timers, timesheets, and invoices
- Decimal hours — display durations as decimal hours (
1.50 h) instead of1h 30m - Rounding — optionally round each billable entry in your favour to a 15- or 30-minute increment (off by default)
- Theme — switch between Auto / Light / Dark (auto follows your OS preference)
- Accent color — choose from preset highlight swatches or pick any custom hex color; updates the entire app (and the browser-tab favicon) instantly
- Haptic feedback — toggle vibration on navigation and punch actions (shown only on phones, where a vibration motor exists)
- Install — add PunchIn to your home screen, with platform-aware guidance for Android, iOS Safari, and other iOS browsers
- Billing — your invoice identity (name, business, email, phone, address, payment terms, optional logo), the default currency used to format invoice and CSV amounts, and optional invoice numbering (prefix + counter)
- Export JSON — full backup of all data (jobs, labor types, entries)
- Export CSV — all completed entries as a spreadsheet ready for import into bookkeeping apps
- Import JSON — restore from a backup file (smart deduplication prevents duplicates)
- Danger Zone (collapsible) — Clear entries resets logged time while keeping jobs and labor types; Factory Reset wipes all data and restores the app to a clean slate (two confirmation steps required)
- Check for updates — applies any pending service-worker update and reloads; confirms you're on the latest version if none is found
- Header logo — tap the PunchIn logo to return to the Timer view from any screen
- About → PunchIn — opens the GitHub repository
- About → Changelog — browse version history and release notes
How It Works
All state lives in a local IndexedDB database named PunchInDB, managed by Dexie.
| Table | Purpose |
|---|---|
entries |
Time entries — punchOut: null means the timer is still running |
jobs |
Client projects; soft-archived via isActive |
laborTypes |
Billable categories with a color and glyph (an icon id); soft-archived via isArchived |
settings |
Key-value app preferences |
deletions |
Delete tombstones — a removed entry's uuid is recorded here so opt-in sync propagates the deletion across devices instead of it resurrecting from a peer's snapshot |
secrets |
At-rest-encrypted sync credentials — a non-extractable AES-GCM key plus the encrypted OAuth sync token (never stored in plaintext) |
Soft-deletion is used for jobs and laborTypes: those records are never hard-deleted so historical entries always retain valid references. Deleting an entry hard-deletes it and records a deletions tombstone so the removal propagates through opt-in sync.
No Redux, no global Context. Dexie's useLiveQuery hook makes the database reactive — components re-render automatically when data changes. Local React state handles UI concerns (open modals, active tab, search input).
Dark and light themes are implemented as CSS custom properties. The default "auto" setting tracks prefers-color-scheme via a matchMedia listener; users can override to force light or dark.
The accent color (nav, buttons, active states) is stored as a hex setting and converted to an --accent-rgb CSS variable at runtime. A single appAccent Tailwind token wires the entire UI to the user's chosen color.
A usePlatformContext() hook detects standalone mode and the host OS at runtime. When the app is installed:
- iOS —
env(safe-area-inset-top/bottom)pads the header and nav bar so nothing clips into the notch or home indicator. Modals render as Apple-style bottom sheets with a grabber pill, swipe-down-to-dismiss, and Taptic Engine haptic feedback via the WebKit<input switch>polyfill. - Android — Modals follow Material Design 3 (28 px top radius, 48 dp drag handle). The hardware back button closes open modals instead of exiting the app, implemented via
history.pushState+popstate. Dismiss firesnavigator.vibrate(40)for a crisp tick. - Browser tab — All of the above is bypassed; the original layout and modal behavior is unchanged.
Tech Stack & Project Structure
| Layer | Technology |
|---|---|
| Framework | React 19 |
| Build | Vite 8 |
| Styling | Tailwind CSS 4 + CSS custom properties |
| Database | Dexie 4 (IndexedDB) |
| Charts | Recharts 3 |
| Date utilities | date-fns 4 |
| Typography | Self-hosted Noto Sans / Display / Mono (variable WOFF2, OFL-1.1) — no CDN |
| Icons | lucide-react |
| PWA | vite-plugin-pwa |
| Hosting | Cloudflare Workers |
An abridged, illustrative map — sync internals (
sync/oauthState.js,sync/tokenStore.js,sync/pkce.js) andworker/iconRender.jsare omitted for brevity. The authoritative file-by-file tree lives indocs/ARCHITECTURE.md.
punchin/
├── package.json # Version source of truth
├── wrangler.jsonc # Cloudflare Workers deployment (routes OAuth + serves static assets)
├── .env.example # VITE_* OAuth env var documentation
├── worker/
│ └── oauth.js # Cloudflare Worker: GitHub OAuth code→token exchange
├── app/
│ └── index.html # App shell, fonts, theme-color meta
├── config/
│ ├── vite.config.js # Vite + Vitest + PWA config
│ ├── postcss.config.js # PostCSS pipeline
│ └── tailwind.config.js # Custom font families + CSS-variable-backed color tokens
├── docs/
│ └── CHANGELOG.md # Version history
└── src/
├── main.jsx # React entry point
├── App.jsx # Root: tab state, theme, OAuth callback handling
├── db.js # Dexie schema, seed data, migrations
├── index.css # CSS variables (dark/light), scrollbar utils
├── sync/
│ ├── config.js # OAuth client IDs from VITE_* build env
│ ├── syncManager.js # pull→merge→push snapshot sync, disconnect
│ └── providers/
│ ├── github.js # GitHub Gist API (OAuth + CRUD)
│ ├── google.js # Google Drive appDataFolder API
│ └── onedrive.js # Microsoft Graph App Folder API
├── components/
│ ├── Layout.jsx # Fixed header + bottom nav
│ ├── ErrorBoundary.jsx # Error boundary wrapping each view
│ ├── TimerCard.jsx # Live timer card (1 s interval)
│ ├── StartTimerModal.jsx # Punch-in form
│ ├── EditEntryModal.jsx # Edit active or completed entry
│ ├── InvoiceModal.jsx # Invoice generator with CSV/print export
│ ├── ConfirmModal.jsx # Accessible confirmation dialog
│ ├── ColorPicker.jsx # Preset swatches + custom hex input
│ ├── BrandMark.jsx # Stopwatch mark tile + accent-tinted wordmark
│ ├── LaborGlyph.jsx # Labor-type tag/chip (glyph + colour)
│ ├── EntitySelect.jsx # Bespoke colour/glyph single-select picker
│ ├── GlyphPicker.jsx # Labor-type glyph picker (quick row + search)
│ ├── TimerRail.jsx # Desktop Timer right rail (last session · quick punch · week)
│ ├── ChangelogModal.jsx # Changelog viewer (built from docs/CHANGELOG.md)
│ ├── DataTransfer.jsx # Account-free device-to-device transfer (link + QR)
│ └── InstallPromptModal.jsx # First-run install bottom sheet (platform-aware)
├── views/
│ ├── TimerView.jsx # Active timers list
│ ├── JobsView.jsx # Jobs & Labor Types CRUD
│ ├── TimesheetsView.jsx # Daily/weekly logs + search
│ ├── AnalyticsView.jsx # Charts
│ ├── SettingsView.jsx # iOS-style drill-in root (master-detail on desktop)
│ └── settings/ # Per-panel UI: General / Appearance / Reminders / Install / Billing / Data & Sync / About panels (+ shared components.jsx)
├── hooks/
│ ├── useSettings.js # Reactive settings hook
│ ├── usePlatformContext.js # Standalone + OS detection
│ ├── useInstallPrompt.js # PWA install state + prompt
│ ├── useReminders.js # Local reminder scheduler (no backend)
│ └── useHapticFeedback.jsx # Platform-routed haptic trigger
└── utils/
├── time.js # Date/time helpers
├── printDocument.js # Shared invoice/timesheet print document (self-hosted Noto, fonts-ready gating)
├── favicon.js # Accent-colored tab favicon
├── notifications.js # Browser Notification API wrappers
├── reminders.js # Pure reminder-rule evaluation
├── transfer.js # Device-transfer codec (gzip + base64url, QR)
├── deviceId.js # Stable per-device identifier
└── pwa.js # PWA install/update state bridge
See CONTRIBUTING.md for setup instructions, workflow, and conventions.
This project follows a Code of Conduct — by participating, you agree to uphold it.
All contributors must agree to the Contributor License Agreement before their pull request can be merged.
Found a security vulnerability? Please report it privately — see SECURITY.md for how. Do not open a public issue for security reports.
PunchIn's interface is set in Google's Noto type family — Noto Sans (UI text), Noto Sans Display (headings and the wordmark), and Noto Sans Mono (timers) — self-hosted as variable WOFF2 files (no CDN; the worker CSP is font-src 'self'). The Noto fonts are licensed under the SIL Open Font License 1.1; see docs/THIRD-PARTY-LICENSES.md for full attribution.
PunchIn Time Tracker is source-available under the Business Source License 1.1. Free for personal, non-commercial use by individual freelancers. Commercial use requires a separate license — contact the licensor. The license converts to AGPL-3.0 on 2030-06-02.
Contributions are accepted under the Contributor License Agreement.

























