A personal task management and focus app with PWA support for iPhone. Built with Next.js, NestJS, and PostgreSQL — fully self-hosted via Docker with HTTPS served through Tailscale.
- Task management — create, edit, delete tasks with priority levels, due dates, reminder times, categories, and subtasks
- Focus Sessions — Pomodoro-style timer with 10 tick sound options and adjustable duration (1–90 min)
- Dark / Light theme — toggleable from Settings, persists across sessions with no flash on load
- Accent color picker — 6 colors (Focus Blue, Violet, Teal, Emerald, Rose, Amber) applied system-wide
- User profile — name and email stored in PostgreSQL, survives app restarts
- PWA — installable on iPhone via Safari, service worker caches shell assets
- Daily cleanup — cron job deletes completed tasks from previous days at midnight; shows warning for overdue uncompleted tasks on load
- HTTPS via Tailscale — served over a valid TLS certificate, reachable from any network the device is on
| Layer | Technology |
|---|---|
| Frontend | Next.js 14 (App Router), React 18, Mantine UI v7, TypeScript |
| Backend | NestJS 10, TypeORM, Passport JWT, @nestjs/schedule |
| Database | PostgreSQL 16 |
| Reverse proxy | nginx (TLS termination, HTTP→HTTPS redirect) |
| Containers | Podman / Docker Compose |
| PWA | Service Worker, Web App Manifest, viewport-fit=cover |
| HTTPS | Tailscale HTTPS certificates |
focuson/
├── backend/ # NestJS REST API (port 3001)
│ └── src/
│ ├── auth/ # JWT authentication
│ ├── tasks/ # Task CRUD + overdue query
│ ├── subtasks/ # Subtask management
│ ├── categories/ # Task categories
│ ├── settings/ # Theme, accent color, notification prefs
│ ├── profile/ # User profile (name, email)
│ └── scheduler/ # Daily midnight cleanup cron
├── frontend/ # Next.js app (port 3000)
│ └── src/
│ ├── app/
│ │ ├── login/
│ │ ├── tasks/ # Task list + focus session
│ │ └── settings/ # Profile, theme, accent, categories
│ ├── components/
│ │ ├── AppLayout.tsx # Mobile bottom nav + desktop sidebar
│ │ ├── TaskCard.tsx
│ │ ├── FocusSessionModal.tsx
│ │ ├── ThemeProvider.tsx # Dark/light + accent CSS vars
│ │ └── ServiceWorkerRegistrar.tsx
│ └── lib/
│ ├── api.ts # Axios API client
│ └── auth.ts # localStorage auth helpers
├── nginx/
│ ├── nginx.conf # Reverse proxy + TLS config
│ └── certs/ # Tailscale TLS certificate + key
└── docker-compose.yml
- Podman or Docker with Compose plugin
Edit docker-compose.yml. Under frontend.build.args, set your server address:
args:
NEXT_PUBLIC_API_URL: http://<YOUR-LAN-IP>:3001/apiFor HTTPS (required for PWA service workers), see the Tailscale section below.
podman compose up --build -d| Field | Value |
|---|---|
| Username | alphatrix |
| Password | alphatrix$ |
Service workers require HTTPS. Tailscale provides free, automatic certificates for your machine.
# 1. Install Tailscale and connect your machine + iPhone to the same tailnet
# 2. Issue a certificate
tailscale cert your-machine-name.your-tailnet.ts.net
# 3. Copy certs into the project
mkdir -p nginx/certs
cp your-machine-name.*.crt nginx/certs/
cp your-machine-name.*.key nginx/certs/
# 4. Update nginx/nginx.conf — replace the server_name and cert filenames
# 5. Update docker-compose.yml frontend build arg:
# NEXT_PUBLIC_API_URL: https://your-machine-name.your-tailnet.ts.net/api
# 6. Rebuild
podman compose up --build -dCert renewal: Tailscale certs expire every 90 days. Re-run
tailscale cert ..., copy the new files, thenpodman compose restart nginx.
- Open Safari on your iPhone (Chrome cannot install PWAs on iOS)
- Go to
https://your-tailscale-domain - Tap Share → Add to Home Screen → Add
The app launches fullscreen with proper notch and home-indicator handling.
All endpoints require Authorization: Bearer <token> except /api/auth/login.
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/login |
Authenticate, returns JWT |
GET |
/api/profile |
Get user profile |
PUT |
/api/profile |
Update name / email |
GET |
/api/tasks |
List all tasks |
POST |
/api/tasks |
Create task |
GET |
/api/tasks/overdue |
Get uncompleted past-due tasks |
GET |
/api/tasks/:id |
Get task by ID |
PUT |
/api/tasks/:id |
Update task |
PATCH |
/api/tasks/:id/complete |
Toggle completion |
DELETE |
/api/tasks/:id |
Delete task |
POST |
/api/tasks/:taskId/subtasks |
Add subtask |
PATCH |
/api/subtasks/:id |
Update subtask |
DELETE |
/api/subtasks/:id |
Delete subtask |
GET |
/api/categories |
List categories |
POST |
/api/categories |
Create category |
PUT |
/api/categories/:id |
Update category |
DELETE |
/api/categories/:id |
Delete category |
GET |
/api/settings |
Get user settings |
PUT |
/api/settings |
Update settings |
PostgreSQL 16, running in Docker with a persistent named volume (postgres_data).
Host: localhost
Port: 5432
Database: curator
Username: postgres
Password: postgres
Connect:
# From host
psql -h localhost -U postgres -d curator
# Inside container
podman exec -it curator-postgres psql -U postgres -d curatorTables created automatically by TypeORM on first start:
| Table | Description |
|---|---|
tasks |
Tasks with priority, due date, reminder |
subtasks |
Subtasks linked to tasks |
task_categories |
Join table |
categories |
Categories with color |
user_settings |
Theme, accent color, notification prefs |
user_profile |
User name and email |
| Variable | Default | Description |
|---|---|---|
DB_HOST |
postgres |
PostgreSQL hostname |
DB_PORT |
5432 |
PostgreSQL port |
DB_USERNAME |
postgres |
DB user |
DB_PASSWORD |
postgres |
DB password |
DB_DATABASE |
curator |
DB name |
JWT_SECRET |
(change this) | JWT signing secret |
PORT |
3001 |
API server port |
| Variable | Description |
|---|---|
NEXT_PUBLIC_API_URL |
Full URL to the backend API, e.g. https://host/api |
cd backend
npm install
npm run start:dev # runs on http://localhost:3001cd frontend
npm install
NEXT_PUBLIC_API_URL=http://localhost:3001/api npm run dev # runs on http://localhost:3000

