[{"data":1,"prerenderedAt":836},["ShallowReactive",2],{"navigation":3,"\u002Freference\u002Fdata-model":80,"\u002Freference\u002Fdata-model-surround":831},[4,9,14,50,75],{"title":5,"path":6,"stem":7,"icon":8},"Getting started","\u002Fgetting-started","1.getting-started","i-lucide-rocket",{"title":10,"path":11,"stem":12,"icon":13},"Installation","\u002Finstallation","2.installation","i-lucide-hard-drive-download",{"title":15,"path":16,"stem":17,"children":18,"page":49},"Guide","\u002Fguide","3.guide",[19,24,29,34,39,44],{"title":20,"path":21,"stem":22,"icon":23},"Timer & entries","\u002Fguide\u002Ftimer-and-entries","3.guide\u002F1.timer-and-entries","i-lucide-timer",{"title":25,"path":26,"stem":27,"icon":28},"Projects, clients & tasks","\u002Fguide\u002Fprojects-clients-tasks","3.guide\u002F2.projects-clients-tasks","i-lucide-git-branch",{"title":30,"path":31,"stem":32,"icon":33},"Reports & calendar","\u002Fguide\u002Freports-calendar","3.guide\u002F3.reports-calendar","i-lucide-chart-column",{"title":35,"path":36,"stem":37,"icon":38},"Theme editor","\u002Fguide\u002Ftheme-editor","3.guide\u002F4.theme-editor","i-lucide-palette",{"title":40,"path":41,"stem":42,"icon":43},"Importing data","\u002Fguide\u002Fimport","3.guide\u002F5.import","i-lucide-hard-drive-upload",{"title":45,"path":46,"stem":47,"icon":48},"Team & organizations","\u002Fguide\u002Fteam","3.guide\u002F6.team","i-lucide-users",false,{"title":51,"path":52,"stem":53,"children":54,"page":49},"Reference","\u002Freference","4.reference",[55,60,65,70],{"title":56,"path":57,"stem":58,"icon":59},"Data model","\u002Freference\u002Fdata-model","4.reference\u002F1.data-model","i-lucide-database",{"title":61,"path":62,"stem":63,"icon":64},"API routes","\u002Freference\u002Fapi","4.reference\u002F2.api","i-lucide-route",{"title":66,"path":67,"stem":68,"icon":69},"Environment variables","\u002Freference\u002Fenv","4.reference\u002F3.env","i-lucide-settings-2",{"title":71,"path":72,"stem":73,"icon":74},"Security","\u002Freference\u002Fsecurity","4.reference\u002F4.security","i-lucide-shield",{"title":76,"path":77,"stem":78,"icon":79},"About","\u002Fabout","5.about","i-lucide-info",{"id":81,"title":56,"body":82,"description":825,"extension":826,"meta":827,"navigation":828,"path":57,"seo":829,"stem":58,"__hash__":830},"docs\u002F4.reference\u002F1.data-model.md",{"type":83,"value":84,"toc":816},"minimark",[85,100,105,463,477,499,521,525,553,562,566,580,602,606,626,660,679,725,729],[86,87,88,89,93,94,99],"p",{},"One PostgreSQL database, managed by Drizzle ORM. Migrations live in ",[90,91,92],"code",{},"server\u002Fdb\u002Fmigrations"," and ",[95,96,98],"a",{"href":97},"\u002Finstallation#automatic-migrations","apply automatically"," on startup.",[101,102,104],"h2",{"id":103},"entities","Entities",[106,107,108,121],"table",{},[109,110,111],"thead",{},[112,113,114,118],"tr",{},[115,116,117],"th",{},"Table",[115,119,120],{},"Columns (essentials)",[122,123,124,165,178,213,237,279,308,326,375,391,411,443],"tbody",{},[112,125,126,132],{},[127,128,129],"td",{},[90,130,131],{},"users",[127,133,134,137,138,137,141,144,145,148,149,152,153,156,157,160,161,164],{},[90,135,136],{},"id",", ",[90,139,140],{},"name",[90,142,143],{},"email"," (unique), ",[90,146,147],{},"password_hash"," (scrypt, ",[90,150,151],{},"salt:hex","), ",[90,154,155],{},"default_rate"," ($\u002Fh), ",[90,158,159],{},"theme"," (jsonb), ",[90,162,163],{},"session_version"," (int, revocation stamp)",[112,166,167,172],{},[127,168,169],{},[90,170,171],{},"orgs",[127,173,174,137,176],{},[90,175,136],{},[90,177,140],{},[112,179,180,185],{},[127,181,182],{},[90,183,184],{},"org_members",[127,186,187,190,191,194,195,198,199,202,203,202,206,152,209,212],{},[90,188,189],{},"org_id"," + ",[90,192,193],{},"user_id"," (PK), ",[90,196,197],{},"role"," (",[90,200,201],{},"owner"," | ",[90,204,205],{},"admin",[90,207,208],{},"member",[90,210,211],{},"rate"," (per-member override)",[112,214,215,220],{},[127,216,217],{},[90,218,219],{},"clients",[127,221,222,137,224,137,226,137,228,137,231,137,234],{},[90,223,136],{},[90,225,189],{},[90,227,140],{},[90,229,230],{},"rate?",[90,232,233],{},"deleted_at?",[90,235,236],{},"delete_batch_id?",[112,238,239,244],{},[127,240,241],{},[90,242,243],{},"projects",[127,245,246,137,248,137,250,137,252,137,255,137,257,137,260,137,263,198,266,202,269,152,272,137,275,137,277],{},[90,247,136],{},[90,249,189],{},[90,251,140],{},[90,253,254],{},"client_id?",[90,256,230],{},[90,258,259],{},"billable_default",[90,261,262],{},"estimate_minutes?",[90,264,265],{},"visibility",[90,267,268],{},"private",[90,270,271],{},"public",[90,273,274],{},"archived",[90,276,233],{},[90,278,236],{},[112,280,281,286],{},[127,282,283],{},[90,284,285],{},"tasks",[127,287,288,137,290,137,292,137,294,137,297,137,299,137,301,137,304,137,306],{},[90,289,136],{},[90,291,189],{},[90,293,140],{},[90,295,296],{},"project_id?",[90,298,230],{},[90,300,262],{},[90,302,303],{},"done",[90,305,233],{},[90,307,236],{},[112,309,310,315],{},[127,311,312],{},[90,313,314],{},"tags",[127,316,317,137,319,137,321,323,324],{},[90,318,136],{},[90,320,189],{},[90,322,140],{}," (lowercase, unique per org), ",[90,325,233],{},[112,327,328,333],{},[127,329,330],{},[90,331,332],{},"time_entries",[127,334,335,137,337,137,339,137,341,137,343,198,346,202,349,202,352,355,356,137,359,137,362,137,365,137,368,137,371,137,373],{},[90,336,136],{},[90,338,189],{},[90,340,193],{},[90,342,140],{},[90,344,345],{},"ref_type",[90,347,348],{},"client",[90,350,351],{},"project",[90,353,354],{},"task"," | null), ",[90,357,358],{},"ref_id?",[90,360,361],{},"billable",[90,363,364],{},"rate_override?",[90,366,367],{},"start",[90,369,370],{},"end?",[90,372,233],{},[90,374,236],{},[112,376,377,382],{},[127,378,379],{},[90,380,381],{},"entry_tags",[127,383,384,190,387,390],{},[90,385,386],{},"entry_id",[90,388,389],{},"tag_id"," (PK)",[112,392,393,398],{},[127,394,395],{},[90,396,397],{},"delete_batches",[127,399,400,137,402,137,404,160,407,410],{},[90,401,136],{},[90,403,189],{},[90,405,406],{},"relinked",[90,408,409],{},"created_at"," — one cascade delete, so undo needs only this id",[112,412,413,418],{},[127,414,415],{},[90,416,417],{},"invites",[127,419,420,137,422,137,424,137,426,198,428,202,430,202,432,152,434,144,437,137,440],{},[90,421,136],{},[90,423,189],{},[90,425,143],{},[90,427,197],{},[90,429,201],{},[90,431,205],{},[90,433,208],{},[90,435,436],{},"token",[90,438,439],{},"expires_at",[90,441,442],{},"accepted_at?",[112,444,445,450],{},[127,446,447],{},[90,448,449],{},"password_reset_tokens",[127,451,452,137,454,137,456,144,458,137,460],{},[90,453,136],{},[90,455,193],{},[90,457,436],{},[90,459,439],{},[90,461,462],{},"used_at?",[86,464,465,468,469,471,472,476],{},[90,466,467],{},"delete_batch_id"," is how one cascade delete is undone whole: every row it trashes is stamped with the ",[90,470,397],{}," id, and the batch row carries the relink snapshot for the rows it ",[473,474,475],"em",{},"detached"," (those stay alive, so nothing on them records which cascade cleared their parent). The stamp is cleared whenever a row leaves the trash, so a later delete is never undone by an older batch. Batches are dropped with the rest of the trash after 30 days.",[86,478,479,480,483,484,486,487,490,491,494,495,498],{},"All timestamps are ",[90,481,482],{},"timestamptz",". A task's ",[90,485,211],{}," applies whether the task belongs to a project or stands alone, and it overrides the project chain until cleared (Rule 2). Note what is otherwise ",[473,488,489],{},"absent",": entries have no ",[90,492,493],{},"project_id","\u002F",[90,496,497],{},"client_id"," columns — that's Rule 1.",[86,500,501,503,504,93,506,137,508,503,510,93,512,503,514,516,517,520],{},[90,502,197],{}," on ",[90,505,184],{},[90,507,417],{},[90,509,265],{},[90,511,243],{},[90,513,345],{},[90,515,332],{}," are text columns with ",[90,518,519],{},"CHECK"," constraints rather than enums, so the allowed values live in one place and migrate cheaply.",[101,522,524],{"id":523},"sessions-and-revocation","Sessions and revocation",[86,526,527,528,531,532,536,537,540,541,544,545,548,549,552],{},"Sessions are sealed cookies — there is no session table. ",[90,529,530],{},"users.session_version"," is what makes revocation possible: the version is stamped into the cookie at login, every authenticated request compares the stamp against the live column, and a password ",[533,534,535],"strong",{},"reset"," or ",[533,538,539],{},"change"," increments it. Cookies carrying an older stamp are cleared and answered ",[90,542,543],{},"401"," on their next request. A password change re-stamps the calling session so only ",[473,546,547],{},"other"," devices are logged out; a reset logs out everything. See ",[95,550,71],{"href":551},"\u002Freference\u002Fsecurity#sessions",".",[86,554,555,557,558,561],{},[90,556,449],{}," rows are single-use and live one hour. Finishing a reset marks the used token ",[473,559,560],{},"and"," every other outstanding token for that user as used, so an older link in an inbox cannot be replayed.",[101,563,565],{"id":564},"running-timers","Running timers",[86,567,568,569,571,572,575,576,579],{},"A timer is not a separate concept: it is a ",[90,570,332],{}," row with ",[90,573,574],{},"end IS NULL",". A user may have ",[533,577,578],{},"up to 10"," of them at once, and every timer route names one by its entry id.",[86,581,582,583,503,586,589,590,593,594,597,598,601],{},"Ten is a guardrail, not an invariant, so no index enforces it: a partial index (",[90,584,585],{},"time_entries_user_running_idx",[90,587,588],{},"user_id, start"," where ",[90,591,592],{},"end is null and deleted_at is null",") makes the running set cheap to read, and ",[90,595,596],{},"POST \u002Fapi\u002Ftimers"," counts and inserts inside one transaction holding ",[90,599,600],{},"pg_advisory_xact_lock(hashtext(user_id))",", so simultaneous starts can't overshoot the cap. The eleventh start gets HTTP 409.",[101,603,605],{"id":604},"the-rules","The rules",[86,607,608,611,612,615,616,494,618,621,622,625],{},[533,609,610],{},"Rule 1 — deepest reference only."," An entry stores one of ",[90,613,614],{},"client | project | task | nothing"," in ",[90,617,345],{},[90,619,620],{},"ref_id",". Project and client are derived at read time (",[90,623,624],{},"task → project → client","), never copied. Re-parenting a task re-files its history.",[86,627,628,631,634,635,637,638,641,642,645,646,649,650,137,653,656,657,552],{},[533,629,630],{},"Rule 2 — rate inheritance, first non-null wins.",[90,632,633],{},"entry.rate_override → task.rate → project.rate → client.rate → org_member.rate → user.default_rate",".\nA task's rate applies whether the task belongs to a project or is standalone; clearing it falls back to the project chain, same as clients and projects today. ",[90,636,361],{}," defaults from ",[90,639,640],{},"project.billable_default"," when a project resolves, else ",[90,643,644],{},"true",". ",[90,647,648],{},"amount = hours × rate",", only when billable. Resolution happens server-side; DTOs carry ",[90,651,652],{},"resolvedRate",[90,654,655],{},"rateOverridden"," and (for projects and tasks) ",[90,658,659],{},"rateSource",[86,661,662,665,666,668,669,671,672,674,675,678],{},[533,663,664],{},"Rule 3 — cascade delete."," Client\u002Fproject deletion cascades downward via explicit checkboxes (Projects ⇒ Tasks ⇒ Entries). Unchecked survivors are ",[473,667,475],{},", not deleted: projects lose ",[90,670,497],{},", tasks lose ",[90,673,493],{},", entries pointing at a deleted object get ",[90,676,677],{},"ref_type = null",". Time is never lost silently.",[86,680,681,684,685,688,689,692,693,695,696,699,700,703,704,706,707,710,711,714,715,717,718,721,722,552],{},[533,682,683],{},"Rule 4 — soft deletion."," Every delete sets ",[90,686,687],{},"deleted_at"," and lands in the 30-day trash; hard-purge happens automatically after 30 days (on the next trash load) or explicitly via ",[473,690,691],{},"Delete forever",". Deleting a tag only strips labels (",[90,694,381],{}," rows), never time. Every delete API returns a ",[90,697,698],{},"DeleteResult"," snapshot (",[90,701,702],{},"deleted"," ids per entity + ",[90,705,475],{}," counts) that ",[90,708,709],{},"POST \u002Fapi\u002Frestore"," can undo exactly; cascade deletes return a ",[90,712,713],{},"CascadeDeleteResult",", which adds a ",[90,716,406],{}," snapshot of the references they cleared and a ",[90,719,720],{},"batch_id",". Undo a cascade by posting that one id — an id snapshot cannot describe a delete of tens of thousands of entries inside a 1MB request. The undo window itself is a per-browser preference — 8 seconds by default, 3–30 in ",[533,723,724],{},"Settings → Profile",[101,726,728],{"id":727},"dtos","DTOs",[86,730,731,732,735,736,739,740,743,744,137,746,137,749,137,752,152,754,757,758,757,761,757,764,767,768,137,771,137,774,137,777,757,780,783,784,137,787,137,790,137,793,137,796,799,800,803,804,190,806,137,809,137,812,815],{},"API responses use shared DTO shapes (see ",[90,733,734],{},"shared\u002Ftypes\u002F","): ",[90,737,738],{},"EntryDto"," (with resolved ",[90,741,742],{},"ref"," chain, ",[90,745,652],{},[90,747,748],{},"durationSec",[90,750,751],{},"amount",[90,753,314],{},[90,755,756],{},"ClientDto"," \u002F ",[90,759,760],{},"ProjectDto",[90,762,763],{},"TaskDto",[90,765,766],{},"TagDto"," (with usage counts and tracked totals), ",[90,769,770],{},"TimerState",[90,772,773],{},"DashboardSummary",[90,775,776],{},"ReportSummary",[90,778,779],{},"ImportPreview",[90,781,782],{},"ImportCommitResult",", the settings DTOs (",[90,785,786],{},"OrgInfoDto",[90,788,789],{},"OrgMemberDto",[90,791,792],{},"InviteDto",[90,794,795],{},"InviteLookupDto",[90,797,798],{},"TrashDto",") and the mail DTOs (",[90,801,802],{},"InviteCreateDto"," with its ",[90,805,436],{},[90,807,808],{},"emailSent",[90,810,811],{},"ForgotResponseDto",[90,813,814],{},"ResetResponseDto","). Clients never compute chains or rates — the server resolves everything.",{"title":817,"searchDepth":818,"depth":818,"links":819},"",2,[820,821,822,823,824],{"id":103,"depth":818,"text":104},{"id":523,"depth":818,"text":524},{"id":564,"depth":818,"text":565},{"id":604,"depth":818,"text":605},{"id":727,"depth":818,"text":728},"The PostgreSQL entities and the four rules that govern references, rates and deletion.","md",{},{"icon":59},{"title":56,"description":825},"dccT0BVro2DFC_pooxHBDYEMgcVHqQKx17BRE-LPLMY",[832,834],{"title":45,"path":46,"stem":47,"description":833,"icon":48,"children":-1},"Orgs, member roles, per-member rates, invites, and the password-reset flow.",{"title":61,"path":62,"stem":63,"description":835,"icon":64,"children":-1},"Every server route under \u002Fapi — JSON, zod-validated, session-guarded.",1790777312389]