[{"data":1,"prerenderedAt":1378},["ShallowReactive",2],{"navigation":3,"\u002Finstallation":80,"\u002Finstallation-surround":1373},[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":10,"body":82,"description":1367,"extension":1368,"meta":1369,"navigation":1370,"path":11,"seo":1371,"stem":12,"__hash__":1372},"docs\u002F2.installation.md",{"type":83,"value":84,"toc":1354},"minimark",[85,89,94,119,192,199,202,278,309,313,316,394,403,440,444,451,542,553,557,564,611,618,717,722,735,738,750,782,792,796,807,870,884,893,897,904,978,990,1012,1017,1021,1035,1048,1052,1059,1070,1084,1095,1098,1101,1261,1267,1271,1328,1350],[86,87,88],"p",{},"Tick is a single Nuxt server plus a PostgreSQL database. Pick whichever of the three setups fits your host.",[90,91,93],"h2",{"id":92},"docker-compose-recommended","Docker Compose (recommended)",[86,95,96,97,101,102,106,107,110,111,114,115,118],{},"The repo's ",[98,99,100],"code",{},"docker-compose.yml"," runs the app together with a bundled ",[103,104,105],"strong",{},"Postgres 17"," (persistent named volume ",[98,108,109],{},"tick-pgdata","). Compose reads ",[98,112,113],{},".env"," automatically; ",[98,116,117],{},"NUXT_SESSION_PASSWORD"," is required and the stack refuses to start without it.",[120,121,126],"pre",{"className":122,"code":123,"language":124,"meta":125,"style":125},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","git clone https:\u002F\u002Fgithub.com\u002Fticktimer\u002FTick.git tick && cd tick\ncp .env.example .env\n# edit .env: set NUXT_SESSION_PASSWORD (openssl rand -base64 36)\ndocker compose up -d\n","bash","",[98,127,128,158,170,177],{"__ignoreMap":125},[129,130,133,137,141,144,147,151,155],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"sBMFI","git",[129,138,140],{"class":139},"sfazB"," clone",[129,142,143],{"class":139}," https:\u002F\u002Fgithub.com\u002Fticktimer\u002FTick.git",[129,145,146],{"class":139}," tick",[129,148,150],{"class":149},"sMK4o"," &&",[129,152,154],{"class":153},"s2Zo4"," cd",[129,156,157],{"class":139}," tick\n",[129,159,161,164,167],{"class":131,"line":160},2,[129,162,163],{"class":135},"cp",[129,165,166],{"class":139}," .env.example",[129,168,169],{"class":139}," .env\n",[129,171,173],{"class":131,"line":172},3,[129,174,176],{"class":175},"sHwdD","# edit .env: set NUXT_SESSION_PASSWORD (openssl rand -base64 36)\n",[129,178,180,183,186,189],{"class":131,"line":179},4,[129,181,182],{"class":135},"docker",[129,184,185],{"class":139}," compose",[129,187,188],{"class":139}," up",[129,190,191],{"class":139}," -d\n",[86,193,194,195,198],{},"Open ",[98,196,197],{},"http:\u002F\u002Flocalhost:3000"," and register your first account.",[86,200,201],{},"Notes on the compose file:",[203,204,205,217,237,253,264,275],"ul",{},[206,207,208,209,212,213,216],"li",{},"The app image defaults to ",[98,210,211],{},"ghcr.io\u002Fticktimer\u002Ftick:latest"," with a commented-out ",[98,214,215],{},"build: ."," line, so you can swap in a local build from a fresh clone.",[206,218,219,222,223,226,227,230,231,236],{},[98,220,221],{},"TICK_PORT"," changes the published host port, ",[98,224,225],{},"TICK_IP"," the host address it binds to, ",[98,228,229],{},"POSTGRES_PASSWORD"," the bundled database password (all optional, see the ",[232,233,235],"a",{"href":234},"#environment-variables","env table",").",[206,238,239,240,243,244,247,248,252],{},"Opening Tick over plain ",[98,241,242],{},"http:\u002F\u002F\u003CLAN-IP>:3000"," (no TLS) needs ",[98,245,246],{},"NUXT_SESSION_COOKIE_SECURE=false","; without it, sign-in shows a \"browser refused the sign-in cookie\" message and you stay logged out — see ",[232,249,251],{"href":250},"#plain-http-on-a-lan","Plain HTTP on a LAN",".",[206,254,255,256,259,260,263],{},"Behind a reverse proxy on the same host, set ",[98,257,258],{},"TICK_IP=127.0.0.1",". Docker writes published ports directly into iptables, so a port bound to ",[98,261,262],{},"0.0.0.0"," stays reachable even when ufw or firewalld claims to block it.",[206,265,266,267,270,271,274],{},"Postgres is not exposed to the host by default; uncomment the ",[98,268,269],{},"ports"," block in the ",[98,272,273],{},"db"," service if you want to reach it directly.",[206,276,277],{},"Both services have healthchecks; the app waits for the database to be healthy before starting.",[279,280,281],"note",{},[86,282,283,284,287,288,290,291,293,294,297,298,301,302,305,306,308],{},"The ",[98,285,286],{},"app"," service passes the database, session, session-cookie, migration, registration mode, rate limit and SMTP variables from ",[98,289,113],{}," through to the container, so setting them in ",[98,292,113],{}," is enough. Demo mode is deliberately ",[103,295,296],{},"not"," passed through (it wipes data): add it to the ",[98,299,300],{},"environment:"," block by hand, and only on a throwaway instance. Compose expands ",[98,303,304],{},"$"," inside ",[98,307,113],{}," values, so single-quote secrets that contain one.",[90,310,312],{"id":311},"docker-run-bring-your-own-postgres","docker run (bring your own Postgres)",[86,314,315],{},"If you already run Postgres elsewhere, the image alone is enough:",[120,317,319],{"className":122,"code":318,"language":124,"meta":125,"style":125},"docker run -d --name tick -p 3000:3000 \\\n  -e NUXT_DATABASE_URL='postgresql:\u002F\u002Ftick:password@db-host:5432\u002Ftick' \\\n  -e NUXT_SESSION_PASSWORD=\"$(openssl rand -base64 36)\" \\\n  ghcr.io\u002Fticktimer\u002Ftick:latest\n",[98,320,321,346,364,389],{"__ignoreMap":125},[129,322,323,325,328,331,334,336,339,342],{"class":131,"line":132},[129,324,182],{"class":135},[129,326,327],{"class":139}," run",[129,329,330],{"class":139}," -d",[129,332,333],{"class":139}," --name",[129,335,146],{"class":139},[129,337,338],{"class":139}," -p",[129,340,341],{"class":139}," 3000:3000",[129,343,345],{"class":344},"sTEyZ"," \\\n",[129,347,348,351,354,357,360,362],{"class":131,"line":160},[129,349,350],{"class":139},"  -e",[129,352,353],{"class":139}," NUXT_DATABASE_URL=",[129,355,356],{"class":149},"'",[129,358,359],{"class":139},"postgresql:\u002F\u002Ftick:password@db-host:5432\u002Ftick",[129,361,356],{"class":149},[129,363,345],{"class":344},[129,365,366,368,371,374,377,380,384,387],{"class":131,"line":172},[129,367,350],{"class":139},[129,369,370],{"class":139}," NUXT_SESSION_PASSWORD=",[129,372,373],{"class":149},"\"$(",[129,375,376],{"class":135},"openssl",[129,378,379],{"class":139}," rand -base64 ",[129,381,383],{"class":382},"sbssI","36",[129,385,386],{"class":149},")\"",[129,388,345],{"class":344},[129,390,391],{"class":131,"line":179},[129,392,393],{"class":139},"  ghcr.io\u002Fticktimer\u002Ftick:latest\n",[395,396,397],"warning",{},[86,398,399,400,402],{},"Generate ",[98,401,117],{}," once and keep it stable — it encrypts session cookies, and changing it logs every user out.",[86,404,405,406,409,410,413,414,417,418,409,421,424,425,428,429,432,433,436,437,252],{},"Multi-arch images (",[98,407,408],{},"linux\u002Famd64",", ",[98,411,412],{},"linux\u002Farm64",") are published to GHCR by CI on pushes to ",[98,415,416],{},"main"," (",[98,419,420],{},"latest",[98,422,423],{},"sha-…"," tags) and on ",[98,426,427],{},"v*"," tags (semver). The package is public — ",[98,430,431],{},"docker pull ghcr.io\u002Fticktimer\u002Ftick:latest"," works without ",[98,434,435],{},"docker login",". The image runs as a non-root user and ships a healthcheck against ",[98,438,439],{},"\u002Flogin",[90,441,443],{"id":442},"native-node-pm2","Native (Node + PM2)",[86,445,446,447,450],{},"Requires ",[103,448,449],{},"Node 22+"," and a reachable Postgres.",[120,452,454],{"className":122,"code":453,"language":124,"meta":125,"style":125},"npm ci\nnpm run build\nexport NUXT_DATABASE_URL='postgresql:\u002F\u002Ftick:password@localhost:5432\u002Ftick'\nexport NUXT_SESSION_PASSWORD=\"$(openssl rand -base64 36)\"   # keep it stable across restarts\nnode .output\u002Fserver\u002Findex.mjs        # or:\npm2 start ecosystem.config.cjs       # see the file for env docs\n",[98,455,456,464,473,493,515,527],{"__ignoreMap":125},[129,457,458,461],{"class":131,"line":132},[129,459,460],{"class":135},"npm",[129,462,463],{"class":139}," ci\n",[129,465,466,468,470],{"class":131,"line":160},[129,467,460],{"class":135},[129,469,327],{"class":139},[129,471,472],{"class":139}," build\n",[129,474,475,479,482,485,487,490],{"class":131,"line":172},[129,476,478],{"class":477},"spNyl","export",[129,480,481],{"class":344}," NUXT_DATABASE_URL",[129,483,484],{"class":149},"=",[129,486,356],{"class":149},[129,488,489],{"class":139},"postgresql:\u002F\u002Ftick:password@localhost:5432\u002Ftick",[129,491,492],{"class":149},"'\n",[129,494,495,497,500,502,504,506,508,510,512],{"class":131,"line":179},[129,496,478],{"class":477},[129,498,499],{"class":344}," NUXT_SESSION_PASSWORD",[129,501,484],{"class":149},[129,503,373],{"class":149},[129,505,376],{"class":135},[129,507,379],{"class":139},[129,509,383],{"class":382},[129,511,386],{"class":149},[129,513,514],{"class":175},"   # keep it stable across restarts\n",[129,516,518,521,524],{"class":131,"line":517},5,[129,519,520],{"class":135},"node",[129,522,523],{"class":139}," .output\u002Fserver\u002Findex.mjs",[129,525,526],{"class":175},"        # or:\n",[129,528,530,533,536,539],{"class":131,"line":529},6,[129,531,532],{"class":135},"pm2",[129,534,535],{"class":139}," start",[129,537,538],{"class":139}," ecosystem.config.cjs",[129,540,541],{"class":175},"       # see the file for env docs\n",[86,543,544,545,548,549,552],{},"PM2 inherits your shell environment; for a persistent setup put the ",[98,546,547],{},"NUXT_*"," variables in your service manager's env file (or a PM2 ecosystem ",[98,550,551],{},"env"," block) rather than exporting them ad hoc.",[90,554,556],{"id":555},"reverse-proxy","Reverse proxy",[86,558,559,560,563],{},"The production server speaks plain HTTP on port 3000. For anything beyond localhost, put a reverse proxy in front of it for TLS, compression and security headers. The repo ships ready-made configs in ",[98,561,562],{},"deploy\u002F",":",[203,565,566,589],{},[206,567,568,573,574,577,578,581,582,584,585,588],{},[103,569,570],{},[98,571,572],{},"deploy\u002Fnginx.conf.example"," — drop into ",[98,575,576],{},"\u002Fetc\u002Fnginx\u002Fconf.d\u002Ftick.conf",". Contains two variants: a public host with TLS (port 80 redirects to 443; uncomment one ",[98,579,580],{},"ssl_certificate"," pair — certbot paths or your internal CA's) and a commented-out plain-HTTP variant for LAN-only boxes — also set ",[98,583,246],{}," if you use it; prefer the TLS variant with an internal-CA cert instead. Run ",[98,586,587],{},"nginx -t"," and reload.",[206,590,591,596,597,600,601,604,605,607,608,610],{},[103,592,593],{},[98,594,595],{},"deploy\u002FCaddyfile.example"," — public hostnames get automatic Let's Encrypt certificates with zero cert config. On an internal box, uncomment ",[98,598,599],{},"tls internal"," (Caddy's built-in CA) or use an ",[98,602,603],{},"http:\u002F\u002F"," site address, which also needs ",[98,606,246],{},"; ",[98,609,599],{}," avoids that.",[86,612,613,614,617],{},"Details and setup commands are in ",[98,615,616],{},"deploy\u002FREADME.md",". Whatever proxy you use, these five things matter:",[619,620,621,634],"table",{},[622,623,624],"thead",{},[625,626,627,631],"tr",{},[628,629,630],"th",{},"Requirement",[628,632,633],{},"Why",[635,636,637,657,672,686,697],"tbody",{},[625,638,639,654],{},[640,641,642,643,646,647,650,651],"td",{},"Forward ",[98,644,645],{},"Host"," and ",[98,648,649],{},"X-Forwarded-Proto"," \u002F ",[98,652,653],{},"X-Forwarded-For",[640,655,656],{},"Tick builds absolute links (invite and password-reset URLs) from the request origin, and session cookies need the right host",[625,658,659,669],{},[640,660,661,664,665,668],{},[98,662,663],{},"client_max_body_size 8m"," (nginx) \u002F ",[98,666,667],{},"request_body max_size 8MB"," (Caddy)",[640,670,671],{},"CSV import posts up to 5 MB of body; the default 1 MB proxy limit rejects it before Tick sees it",[625,673,674,683],{},[640,675,676,677,646,680],{},"No caching on ",[98,678,679],{},"\u002Fsw.js",[98,681,682],{},"\u002Fmanifest.webmanifest",[640,684,685],{},"Otherwise installed PWA clients keep serving a stale app shell after an upgrade",[625,687,688,691],{},[640,689,690],{},"TLS, plus gzip and security headers",[640,692,693,694],{},"The app speaks plain HTTP and leaves HSTS to you — see ",[232,695,71],{"href":696},"\u002Freference\u002Fsecurity#what-you-have-to-do",[625,698,699,710],{},[640,700,701,702,705,706,709],{},"Leave ",[98,703,704],{},"NUXT_SESSION_COOKIE_SECURE"," at ",[98,707,708],{},"true"," behind TLS",[640,711,712,713,716],{},"The browser sees https, so the Secure cookie works, and ",[98,714,715],{},"Secure"," keeps it off the port-80 redirect",[395,718,719],{},[86,720,721],{},"If you enable the (commented) HSTS header, do it only once TLS is known-good — browsers cache it, and a broken certificate then locks users out for the whole max-age.",[279,723,724],{},[86,725,726,727,731,732,252],{},"Behind a proxy, Tick's ",[232,728,730],{"href":729},"\u002Freference\u002Fsecurity#rate-limiting","auth rate limiter"," sees the proxy's address for every request, which makes its 10\u002Fmin window instance-wide rather than per-client. Rate-limit at the proxy instead and set ",[98,733,734],{},"NUXT_AUTH_RATE_LIMIT=0",[90,736,251],{"id":737},"plain-http-on-a-lan",[86,739,740,741,743,744,746,747,749],{},"Opening Tick directly at ",[98,742,242],{},", with no reverse proxy: the session cookie is ",[98,745,715],{}," by default, and browsers refuse to store a ",[98,748,715],{}," cookie over plain HTTP. Login returns success but the browser drops the cookie — the sign-in page reports this, naming the host. In order of preference:",[751,752,753,763,769,774],"ol",{},[206,754,755,760,761,252],{},[103,756,757,758],{},"Caddy with ",[98,759,599],{}," — real HTTPS on a LAN hostname; trust its root certificate once per device. See ",[98,762,595],{},[206,764,765,768],{},[103,766,767],{},"A VPN or overlay network with HTTPS certs"," (WireGuard, Tailscale).",[206,770,771],{},[103,772,773],{},"Your own internal CA with nginx variant A.",[206,775,776,781],{},[103,777,778,779],{},"Set ",[98,780,246],{},", only on a network you fully trust — it also puts session cookies and passwords in cleartext on the wire.",[86,783,784,785,789,790,252],{},"See ",[232,786,788],{"href":787},"\u002Freference\u002Fsecurity#plain-http-and-the-secure-flag","Plain HTTP and the Secure flag"," for what option 4 costs, and what stays protected without ",[98,791,715],{},[90,793,795],{"id":794},"registration-modes","Registration modes",[86,797,798,799,802,803,806],{},"By default ",[103,800,801],{},"anyone who can reach the host can create a workspace",". ",[98,804,805],{},"NUXT_PUBLIC_REGISTRATION"," changes that:",[619,808,809,819],{},[622,810,811],{},[625,812,813,816],{},[628,814,815],{},"Value",[628,817,818],{},"Effect",[635,820,821,836,855],{},[625,822,823,829],{},[640,824,825,828],{},[98,826,827],{},"open"," (default)",[640,830,831,832,835],{},"Self-serve signup at ",[98,833,834],{},"\u002Fregister","; each new account gets its own personal org",[625,837,838,843],{},[640,839,840],{},[98,841,842],{},"invite",[640,844,845,847,848,851,852],{},[98,846,834],{}," only accepts a valid ",[98,849,850],{},"?invite=TOKEN"," link; without one the form is replaced by an explanation, and the API answers ",[98,853,854],{},"403",[625,856,857,862],{},[640,858,859],{},[98,860,861],{},"closed",[640,863,864,865,869],{},"No self-serve signup at all. Existing users still log in; new people must be ",[232,866,868],{"href":867},"\u002Fguide\u002Fteam#invites","invited"," into an org",[120,871,873],{"className":122,"code":872,"language":124,"meta":125,"style":125},"NUXT_PUBLIC_REGISTRATION=invite\n",[98,874,875],{"__ignoreMap":125},[129,876,877,879,881],{"class":131,"line":132},[129,878,805],{"class":344},[129,880,484],{"class":149},[129,882,883],{"class":139},"invite\n",[86,885,886,887,889,890,892],{},"Pick ",[98,888,842],{}," or ",[98,891,861],{}," for anything exposed to the internet. Both still let owners and admins add people through invite links.",[90,894,896],{"id":895},"email-smtp","Email (SMTP)",[86,898,899,900,903],{},"Mail is ",[103,901,902],{},"optional",". Configure it and Tick emails invites and password-reset links; leave it unset and the app keeps working with visible fallbacks.",[120,905,907],{"className":122,"code":906,"language":124,"meta":125,"style":125},"NUXT_SMTP_HOST=smtp.example.com\nNUXT_SMTP_PORT=587\nNUXT_SMTP_USER=tick@example.com\nNUXT_SMTP_PASS=app-password\nNUXT_SMTP_SECURE=false                     # true = implicit TLS, usually port 465\nNUXT_MAIL_FROM=\"Tick \u003Ctick@example.com>\"\n",[98,908,909,919,929,939,949,962],{"__ignoreMap":125},[129,910,911,914,916],{"class":131,"line":132},[129,912,913],{"class":344},"NUXT_SMTP_HOST",[129,915,484],{"class":149},[129,917,918],{"class":139},"smtp.example.com\n",[129,920,921,924,926],{"class":131,"line":160},[129,922,923],{"class":344},"NUXT_SMTP_PORT",[129,925,484],{"class":149},[129,927,928],{"class":139},"587\n",[129,930,931,934,936],{"class":131,"line":172},[129,932,933],{"class":344},"NUXT_SMTP_USER",[129,935,484],{"class":149},[129,937,938],{"class":139},"tick@example.com\n",[129,940,941,944,946],{"class":131,"line":179},[129,942,943],{"class":344},"NUXT_SMTP_PASS",[129,945,484],{"class":149},[129,947,948],{"class":139},"app-password\n",[129,950,951,954,956,959],{"class":131,"line":517},[129,952,953],{"class":344},"NUXT_SMTP_SECURE",[129,955,484],{"class":149},[129,957,958],{"class":139},"false",[129,960,961],{"class":175},"                     # true = implicit TLS, usually port 465\n",[129,963,964,967,969,972,975],{"class":131,"line":529},[129,965,966],{"class":344},"NUXT_MAIL_FROM",[129,968,484],{"class":149},[129,970,971],{"class":149},"\"",[129,973,974],{"class":139},"Tick \u003Ctick@example.com>",[129,976,977],{"class":149},"\"\n",[86,979,980,981,984,985,646,987,989],{},"Mail counts as configured only when ",[103,982,983],{},"both"," ",[98,986,913],{},[98,988,966],{}," are set. Without it:",[203,991,992,1002],{},[206,993,994,997,998,1001],{},[103,995,996],{},"Invites"," still work — the dialog shows the ",[98,999,1000],{},"…\u002Fregister?invite=TOKEN"," link to copy and send yourself.",[206,1003,1004,1007,1008,1011],{},[103,1005,1006],{},"Password resets"," still work — the reset link is written to the server console (",[98,1009,1010],{},"docker compose logs app",") instead of being emailed.",[395,1013,1014],{},[86,1015,1016],{},"That console fallback means anyone who can read your logs can take over an account. Configure SMTP, or treat log access as account access.",[90,1018,1020],{"id":1019},"demo-mode","Demo mode",[86,1022,1023,1024,1027,1028,1030,1031,1034],{},"For a public playground instance, ",[98,1025,1026],{},"NUXT_DEMO_MODE=true"," blocks the destructive operations (password change, org rename, member removal, invite create\u002Frevoke, import commit, permanent trash deletion) with a ",[98,1029,854],{},", and ",[98,1032,1033],{},"NUXT_PUBLIC_DEMO_MODE=true"," shows a dismissible banner with the demo credentials.",[395,1036,1037],{},[86,1038,1039,1040,1043,1044,1047],{},"Demo mode ",[103,1041,1042],{},"wipes and reseeds the database's demo org",". On boot, and then hourly whenever the last reset is over 24 hours old, it deletes the \"Hollow Studio\" org and the ",[98,1045,1046],{},"mara@example.com"," user — cascading away their clients, projects, tasks, tags and entries — and reseeds three weeks of fake history. Never enable it on an instance that holds real data.",[90,1049,1051],{"id":1050},"automatic-migrations","Automatic migrations",[86,1053,1054,1055,1058],{},"The built server applies pending SQL migrations from ",[98,1056,1057],{},"server\u002Fdb\u002Fmigrations"," on startup — a Nitro plugin runs them before the first request, in Docker and native installs alike. That means upgrades are just \"pull new version, restart\".",[86,1060,1061,1062,1065,1066,1069],{},"While they run, every request is answered ",[103,1063,1064],{},"503 Starting up"," with a ",[98,1067,1068],{},"Retry-After"," header, the healthcheck included — a container that is still migrating is not yet healthy. Requests are served normally as soon as the migrations finish, which is typically well inside the healthcheck's start period.",[86,1071,1072,1073,1076,1077,1080,1081,1083],{},"If a migration ",[103,1074,1075],{},"fails",", the server logs the error and exits with status 1 rather than serving traffic against a half-migrated schema. Under the shipped Compose file (",[98,1078,1079],{},"restart: unless-stopped",") the container then restarts and retries; if the migration is genuinely broken it will keep restarting, which is the visible, loud failure you want. Check ",[98,1082,1010],{}," for the SQL error, fix or roll back the migration, and restart.",[86,1085,1086,1087,1090,1091,1094],{},"To run migrations out-of-band instead (e.g. in a CI step or with a locked-down DB user), set ",[98,1088,1089],{},"NUXT_AUTO_MIGRATE=false"," and apply them yourself with ",[98,1092,1093],{},"npx drizzle-kit migrate",". The server then starts immediately and assumes the schema is current.",[90,1096,66],{"id":1097},"environment-variables",[86,1099,1100],{},"Two are required to boot; the rest are opt-in:",[619,1102,1103,1119],{},[622,1104,1105],{},[625,1106,1107,1110,1113,1116],{},[628,1108,1109],{},"Variable",[628,1111,1112],{},"Required",[628,1114,1115],{},"Default",[628,1117,1118],{},"Description",[635,1120,1121,1139,1156,1182,1203,1223,1239],{},[625,1122,1123,1128,1131,1134],{},[640,1124,1125],{},[98,1126,1127],{},"NUXT_DATABASE_URL",[640,1129,1130],{},"yes",[640,1132,1133],{},"—",[640,1135,1136,1137],{},"Postgres connection string, e.g. ",[98,1138,489],{},[625,1140,1141,1145,1147,1149],{},[640,1142,1143],{},[98,1144,117],{},[640,1146,1130],{},[640,1148,1133],{},[640,1150,1151,1152,1155],{},"Session cookie encryption secret, 32+ chars. Generate: ",[98,1153,1154],{},"openssl rand -base64 36",". Changing it logs everyone out",[625,1157,1158,1162,1165,1169],{},[640,1159,1160],{},[98,1161,805],{},[640,1163,1164],{},"no",[640,1166,1167],{},[98,1168,827],{},[640,1170,1171,650,1173,650,1175,1177,1178],{},[98,1172,827],{},[98,1174,842],{},[98,1176,861],{}," — see ",[232,1179,1181],{"href":1180},"#registration-modes","registration modes",[625,1183,1184,1189,1191,1196],{},[640,1185,1186],{},[98,1187,1188],{},"NUXT_AUTH_RATE_LIMIT",[640,1190,1164],{},[640,1192,1193],{},[98,1194,1195],{},"10",[640,1197,1198,1199,1202],{},"Auth POSTs per IP per minute; ",[98,1200,1201],{},"0"," defers to your proxy",[625,1204,1205,1209,1211,1215],{},[640,1206,1207],{},[98,1208,704],{},[640,1210,1164],{},[640,1212,1213],{},[98,1214,708],{},[640,1216,1217,1218,1220,1221],{},"Secure flag on the session cookie; ",[98,1219,958],{}," only for plain-HTTP LAN access — see ",[232,1222,251],{"href":250},[625,1224,1225,1230,1232,1236],{},[640,1226,1227],{},[98,1228,1229],{},"NUXT_AUTO_MIGRATE",[640,1231,1164],{},[640,1233,1234],{},[98,1235,708],{},[640,1237,1238],{},"Apply pending SQL migrations on production startup",[625,1240,1241,1249,1251,1258],{},[640,1242,1243,650,1246],{},[98,1244,1245],{},"NITRO_HOST",[98,1247,1248],{},"NITRO_PORT",[640,1250,1164],{},[640,1252,1253,650,1255],{},[98,1254,262],{},[98,1256,1257],{},"3000",[640,1259,1260],{},"Bind address and port of the production server",[86,1262,283,1263,1266],{},[232,1264,1265],{"href":67},"Environment variables reference"," has the complete table — SMTP, mail sender, migrations directory, demo mode and the docker-compose-only variables — plus the npm scripts.",[90,1268,1270],{"id":1269},"development-setup","Development setup",[120,1272,1274],{"className":122,"code":1273,"language":124,"meta":125,"style":125},"npm i\ncp .env.example .env      # point NUXT_DATABASE_URL at a local Postgres\nnpm run db:push           # create the schema (dev uses push, not migrations)\nnpm run db:seed           # demo org + ~3 weeks of entries\nnpm run dev\n",[98,1275,1276,1283,1295,1307,1319],{"__ignoreMap":125},[129,1277,1278,1280],{"class":131,"line":132},[129,1279,460],{"class":135},[129,1281,1282],{"class":139}," i\n",[129,1284,1285,1287,1289,1292],{"class":131,"line":160},[129,1286,163],{"class":135},[129,1288,166],{"class":139},[129,1290,1291],{"class":139}," .env",[129,1293,1294],{"class":175},"      # point NUXT_DATABASE_URL at a local Postgres\n",[129,1296,1297,1299,1301,1304],{"class":131,"line":172},[129,1298,460],{"class":135},[129,1300,327],{"class":139},[129,1302,1303],{"class":139}," db:push",[129,1305,1306],{"class":175},"           # create the schema (dev uses push, not migrations)\n",[129,1308,1309,1311,1313,1316],{"class":131,"line":179},[129,1310,460],{"class":135},[129,1312,327],{"class":139},[129,1314,1315],{"class":139}," db:seed",[129,1317,1318],{"class":175},"           # demo org + ~3 weeks of entries\n",[129,1320,1321,1323,1325],{"class":131,"line":517},[129,1322,460],{"class":135},[129,1324,327],{"class":139},[129,1326,1327],{"class":139}," dev\n",[86,1329,1330,1331,650,1336,1339,1340,409,1343,1346,1347,1349],{},"Seed credentials: ",[103,1332,1333],{},[232,1334,1046],{"href":1335},"mailto:mara@example.com",[103,1337,1338],{},"tick-demo"," (org \"Hollow Studio\"). Other scripts: ",[98,1341,1342],{},"npm run typecheck",[98,1344,1345],{},"npm run db:generate"," (writes a new SQL migration from schema changes — commit the ",[98,1348,1057],{}," output so deployed instances pick it up).",[1351,1352,1353],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}",{"title":125,"searchDepth":160,"depth":160,"links":1355},[1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366],{"id":92,"depth":160,"text":93},{"id":311,"depth":160,"text":312},{"id":442,"depth":160,"text":443},{"id":555,"depth":160,"text":556},{"id":737,"depth":160,"text":251},{"id":794,"depth":160,"text":795},{"id":895,"depth":160,"text":896},{"id":1019,"depth":160,"text":1020},{"id":1050,"depth":160,"text":1051},{"id":1097,"depth":160,"text":66},{"id":1269,"depth":160,"text":1270},"Run Tick with Docker Compose, plain docker run, or natively with Node and PM2.","md",{},{"icon":13},{"title":10,"description":1367},"g3Q-eOmNRiN0Cc3LFLqsirsT9eEq-P7H98PLhW6rCFk",[1374,1376],{"title":5,"path":6,"stem":7,"description":1375,"icon":8,"children":-1},"What Tick is, what it does, and how to get a copy running in two minutes.",{"title":20,"path":21,"stem":22,"description":1377,"icon":23,"children":-1},"The timer bar, several timers at once, the picker, manual entries with free-text dates, editing and bulk actions.",1790777311702]