Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Aking16/timify/llms.txt

Use this file to discover all available pages before exploring further.

Time tracking in Timify is designed to stay out of your way. A single button press starts a timer against the active project; another press stops it. Behind the scenes, Timify records startTime and endTime as Unix timestamps, calculates duration in seconds, and immediately reflects the running entry as a live, ticking clock in the UI. All data is stored locally in SQLite so everything works completely offline.

Time entry data shape

Each row in the time_entries table holds the following fields.
id
string
required
UUID primary key, auto-generated with crypto.randomUUID() on insert.
userId
string
required
Foreign key referencing the user table. Deleting a user cascades and removes all their time entries.
projectId
string
Foreign key referencing the projects table. Set to null if the associated project is deleted (onDelete: "set null").
title
string
Short label for the entry. Must be between 4 and 32 characters when edited. New entries default to a placeholder title.
description
string
Longer description. Must be at least 4 characters when edited.
startTime
timestamp
When the timer was started. Defaults to the current Unix time via unixepoch(). Used as the reference point for all duration calculations.
endTime
timestamp
When the timer was stopped. null while the entry is still running.
duration
integer
Elapsed time in seconds, stored as a calculated field when the timer stops: Math.floor((endTime - startTime) / 1000). null while running.
isRunning
boolean
true while the timer is active. Defaults to true on insert. Set to false when stopped.
billable
boolean
Whether this entry should count toward billing. Defaults to false.
hourlyRate
number
Per-entry rate override. When set, this takes precedence over the parent project’s hourlyRate. Optional — null means “use project rate”.
createdAt
timestamp
Creation timestamp, set automatically by SQLite.
updatedAt
timestamp
Last-updated timestamp, refreshed on every write via Drizzle’s $onUpdateFn.

Starting a timer

1

Select an active project

A project must be selected before a timer can start. If none is active, Timify redirects you to /app/projects. The active project is stored in localStorage under the key "active-project".
2

Press Start Timer

Click the Start Timer button in the sidebar or the bottom navigation bar. This calls the createTimeEntry server action with the current projectId.
3

Existing timers are stopped automatically

Before inserting the new entry, createTimeEntry queries all running entries for your user (isRunning = true) and stops each one by computing its duration and setting isRunning = false, endTime = now.
4

New entry is inserted

A fresh row is inserted into time_entries with isRunning = true, startTime defaulting to the current Unix time, and placeholder title / description values that you can edit immediately.
Only one timer can be running at a time per user. Starting a second timer automatically stops all currently running entries before creating the new one.

Real-time duration display

While a timer is running, the duration shown in the entry card ticks every second. This is powered by the useRealtimeDuration hook.
// src/hooks/use-realtime-duration.ts
export function useRealtimeDuration(
  startTime: Date | null,
  isRunning: boolean,
  staticDuration?: number | null
) {
  const [duration, setDuration] = useState<string | number>(() =>
    isRunning
      ? calculateDuration({ startTime, showFormatted: true })
      : formatDuration(staticDuration ?? 0)
  );

  // Sync static duration when entry is no longer running
  useEffect(() => {
    if (!isRunning) {
      setDuration(formatDuration(staticDuration ?? 0));
    }
  }, [staticDuration, isRunning]);

  // Tick every second only when isRunning is true
  useInterval(
    () => {
      if (startTime) {
        const newDuration = calculateDuration({ startTime, showFormatted: true });
        setDuration(newDuration);
      }
    },
    isRunning ? 1000 : undefined
  );

  return duration;
}

Running entry

The hook fires calculateDuration every 1 000 ms using ahooks/useInterval, displaying elapsed time as H:MM:SS or MM:SS.

Stopped entry

The interval is paused (delay = undefined) and the formatted staticDuration from the database is displayed instead.

Duration formatting

The formatDuration(seconds) utility converts raw seconds to a human-readable string:
// src/lib/calculate-duration.ts
export function formatDuration(seconds: number): string {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = seconds % 60;

  if (hours > 0) {
    return `${hours}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  }
  return `${minutes}:${secs.toString().padStart(2, "0")}`;
}

Stopping a timer

Click the Stop button on the running entry card. This calls stopTimeEntry with the entry’s id.
duration = Math.floor((now - startTime) / 1000)   // in seconds
The action then writes isRunning: false, endTime: now, and duration to the database in a single db.update call and revalidates the get-time-entries cache tag.
If stopTimeEntry is called on an entry where isRunning is already false, the action returns an error and no update is made.

Editing a time entry

Open the entry’s edit panel by clicking the edit (pencil) icon on an entry card. The editTimeEntry server action accepts the following fields.

Validation rules for editTimeEntry

FieldRule
idRequired · existing entry UUID
titleRequired · 4–32 characters
descriptionRequired · minimum 4 characters
startTimeRequired · ISO date string, converted to Date
endTimeOptional · ISO date string, converted to Date
After validation, duration is recalculated from the new startTime and endTime:
const durationInSeconds = Math.floor(
  ((endTime?.getTime() ?? 0) - startTime.getTime()) / 1000
);
You can manually adjust startTime and endTime to correct an accidentally started or forgotten entry. The duration is always recomputed on save.

Deleting a time entry

Call deleteTimeEntry(id) from the entry card’s action menu. The action verifies your session, runs db.delete(timeEntries).where(eq(timeEntries.id, id)), and revalidates the entry list. This action is permanent — there is no undo.

Billable entries and hourly rate override

Each entry has a billable boolean (default false) and an optional hourlyRate (default null). When hourlyRate is set on an entry it overrides the parent project’s rate for any billing calculations in reports. Toggle the Billable switch in the entry edit panel to mark work as billable.

Build docs developers (and LLMs) love