The W-03 QA fix (removing refetchIntervalInBackground) broke toast notifications when the receiver tab is hidden (e.g. user switches to sender's tab). Without background polling, unread count never updates, NotificationToaster never detects new notifications, and no toast fires. Restored with explanatory comment documenting the dependency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UMBRA
A self-hosted, multi-user life administration app with a dark-themed UI and role-based access control. Manage your todos, calendar events, projects, reminders, contacts, and locations from a single dashboard.
Features
- Multi-user RBAC - Admin and standard user roles, per-user data isolation, admin portal with IAM, system config, and audit logs
- 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 (8 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)
- Admin Portal - User management (create, delete, activate/deactivate, role assignment, password reset), system configuration (open registration, MFA enforcement), audit log viewer
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 (37 migrations) |
| Auth | Argon2id hashing, DB-backed sessions (signed httpOnly cookies), TOTP MFA, CSRF middleware, role-based access control |
| 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 an admin account.
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
- Multi-user data isolation — all resources scoped by
user_idwith per-query filtering; pentest-verified (51+ test cases, 0 exploitable IDOR findings) - Role-based access control —
adminandstandardroles withrequire_admindependency on all admin endpoints - CSRF protection — global
CSRFHeaderMiddlewarerequiresX-Requested-With: XMLHttpRequeston all mutating requests - Input validation —
extra="forbid"on all Pydantic schemas prevents mass-assignment;max_lengthon all string fields;ge=1, le=2147483647on path IDs - 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
- Inactive user blocking — disabled accounts rejected at login (HTTP 403) without session creation, lockout reset, or last_login_at update
- Timing-safe login — dummy Argon2id hash for non-existent users prevents username enumeration
- Password reuse prevention — change-password endpoint rejects same password as old
- 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 httpOnly cookies, 7-day sliding window with 30-day hard ceiling
- Optional TOTP MFA — authenticator app support with backup codes, admin-enforced MFA for new users
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 and auto-enable secure cookies
ENVIRONMENT=production
Additionally for production:
- Place behind a reverse proxy with TLS termination (e.g., Caddy, Traefik, or nginx with Let's Encrypt)
- Set
ENVIRONMENT=production— this disables API docs and auto-enables HTTPS-only session cookies (COOKIE_SECUREderives fromENVIRONMENT; override withCOOKIE_SECURE=falseif running non-TLS prod behind a proxy) - 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. Admin endpoints require the admin role.
| Endpoint | Description |
|---|---|
GET /health |
Health check |
/api/auth/* |
Login, logout, setup, register, status, password change, TOTP MFA |
/api/admin/* |
User management, system config, audit logs (admin only) |
/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 + 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/ # 37 migrations (001–037)
│ └── app/
│ ├── main.py # FastAPI app, CSRF middleware, router registration, health endpoint
│ ├── config.py # Pydantic BaseSettings (DATABASE_URL, SECRET_KEY, CORS, etc.)
│ ├── database.py # Async SQLAlchemy engine + session factory
│ ├── models/ # 18 SQLAlchemy ORM models (incl. User, UserSession, SystemConfig, AuditLog)
│ ├── schemas/ # 13 Pydantic v2 request/response schema modules (incl. admin)
│ ├── routers/ # 14 API route handlers (incl. auth, admin, totp)
│ ├── services/ # Auth (Argon2id), recurrence, TOTP, ntfy, audit
│ └── jobs/ # APScheduler notification dispatch
└── frontend/
├── Dockerfile
├── nginx.conf
├── proxy-params.conf # Shared proxy settings (DRY include)
├── package.json
└── src/
├── App.tsx # Routes, ProtectedRoute, AdminRoute auth guards
├── lib/ # api.ts (axios + 401 interceptor), date-utils.ts, utils.ts
├── hooks/ # useAuth, useAdmin, useSettings, useTheme, useCalendars, useConfirmAction, useCategoryOrder, useTableVisibility
├── types/ # TypeScript interfaces
└── components/
├── ui/ # 17 base components (Button, Dialog, Sheet, Card, Input, Select, Switch, etc.)
├── shared/ # EntityTable, EntityDetailPanel, CategoryFilterBar, CategoryAutocomplete, CopyableField
├── layout/ # AppLayout, Sidebar, LockOverlay
├── auth/ # LockScreen, AmbientBackground
├── admin/ # AdminPortal, IAMPage, ConfigPage, AdminDashboardPage, CreateUserDialog, UserActionsMenu, UserDetailSection
├── 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.