How to build a Toggl alternative
How to build a Toggl alternative with a start button, a daily total, and a week you trust. Follow the prompts, check each step, then skip surveillance.
You can learn how to build a Toggl alternative as a start button, a daily total, and a week that adds up. Toggl Track is that product. The catalog line is “One button when work starts. Honest answers about where the week went.”
You pay $9–$18 a person a month. That is $108–$216 a year for every person on the team, and the card says the price scales with the team. The verdict is “Track effort, not people”. The first estimate is 8 hours.
I already wrote the teardown, Build a Toggl alternative. This guide is the walk. Prompts in order, then what to look at.
The audience is a product team of up to 10 people tracking effort across a handful of projects. If you need native apps, a billing export, or a desktop tracker watching the team, keep paying. The same v1 also replaces Clockify if what you wanted was a start button you own.
Hours that later become an invoice belong in Harvest, $13–$17.50 a person a month, about 4 hours. I walked that as How to build a Harvest alternative. A numbered PDF after the week exists is FreshBooks, $23–$60 a month, about 10 hours.
How to build a Toggl alternative without watching the team
The Toggl Track page splits the work into three rows.
The first row is the one you want. A one-click timer with a description, a project, and a task. Manual entries when you forgot to press start. A weekly view that adds those rows up. That is the 8-hour v1.
The second row is 3 days. Projects with members, tags, estimates, team reports, and a CSV. Do that after one real week already has a total you trust.
The third row is a month and up. Native desktop and mobile apps, integrations, billing. Skip it.
If you tell an agent to “clone Toggl”, it aims at the third row. Pin every prompt to the first one.
v1 does this. Flavio opens Daytrace, picks October writing and Blog posts, hits start at 09:30 in Rome, switches to Edits at lunch so the old stop equals the new start, types Thursday’s forgotten hour, and reads Friday as 90 minutes plus whatever is still running. Tags, estimates, and a team CSV stay off the page. Surveillance stays off forever.
The parts and where a request goes
Every product prompt ships in two flavors: Node.js plus Astro, and TanStack Start. The capability line for Toggl Track is Astro, HTMX, and D1. This site is an Astro app on Cloudflare, so that is the path I default to. Use TanStack Start if React is your weekday language. Pick one stack and stay on it. The timer still has to live in one SQL row.
The stack post is which tab you copy. The prompt post is how you pin the first estimate. The scope post is why native apps wait.
Open Build your own Toggl Track. The guardrail is the useful sentence: never capture screenshots, keystrokes, app activity, or location. Members control their own timers, and every admin edit is visible in history. The non-goal sits next to it. No invoicing, payroll, activity monitoring, timesheet approvals, or native apps.
Start, stop, switch, and a typed hour are ordinary form posts. Nothing runs after the response. If the plan invents a screenshot agent or a nightly digest, send it back.
The data the timer carries
The card lists TeamMember, Project, Task, Tag, TimeEntry, and ProjectEstimate. Tonight you need those minus Tag and ProjectEstimate. Tags and estimates belong on the 3-day row.
Flavio is a member with a named timezone, Europe/Rome. He owns projects. October writing has tasks, Blog posts and Edits. A time entry belongs to one member, and usually one project and one task.
There are two kinds of entry. A timed row has UTC startedAt and stoppedAt. Duration is elapsed seconds between those instants. A typed row has a localDate and a duration, and no start instant. Overlap checks apply only to timed rows.
A running timer is a timed row with stoppedAt still empty. At most one of those exists per member. Starting the next task stops the current one at the same instant.
Store start and stop in UTC. Keep the timezone on the member, or copy it onto the row when the entry starts. Daily totals use local day boundaries. The stored duration never changes when you cross midnight or a daylight-saving shift.
Every query is scoped by the member. Only the owner edits their time by default. A guessed entry id from another account returns nothing: no project name, no note, no 404 that confirms the slug.
The screens you ship
Three surfaces. Keep them boxes and labels. If the agent starts matching Toggl’s live UI, send it back. Reports wait for the 3-day row, so a filterable CSV is not a screen tonight.
The timer is the product. Current duration. Description. Project and task. Start and stop. Today’s entries. A continue action on a recent row. Today’s total. A recovery banner if a timer has been running for hours because the laptop slept.
The week is how you trust the log. Monday to Sunday in Europe/Rome. Each day lists timed slots and typed hours. A day total is the local wall-clock allocation. The week total is the sum of stored durations. Previous and next week stay on the page.
The projects page is names, not estimates. Active projects, their tasks, archive. No progress bar tonight.
Start, switch, and read the week
The first journey on the card is this walk. Start from a recent project. Add a note. Switch tasks. Read both rows on today, then on the week.
A timer that starts at 23:00 Rome and stops at 01:00 Rome is still two hours on the entry. The week puts one hour on Thursday and one hour on Friday. Spring-forward has 23 local hours. Fall-back has 25. Daily totals follow wall-clock slots. The weekly saved total still equals the sum of stored durations.
Harvest’s grid is a billable sheet you later submit. This week view is a log you read. No client, no rate, no submit.
Prompt 1. Scaffold Daytrace and stop
Copy the Astro prompt from the product page. Paste it into your coding tool. Put this block above it.
Ship the 8-hour row only.
One-click timer, manual entries,
weekly view.
Project name: Daytrace.
Public URL is https://hours.flaviocopes.com.
Owner is Flavio.
Timezone Europe/Rome.
Seed project: October writing.
Tasks: Blog posts, Edits.
Do not add tags, estimates,
team reports, or CSV export.
Do not add invoicing, payroll,
timesheet approvals, billing,
native apps, or any capture of
screenshots, keystrokes, apps,
or location.
When the plan is done, stop.
Do not write code until I say go.
The official prompt already asks for a short technical plan first: routes, tables, authorization, jobs. Let it produce that plan. Read it.
You want /timer for the runner and today’s total, /week/2026-10-26 for the Monday-start week, /projects for names. Tables for members, projects, tasks, and time entries. No tags, no estimates, no invoices, no activity_events.
If the plan includes a desktop watcher, Stripe, or a cron mailer, send it back. When the plan is narrow, say go.
What to check after the scaffold
Start the app. Sign in as Flavio. You should see an empty timer and an add-entry action.
Open the migrations. time_entries stores started_at and stopped_at as UTC instants. There is a partial unique index so only one row per owner can be running. Duration is elapsed seconds, or integer minutes derived from those instants, not a float hour.
If the README is a placeholder, stop. It has to name the timezone, the one-runner rule, and the sentence that the app never watches screens.
Prompt 2. Start one timer and refuse a second
The first mini-prompt on the card is “The timer that refuses to run twice”. That is the product.
Build the timer for Daytrace
at /timer.
Flavio picks project, task, and
an optional note, then starts.
started_at is now, stored in UTC.
timezone on the row is Europe/Rome.
At most one running timer per
owner. Starting another entry
stops the current one in the
same transaction, at the same
instant, then inserts the new
runner. Two near-simultaneous
starts leave exactly one runner
and no overlap.
Stop writes stopped_at.
Duration is elapsed seconds
between the two UTC instants.
Never recompute it from a
local clock.
Today's list shows stopped
rows plus the runner.
Continue copies project, task,
and note onto a new start,
and stops whatever is running.
A timer left running longer
than 8 hours is flagged on
load. Suggest a stop time.
Never silently discard it.
Start and stop are the only
timer actions. No billable
toggle. No screenshot.
Done when I can start on Blog
posts, start on Edits through
two near-simultaneous requests,
and the database has one
running row and no overlap.
What to check after the timer runs
Start on Blog posts at 09:30 Rome. The header shows a running duration. Rome is on CET after 25 October 2026, so that instant is 2026-10-30T08:30:00.000Z.
Start on Edits without stopping. The first row now ends at that instant. One new runner. Refresh. Still one runner.
Hit start twice from two tabs in the same second. One running row.
Leave a test runner with a start 14 hours ago. Reload. A recovery banner, not a 14-hour saved day.
If started_at looks like 2026-10-30 09:30 with no Z and no offset, the agent stored local time. Send prompt 2 back.
Prompt 3. Split the week at local midnight
The second mini-prompt is “Midnight, DST, and the entry that lives on two days”. The week view is that layer.
Build the weekly view for
Daytrace at /week/2026-10-26.
Monday start in Europe/Rome.
Previous and next week.
Each day lists its timed
slots and typed hours.
Store start and stop in UTC.
Daily totals use local day
boundaries. An entry from
23:00 to 01:00 Rome shows
one hour on each local day.
The stored duration stays
two hours.
Spring-forward has 23 local
hours. Fall-back has 25.
Daily totals follow wall
clock slots. The week saved
total equals the sum of
stored durations.
The running timer is a
projected figure on today.
It is not part of the saved
week total.
Do not add tags.
Do not add a CSV.
Do not import calendar events.
Done when a 23:00–01:00 Rome
entry shows on Thursday and
Friday, the two day slots
sum to 2:00, and the week
saved total is 2:00.
What to check after the week splits
Stop a test timer at 01:00 Rome that started at 23:00 the night before. One entry. Two day slots. Stored duration is 7200 seconds, or 120 minutes.
Open /week/2026-10-26. Thursday shows 1:00. Friday shows 1:00. Week saved is 2:00.
Click next week and back. The same two hours are still there.
Start a live timer. Today’s projected total grows. Week saved does not. Export is not a button yet, so you check this by refreshing twice.
If the agent stored 23:00 as a local string and derived duration from it, stop. Duration comes from UTC instants only.
Prompt 4. Type the forgotten hours
Manual entries are the rest of the 8-hour row. The overlap fixer on the card is the rule next to that form. You do not need drag handles tonight. You need a form that refuses a collision.
Build manual entries for
Daytrace.
From /timer and from a day
on /week, Flavio can add a
typed row: localDate, duration,
project, task, note.
1.0 means 60 minutes.
0.25 is the smallest step.
Reject negatives and anything
above 16 hours.
Typed rows have no started_at.
They cannot overlap.
They add to the same day
total as timed rows.
He can also add a timed
manual row with a start and
a stop on that local date.
Reject zero and negative
spans. If the new interval
overlaps another timed row
he owns, show the collision
and offer trim, split, or
replace in one transaction.
Do not save the overlap.
Editing a day that holds one
typed row updates that row.
A day with several rows, or
any timed row, opens a
breakdown. Never overwrite a
timed entry by typing 3.5
into the day.
Do not add submit or approve.
Do not snapshot a rate.
Done when Thursday shows a
typed 1:00 on Blog posts,
Friday still has the 1:30
timed row, the week saved
total is 2:30, and typing
over Friday opens the
breakdown.
What to check after a typed hour lands
On /week/2026-10-26, add 1:00 on Thursday for Blog posts. Refresh. Thursday stays 1:00. Week saved is Thursday plus every stopped timed row.
Type 3.5 onto Friday. The page opens the breakdown with the 09:30–11:00 row. Those 90 minutes are still there.
Add a timed manual row that starts inside that Friday interval. The collision names the existing entry. Trim, split, or replace. After you pick one, no two timed rows overlap.
Hammer save on the typed Thursday hour. One row.
Open a private window with no cookie. /timer is a login wall. A guessed entry URL shows nothing useful.
Failures the agent ships anyway
Two running timers
A unique index on owner_id where stopped_at is null is the product. Application code that “checks first” loses the race. If two runners exist after a double click, the migration is wrong.
Local timestamps in the database
2026-10-30 09:30 without a zone will shift when you deploy, or when Rome changes offset. Store 2026-10-30T08:30:00.000Z. Display 09:30 to Flavio.
A silent 14-hour day
A closed laptop is not 14 hours of Blog posts. You notice when Friday’s total looks like a full waking day and there is no banner. Flag it. Suggest a stop. Let Flavio pick.
A desktop agent that watches the team
Screenshots, keystrokes, app titles, and location are surveillance. The guardrail names those on purpose. RescueTime is $7–$12 a month, about 3 days, when you want your own local app durations with a pause control. Do not bolt that onto Daytrace.
An invoice on the 8-hour row
Toggl the company will sell you more than a timer. The 8-hour row on this site will not. If invoices or billable_rate appears in the plan, you left the first estimate. Paste Friday’s 1:30 into FreshBooks by hand. The FreshBooks teardown is that other app.
Deploy the timer
Daytrace wants Astro, HTMX, and D1. Put it on a subdomain you already own, hours.flaviocopes.com. D1 holds the log. There is no cron on the 8-hour row.
The hosting post is that path: the adapter, then the bindings, then the custom domain.
Attach the domain on the Worker. Sign in as Flavio from your phone on cellular. Start a timer. Switch tasks. Type yesterday’s forgotten hour. Confirm /week on the laptop shows the same saved minutes after a refresh.
Take a D1 backup before you delete the paid account. The week view is the history you will miss.
Use it as the real log for a week. If you miss tags or a CSV, those are the 3-day row. Then cancel Toggl Track, or Clockify, whichever one you pay for.
What to skip on purpose
Leave out invoicing, payroll, automated activity monitoring, timesheet approvals, and native apps.
Tags, estimates, team reports, and the formula-safe CSV wait until one real week already matches a from-scratch sum. The fourth mini-prompt on the card lives there. Compute summary, detail, and file from the same durations. Exclude still-running timers. Escape fields that start with =, +, -, or @.
The first-project post still points at a tonight card. This is the weekend after that, when you still rent a timer to remember Tuesday.
A timesheet you already bill from is the Harvest guide. A public board that says whether the product is up is the status page guide. A cron that pings a URL is the uptime monitor guide. This app has no scheduled job at 8 hours.
More writeups land on the guides index. The audit is the catalog if you want a smaller first ship.
Start now
Go to Build your own Toggl Track. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate only: one-click timer, manual entries, weekly view. Eight hours.
Start one real task this afternoon. Stop it when you stand up.