- Add max_length constraints to all string fields in request schemas, matching DB column limits (title:255, description:5000, etc.) - Add min_length=1 to required name/title fields - Add ConfigDict(extra="forbid") to all request schemas to reject unknown fields (prevents silent field injection) - Add Path(ge=1, le=2147483647) to all integer path parameters across all routers to prevent integer overflow → 500 errors - Add max_length to TOTP inline schemas (code:6, mfa_token:256, etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UMBRA
A self-hosted personal life administration app with a dark-themed UI. Manage your todos, calendar events, projects, reminders, contacts, and locations from a single dashboard.
Features
- Dashboard - Contextual greeting, week timeline, stat cards, upcoming events, weather widget, day briefing
- Todos - Task management with priorities, due dates, recurrence, and grouped sections (overdue/today/upcoming)
- Calendar - Multi-calendar system with month/week/day views, recurring events, drag-and-drop, event templates
- Projects - Project boards with kanban view, nested tasks/subtasks, comments, progress tracking
- Reminders - Time-based reminders with snooze, dismiss, recurrence, and real-time alert notifications (dashboard banner + toasts)
- People - Contact directory with avatar initials, favourites, birthday tracking, category filtering
- Locations - Location management with OSM search integration, category filtering, frequent locations
- Weather - Dashboard weather widget with temperature, conditions, and contextual rain warnings
- Settings - Accent color picker (5 presets), first day of week, weather city, ntfy push notifications, TOTP two-factor auth, auto-lock, password management
- Notifications - ntfy push notifications for reminders (configurable per-user)
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite 6, Tailwind CSS 3 |
| UI Components | Custom shadcn/ui-style components, FullCalendar 6, Lucide icons, Sonner toasts |
| Fonts | Sora (headings), DM Sans (body) via Google Fonts |
| State | TanStack Query v5, React Router v6 |
| Backend | FastAPI, Python 3.12, Pydantic v2 |
| Database | PostgreSQL 16, SQLAlchemy 2.0 (async), Alembic (25 migrations) |
| Auth | Username/password with Argon2id hashing, DB-backed sessions (signed cookies), optional TOTP MFA |
| Scheduler | APScheduler (async) for ntfy notification dispatch |
| Deployment | Docker Compose (3 services), Nginx reverse proxy |
Quick Start
Prerequisites
- Docker and Docker Compose
Setup
-
Clone the repository
git clone https://your-gitea-instance/youruser/umbra.git cd umbra -
Configure environment variables
cp .env.example .envEdit
.envand set secure values (see Production Hardening below for generation commands):POSTGRES_USER=umbra POSTGRES_PASSWORD=your-secure-password POSTGRES_DB=umbra DATABASE_URL=postgresql+asyncpg://umbra:your-secure-password@db:5432/umbra SECRET_KEY=your-random-secret-key OPENWEATHERMAP_API_KEY=your-openweathermap-api-keyWeather widget: The dashboard weather widget requires a free OpenWeatherMap API key. Set
OPENWEATHERMAP_API_KEYin.env, then configure your city in Settings. -
Build and run
docker-compose up --build -
Open the app
Navigate to
http://localhostin your browser. On first launch you'll be prompted to create a username and password.
Architecture
+-----------+
| Browser |
+-----+-----+
|
port 80 (HTTP)
|
+-------+-------+
| Nginx |
| (frontend) |
| non-root:8080 |
+---+-------+---+
| |
static | | /api/*
files | | (rate-limited auth)
v v
+---+-------+---+
| FastAPI |
| (backend) |
| non-root |
+-------+-------+
|
+-------+-------+
| PostgreSQL |
| (db) |
| port 5432 |
+---------------+
- Frontend is built as static files and served by
nginxinc/nginx-unprivileged. Nginx also reverse-proxies API requests to the backend with rate limiting on auth endpoints. - Backend runs Alembic migrations on startup as a non-root user (
appuser), then serves the FastAPI application with--no-server-header. - Database uses a named Docker volume (
postgres_data) for persistence. - Backend port 8000 is not exposed externally — only accessible via the internal Docker network.
Security
Hardened by default
- Non-root containers — both backend (
appuser:1000) and frontend (nginx-unprivileged) run as non-root - No external backend port — port 8000 is internal-only; all traffic flows through nginx
- Server version suppression —
server_tokens off(nginx) and--no-server-header(uvicorn) - Rate limiting — nginx
limit_req_zone(10 req/min) on/api/auth/login(burst=5),/verify-password(burst=5),/change-password(burst=5),/totp-verify(burst=5),/setup(burst=3) - DB-backed account lockout — 10 failed attempts triggers 30-minute lock per account
- Dotfile blocking —
/.env,/.git/config, etc. return 404 (.well-knownpreserved for ACME) - CSP headers — Content-Security-Policy on all responses, scoped for Google Fonts
- CORS — configurable origins with explicit method/header allowlists
- API docs disabled in production — Swagger/ReDoc/OpenAPI only available when
ENVIRONMENT=development - Argon2id password hashing with transparent bcrypt migration on first login
- DB-backed sessions — revocable, with signed itsdangerous cookies
- Optional TOTP MFA — authenticator app support with backup codes
Production Hardening
Before deploying to production, generate secure values for your .env:
# Generate a secure SECRET_KEY (64-char hex string)
python3 -c "import secrets; print(secrets.token_hex(32))"
# or: openssl rand -hex 32
# Generate a secure database password
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
# or: openssl rand -base64 24
# Set ENVIRONMENT to disable Swagger/ReDoc
ENVIRONMENT=production
# Enable secure cookies (requires HTTPS)
COOKIE_SECURE=true
Additionally for production:
- Place behind a reverse proxy with TLS termination (e.g., Caddy, Traefik, or nginx with Let's Encrypt)
- Set
COOKIE_SECURE=trueto enforce HTTPS-only session cookies - Set
ENVIRONMENT=productionto disable API documentation endpoints - Set
CORS_ORIGINSto your actual domain (e.g.,https://umbra.example.com) - Consider adding HSTS headers at the TLS-terminating proxy layer
API Overview
All endpoints require authentication (signed session cookie) except auth routes and the health check.
| Endpoint | Description |
|---|---|
GET /health |
Health check |
/api/auth/* |
Login, logout, setup, status, password change, TOTP MFA |
/api/todos/* |
Todos CRUD + toggle completion |
/api/events/* |
Calendar events CRUD (incl. recurring) |
/api/event-templates/* |
Event templates CRUD |
/api/calendars/* |
User calendars CRUD + visibility |
/api/reminders/* |
Reminders CRUD + dismiss + snooze + due alerts |
/api/projects/* |
Projects + nested tasks + comments CRUD |
/api/people/* |
People CRUD |
/api/locations/* |
Locations CRUD + search |
/api/settings/* |
App settings + password change + ntfy config |
/api/dashboard |
Dashboard aggregation |
/api/upcoming |
Unified upcoming items feed |
/api/weather/* |
Weather data proxy |
API documentation is available at /api/docs (Swagger UI) when ENVIRONMENT=development.
Development
Rebuild a single service
docker-compose up --build backend # Backend only
docker-compose up --build frontend # Frontend only
View logs
docker-compose logs -f # All services
docker-compose logs -f backend # Backend only
Reset database
docker-compose down -v && docker-compose up --build
Stop all services
docker-compose down
Project Structure
umbra/
├── docker-compose.yaml
├── .env / .env.example
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── alembic.ini
│ ├── alembic/versions/ # 25 migrations (001–025)
│ └── app/
│ ├── main.py # FastAPI app, router registration, health endpoint
│ ├── config.py # Pydantic BaseSettings (DATABASE_URL, SECRET_KEY, CORS, etc.)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # 17 SQLAlchemy ORM models
│ ├── schemas/ # Pydantic v2 request/response schemas
│ ├── routers/ # 14 API route handlers
│ ├── services/ # Auth (Argon2id), recurrence, TOTP, ntfy
│ └── jobs/ # APScheduler notification dispatch
└── frontend/
├── Dockerfile
├── nginx.conf
├── proxy-params.conf # Shared proxy settings (DRY include)
├── package.json
└── src/
├── App.tsx # Routes and auth guard
├── lib/ # api.ts, date-utils.ts, utils.ts
├── hooks/ # useAuth, useSettings, useTheme, useCalendars, useConfirmAction, useCategoryOrder, useTableVisibility
├── types/ # TypeScript interfaces
└── components/
├── ui/ # 18 base components (Button, Dialog, Sheet, Card, Input, Select, Switch, etc.)
├── shared/ # EntityTable, EntityDetailPanel, CategoryFilterBar, CategoryAutocomplete, CopyableField
├── layout/ # AppLayout, Sidebar, LockOverlay
├── auth/ # LockScreen, AmbientBackground
├── dashboard/ # DashboardPage + 8 widgets
├── calendar/ # CalendarPage, CalendarSidebar, CalendarForm, EventForm, TemplateForm
├── todos/ # TodosPage, TodoList, TodoItem, TodoForm
├── reminders/ # RemindersPage, ReminderList, ReminderItem, ReminderForm, SnoozeDropdown, AlertBanner
├── projects/ # ProjectsPage, ProjectCard, ProjectDetail, ProjectForm, KanbanBoard, TaskRow, TaskForm, TaskDetailPanel
├── people/ # PeoplePage, PersonForm
├── locations/ # LocationsPage, LocationForm
└── settings/ # SettingsPage, NtfySettingsSection, TotpSetupSection
License
This project is for personal use. Feel free to fork and adapt for your own needs.