Full documentation for reviewers and contributors: overview, high-level design (HLD), low-level design (LLD), how to run, migrations, test login, API reference, and future work.
Table of contents
- Overview
- High-level design (HLD)
- Low-level design (LLD)
- Architecture decisions and tradeoffs
- Running locally
- Running migrations
- Test credentials
- API reference
- What you would do with more time
What it is. TaskFlow is a full-stack task manager. People register or sign in, own projects, add tasks to those projects, assign tasks to users, set priority and due dates, and move work through todo → in_progress → done.
What it does in practice. The browser loads a React SPA. All REST calls go to /api/... on the same host (port 3000 in Docker). The Go service validates JWTs, enforces who may see or change which project/task, persists to PostgreSQL, and returns JSON. Demo data can be loaded once from CSV so reviewers can log in without registering.
Tech stack.
| Area | Technology |
|---|---|
| API | Go 1.22, Chi router, jackc/pgx, golang-migrate, golang-jwt, bcrypt (cost 12), slog (JSON to stdout) |
| Database | PostgreSQL 16 |
| Web UI | React 19, TypeScript, Vite, React Router 7, Tailwind CSS v4, Radix UI primitives |
| Local deployment | Docker Compose: postgres, backend (API), web (nginx: static files + reverse proxy) |
Important URL shape. Every JSON route is under /api (for example POST /api/auth/login). GET /healthz is on the API root (not under /api) and is also exposed through nginx at http://localhost:3000/healthz.
- Actors: End user (browser), reviewer (curl/Postman), optional local developer running Go or Vite on the host.
- External systems: PostgreSQL (data store). No third-party auth or email in this version.
| Container | Role |
|---|---|
| postgres | Holds all relational data. Not published on the host in the default Compose file; only reachable on the internal Docker network. |
| backend | Go HTTP server on port 4000 inside the network. Loads YAML from /app/config, secrets from env (DATABASE_URL, JWT_SECRET, CORS_ALLOWED_ORIGINS). Runs SQL migrations on startup, then CSV seed if needed, then serves Chi routes. |
| web | nginx serves the built SPA from /usr/share/nginx/html and proxies /api and /healthz to backend:4000. Only this service publishes a host port (default 3000). |
So from the reviewer’s laptop: one origin (http://localhost:3000) for both UI and API; no CORS configuration is required for that path as long as CORS_ALLOWED_ORIGINS includes that origin (the sample .env.example does).
- Browser requests
https://localhost:3000/project(or any SPA route). nginx servesindex.html(SPA shell). - Browser calls
POST http://localhost:3000/api/auth/loginwith JSON body. nginx forwards to the Go server with path/api/auth/loginpreserved. - API validates credentials, creates a session row, returns a JWT whose
jticlaim equals that session’s UUID. - Later requests send
Authorization: Bearer <jwt>. Middleware verifies signature and expiry, parsesuser_id, then checks the session row still exists (so logout invalidates the token immediately even before expiry).
- User — identity, bcrypt password hash, unique email.
- Project — owned by one user; deleting a project deletes its tasks (CASCADE).
- Task — belongs to one project; has status, priority, optional assignee and due date, and a creator (used for delete permission together with project ownership).
- Project owner — full CRUD on the project and its tasks.
- Anyone with project access (owner, or assignee/creator on any task in the project) — can PATCH any task in that project; delete task is only for project owner or that task’s creator. Task creator is set at creation and is not changed via the API.
This is enforced in the service layer, not only in HTTP handlers.
Strict order: Handler → Service → Repository → SQL. A shared internal/model package holds domain structs used everywhere.
| Layer | Responsibility | Typical packages |
|---|---|---|
| Handler | HTTP only: read JSON/query, call service, map model → JSON DTOs, map errors → status codes. Must not import repository. |
internal/handler |
| Service | Business rules, authorization, orchestration. Depends on interfaces defined in internal/service/service_repos.go, not concrete repos. Must not import encoding/json or pgx. |
internal/service |
| Repository | Parameterized SQL via pgx; scan into model types; translate pgx.ErrNoRows and constraint violations into small domain errors (errs package). |
internal/repository |
| Model | Plain structs and enums (e.g. TaskPatch for partial updates). |
internal/model |
Composition root: cmd/server/main.go loads config, connects the DB, runs migrations, runs seed, constructs concrete repositories and services, passes them into handlers, starts Chi with graceful shutdown on SIGINT/SIGTERM.
internal/config— Loadsapplication-{APP_ENV}.yml(defaultdevon host,prdin Compose), overlays env vars (DATABASE_URL,JWT_SECRET,HTTP_PORT,CORS_ALLOWED_ORIGINS,SEED_CSV_DIR, etc.). Repo root.envis loaded first, then optionalbackend/.env.internal/db— Builds pgx pool from config; runs embedded migrations frommigrations/*.sqlviamigrations/embed.go.internal/auth—HashPassword/CheckPassword(bcrypt cost 12);SignToken/ParseToken(JWT withuser_id,email,jti, expiry aligned with session row).internal/middleware—RequireAuth: Bearer parse → JWT verify → session existence check → setuser_idand session id in context. Structured request logging and trace IDs.internal/handler/handler_errors.go— Mapserrs.ErrNotFound→ 404,ErrForbidden→ 403, invalid credentials / unauthorized → 401, validation maps → 400 withfieldsobject.internal/httpx—WriteJSON,WriteValidation, boundedReadJSON.internal/server/routes.go— Registers/healthzand/api/...routes; wires CORS from config.
Defined in versioned SQL under backend/migrations/:
users—id,name,email(unique),password_hash,created_at.projects—id,name,description,owner_id→users,created_at.tasks—id,title,description,status(enum),priority(enum),project_id,assignee_id(nullable),creator_id,due_date,created_at,updated_at. Indexes on foreign keys used in filters.app_seed_state— Tracks whether static CSV demo seed has run (key= marker string).user_sessions— One row per issued JWT session;idmatches JWTjti;expires_atmatches token expiry; deleted on logout.
App.tsx— Routes; protected routes wrapped in a guard that requires auth context.contexts/AuthProvider.tsx— Holds current user + JWT; persists token (and user snapshot) inlocalStorage; globalfetchwrapper clears session on401when a token was sent.api/client.ts— All REST paths are relative to/api. Central place forApiErrorand field-level server validation.pages/— Login, register, project list, project detail (tasks).components/— Task side panel, modals, layout, markdown description editor, etc.;components/ui/— Button, Input, Label (Radix + CVA).
State for auth is React Context only (no Redux/Zustand).
- Non-secret tuning — Pool sizes, timeouts, log level: YAML under
backend/config/application-*.yml. - Secrets —
JWT_SECRET, database password, etc.: environment only (.envfor local/Compose). YAMLjwt_secretis empty in repo and must be overridden by env in production-style runs.
- Why layers? Keeps SQL and HTTP concerns out of business rules; tests can mock repositories; handlers stay thin.
- Why JWT + DB session? JWT carries identity without a DB round-trip for parsing, but session row gives revocable logout and a single place to invalidate tokens.
- Why
/apiprefix? Avoids colliding with SPA routes like/projectand/loginwhen everything is served from one host. Tradeoff: spec examples that omit/apineed a mental prefix when comparing. - Why embed migrations? One binary + config dir is enough to run against an empty database; no separate migrate CLI in production path.
- Why CSV seed? Predictable demo for reviewers without a separate “bootstrap” script; marker table prevents duplicate users on restart.
- What was left out or kept small: No OpenAPI/Bruno artifact in-repo; no rate limiting; integration tests are optional and tag-gated; frontend bundle is not code-split by route.
Assume Docker Desktop (or compatible engine) is installed and running. Go and Node are not required for the default path.
git clone https://github.com/YOUR_GITHUB_USERNAME/taskflow-Sweta.git
cd taskflow-Sweta
cp .env.example .env
docker compose up--buildensures images exist on first clone (images are not pushed to a registry in this workflow).- Open http://localhost:3000 in a browser.
Compose requires POSTGRES_PASSWORD, JWT_SECRET, and CORS_ALLOWED_ORIGINS in .env (the sample .env.example supplies working local values).
Optional — run API on the host: Install Go 1.22+, run PostgreSQL with a reachable DATABASE_URL, set JWT_SECRET in backend/.env (and load repo root .env if you use it), then:
cd backend && go run ./cmd/serverOptional — Vite dev server: cd frontend && npm install && npm run dev → http://localhost:5173; Vite proxies /api to http://127.0.0.1:4000.
Migrations run automatically when the Go API starts: golang-migrate applies embedded SQL from backend/migrations/ before the pool serves traffic. No separate migrate command is required for the Docker Compose stack.
After a fresh database (e.g. first docker compose up on a new volume), CSV seed creates:
| Field | Value |
|---|---|
test@example.com |
|
| Password | password123 |
You should see one project (Demo Project) and three tasks with statuses todo, in_progress, and done.
If an older seed already ran, run docker compose down -v then docker compose up --build, or manually clear application tables and delete the static_csv_demo_v1 row from app_seed_state, then restart the API.
Base URL (browser / nginx): http://localhost:3000/api
Direct API (host-only dev): http://localhost:4000/api
Auth: Protected routes expect Authorization: Bearer <jwt>. Login and register responses include token and user.
Common errors
400—{"error":"validation failed","fields":{"email":"is required"}}401—{"error":"unauthorized"}403—{"error":"forbidden"}404—{"error":"not found"}
| Method | Path | Auth | Response |
|---|---|---|---|
| GET | /healthz |
No | {"ok":true} |
| Method | Path | Body | Success |
|---|---|---|---|
| POST | /api/auth/register |
{"name","email","password"} (password ≥ 8 chars) |
201 — {"token","user"} |
| POST | /api/auth/login |
{"email","password"} |
200 — {"token","user"} |
| GET | /api/auth/me |
— | 200 — {"user"} |
| POST | /api/auth/logout |
— | 204 |
Example
curl -s -X POST http://localhost:3000/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"test@example.com","password":"password123"}'| Method | Path | Auth | Success |
|---|---|---|---|
| GET | /api/users |
Bearer | 200 — {"users":[...]} (for assignee picker) |
| Method | Path | Query | Success |
|---|---|---|---|
| GET | /api/projects |
?page=&limit= (default limit 20, max 100) |
200 — projects, total, page |
| POST | /api/projects |
JSON body | 201 — project |
| GET | /api/projects/{id} |
— | 200 — project + tasks |
| PATCH | /api/projects/{id} |
name, description, owner_id (optional) |
200 |
| DELETE | /api/projects/{id} |
— | 204 |
| GET | /api/projects/{id}/collaborators |
— | 200 — {"users"} |
| GET | /api/projects/{id}/stats |
— | 200 — by_status, by_assignee |
Create project
POST /api/projects
Content-Type: application/json
{"name":"My project","description":"optional"}Project object (shape)
{
"id": "uuid",
"name": "string",
"description": "string or omitted",
"owner_id": "uuid",
"created_at": "RFC3339Nano"
}| Method | Path | Query / body | Success |
|---|---|---|---|
| GET | /api/projects/{id}/tasks |
?status=, ?assignee=, ?page=&limit= |
200 — tasks, total |
| POST | /api/projects/{id}/tasks |
JSON body | 201 — task |
| PATCH | /api/tasks/{id} |
partial JSON | 200 — task |
| DELETE | /api/tasks/{id} |
— | 204 |
Create task (body)
{
"title": "required",
"description": "optional",
"status": "todo | in_progress | done",
"priority": "low | medium | high",
"assignee_id": "uuid or null",
"due_date": "YYYY-MM-DD"
}due_date, if present, must be today or later (calendar date, UTC).
Task object (shape)
{
"id": "uuid",
"title": "string",
"description": "string or omitted",
"status": "todo|in_progress|done",
"priority": "low|medium|high",
"project_id": "uuid (may be omitted when nested under project detail)",
"assignee_id": "uuid or null",
"creator_id": "uuid",
"due_date": "YYYY-MM-DD or null",
"created_at": "RFC3339",
"updated_at": "RFC3339"
}PATCH notes: Send only fields that change. For assignee_id, JSON null or "" clears the assignee; omit the key to leave it unchanged. creator_id is read-only in responses and cannot be set on PATCH.
There is no Postman or Bruno collection checked into this repository; copy the examples above into your HTTP client if needed.
- End-to-end tests (Playwright or Cypress) against
docker compose upfor login → project → task flows. - OpenAPI document and a small Bruno or Postman export committed next to the code.
- Production hardening: rate limits on login, structured audit log for mutations, stricter default log level, optional switch to disable CSV seed.
- Realtime updates for task boards (SSE or WebSocket) instead of manual refresh.
- Accessibility and i18n pass on the SPA (focus management in dialogs, reduced motion, translations).
- Frontend performance: route-level code splitting; the production JS bundle is large today.