Focus Trail is a Pomodoro focus timer with an animated, retro pixel-art scene: while the timer runs, a character walks across a scrolling background and terrain. When you pause, the scene pauses with it.
- Pomodoro timer with a walking character and parallax background/terrain that animate only while running.
- Data-driven scenarios (
frontend/src/assets/scenarios.json): character sprite sheets are auto-sliced into frames at runtime, and each scene exposes per-scenario tuning (terrain height, character baseline, camera/terrain/sprite speed). - Scenario picker in the app menu (locked while a session is running).
- Email/password auth with cookie-based JWT sessions and server-side session revocation.
- Session history per user.
- Shareable focus cards (day or task) rendered client-side in canvas from the active scenario, for Instagram, TikTok, LinkedIn and X (see Sharing).
- Backend startup check that fails fast with an actionable message when the database is unreachable or unmigrated.
Frontend
- React 18 + TypeScript, built with Vite 6
- Tailwind CSS 4, Radix UI primitives
- React Router 6
Backend
- Fastify 5 + TypeScript (run with
tsx) - Prisma 6 ORM + PostgreSQL 16
@fastify/jwt(cookie sessions),@node-rs/argon2(password hashing),zod(validation)@fastify/swagger(OpenAPI docs),@fastify/rate-limit,@fastify/cors,@fastify/cookie- Vitest, ESLint, Prettier
Tooling
- Node 24.20.0 (
.nvmrc) - Conventional Commits enforced by commitlint + Husky
- Changelog and releases via changelogen
focus-trail/
├── backend/ Fastify + Prisma API (auth, sessions, /me, health, Swagger)
│ ├── prisma/ schema + migrations
│ ├── src/ controllers, services, repositories, routes, schemas, lib
│ ├── bruno/ HTTP request collection (Bruno)
│ └── docker-compose.yml
├── frontend/ Vite + React app
│ └── src/assets/ scenario pools (background/, character/, terrain/) + scenarios.json
├── CHANGELOG.md
└── package.json root tooling (Husky, commitlint, changelogen)
- Node
24.20.0(nvm recommended) - Docker and Docker Compose (for PostgreSQL) — or a local PostgreSQL 16
- npm
Clone the repository and select the Node version:
git clone git@github.com:isadfrn/focus-trail.git
cd focus-trail
nvm use
npm installThe root npm install sets up the shared tooling (Husky hooks, commitlint, changelogen). The frontend and backend have their own dependencies.
cd backend
npm install
cp .env.example .env
docker compose up -d
npm run prisma:migrate
npm run dev
JWT_SECRETmust be at least 32 characters and not a known placeholder. Generate one withopenssl rand -base64 48. WithNODE_ENV=development(orENABLE_SWAGGER=true), Swagger UI is served athttp://localhost:3001/docs. To run the API in Docker too, use thefullprofile:docker compose --profile full up -d.
cd frontend
npm install
npm run devThe dev server proxies /api to http://localhost:3001, so start the backend first.
| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://focus:pass@127.0.0.1:5432/focustrail?schema=public |
PORT |
API port | 3001 |
HOST |
Bind host | 127.0.0.1 |
NODE_ENV |
development | production | test |
development |
JWT_SECRET |
Session signing secret (≥ 32 chars, no placeholder) | output of openssl rand -base64 48 |
CORS_ORIGIN |
Allowed frontend origin | http://localhost:5173 |
SESSION_MAX_AGE |
Session lifetime in seconds | 604800 |
ENABLE_SWAGGER |
Serve Swagger UI outside development | false |
POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB are consumed by docker-compose.yml to provision the database.
Base path: /api
| Method | Route | Auth | Description |
|---|---|---|---|
GET |
/health |
public | Health check |
POST |
/auth/register |
public | Create an account |
POST |
/auth/login |
public | Sign in (sets session cookie) |
POST |
/auth/logout |
session | Sign out (revokes the session) |
GET |
/me |
session | Current user |
POST |
/sessions |
session | Record a Pomodoro session |
GET |
/sessions |
session | List the user's sessions |
GET |
/sessions/stats |
session | Daily focus/break statistics |
GET |
/sessions/tasks |
session | Focus totals grouped by task |
An HTTP request collection is available in backend/bruno.
Every scene is one entry in frontend/src/assets/scenarios.json. Assets live in shared pools under frontend/src/assets/{background,character,terrain}/.
To add a scenario: drop the images in the pools and add a block referencing them by filename.
{
"id": "diddy",
"name": "Diddy Kong",
"skyColor": "#123a2a",
"background": "forest-background.png",
"terrain": "forest-ground.png",
"sheet": "diddy.png"
}The character sheet is a single sprite sheet whose frames are detected automatically (one frame = idle, two or more = walk animation). Optional per-scenario knobs: frames, height, terrainHeight, baseline, cameraSpeed, terrainSpeed, spriteSpeed.
The share dialog (Timer after a focus session, Performance, History rows) renders a PNG card in the browser — nothing is uploaded. Two formats: Story/TikTok 1080×1920 and Post 1080×1080.
- The card is drawn from the same scenario data as the timer scene (
scenarios.json+ the asset pools), so adding, removing or replacing scenarios and images updates the cards with no extra work. The layout adapts to each scenario's character height and palette. - The link printed on the card and added to the caption comes from
VITE_SHARE_URL(build-time, defaulthttps://isabellanunes.dev/focus/). Point it to a landing page later — no code change needed (frontend/Dockerfileexposes it as a build arg). - Instagram and TikTok have no web share link: on mobile the native share sheet receives the image; on desktop download the PNG. X and LinkedIn open their composers and the image is copied to the clipboard for pasting.
Backend (backend/)
| Script | Purpose |
|---|---|
npm run dev |
API in watch mode |
npm start |
run the API once |
npm run typecheck |
type-check |
npm test |
unit tests (Vitest) |
npm run test:coverage |
tests with coverage |
npm run lint |
ESLint |
npm run prisma:migrate |
create/apply migrations (dev) |
npm run prisma:studio |
open Prisma Studio |
Frontend (frontend/)
| Script | Purpose |
|---|---|
npm run dev |
Vite dev server |
npm run build |
type-check + production build |
npm run preview |
preview the build |
npm run typecheck |
type-check |
Root
| Script | Purpose |
|---|---|
npm run changelog |
regenerate CHANGELOG.md from commits |
npm run release |
bump version, update changelog, commit and tag |
npm run release:minor / release:major |
force the bump type |
Commits follow Conventional Commits (validated by commitlint). Releases and the changelog are generated from those commits with npm run release.
