Version: 2.0
Updated: 2026-09-04
Branch:chore/api-platform-hardening
This document describes the stable, tested behavior of the Life OS backend API. It is the authoritative reference for any client (web UI, future MCP, integrations).
All /api/* routes (except /api/auth/* and /api/health) require:
Authorization: Bearer <jwt>
A missing or invalid token returns:
HTTP 401
{ "error": "Unauthorized", "code": "UNAUTHORIZED" }All database operations are strictly scoped to the authenticated user.
The server derives userId from the JWT — client-provided userId fields are stripped.
Cross-user access returns 404 Not Found, not 403 Forbidden (to prevent information leakage).
All errors follow a consistent structure:
{
"error": "Human-readable message",
"code": "MACHINE_READABLE_CODE",
"details": {} // optional, present for validation errors
}Common codes:
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED |
401 | Missing or invalid JWT |
FORBIDDEN |
403 | Authenticated but not allowed |
NOT_FOUND |
404 | Resource not found (or belongs to another user) |
VALIDATION_ERROR |
400 | Invalid request body/params |
INVALID_ID |
400 | Malformed MongoDB ObjectId in path |
INTERNAL_ERROR |
500 | Unexpected server error |
The server strips the following from all request bodies:
userId— always derived from JWT_id,__v— server-controlled- Keys starting with
$— MongoDB operator injection prevention
| Scope | Limit |
|---|---|
| Global | 1000 req / 15 min per IP |
/api/auth/* |
50 req / 15 min per IP |
| MFA endpoints | 10 req / 15 min per IP |
Rate limit exceeded: HTTP 429 { "error": "Too many requests...", "code": "RATE_LIMIT_EXCEEDED" }
The following operations are implemented as reusable services. The web API and any future MCP server both call these same services.
| Service | Methods |
|---|---|
TaskService |
getTasks, createTask, updateTask, deleteTask |
HabitService |
getHabits, createHabit, updateHabit, deleteHabit, logCompletion, unlogCompletion |
GoalService |
getGoals, createGoal, updateGoal, deleteGoal |
CaptureService |
getCaptures, createCapture, updateCapture, deleteCapture |
WorkoutService |
getWorkouts, logWorkout, updateWorkout, deleteWorkout |
ProjectService |
getProjects, createProject, updateProject, deleteProject |
JournalService |
getEntries, saveEntry, deleteEntry |
SearchService |
search(query) |
BackupService |
runBackupForUser, verifyChecksum, generateBackupFilename |
RestoreService |
validateBackup, applyRestore |
Returns server and database status. No auth required.
{ "status": "ok", "db": "connected", "timestamp": "2026-09-04T..." }Returns HTTP 503 when database is not connected.
All endpoints subject to auth rate limiter (50/15min).
Body: { name, email, password }
- Returns
201 { user, token } 400if fields missing or invalid409if email already registered
Body: { email, password }
- Returns
200 { token, user }or200 { mfaRequired: true, mfaToken } 400if fields missing401if credentials invalid
- Returns the authenticated user (sensitive fields excluded:
password,mfaSecret,googleTokens,settings.aiKeys)
- Returns all tasks for the authenticated user, sorted newest first
- No pagination (tasks are bounded in personal use)
Body: { title, status?, priority?, dueDate?, tags? }
titleis required (non-empty string)- Returns
201 { task } 400if title is missing
Body: partial task fields
- Returns
200 { task } 404if task not found or not owned by user- Awards 15 XP atomically on first
status: 'done'transition
- Returns
200 { success: true } 404if not found or not owned
- Returns all habits, sorted by
order
Body: { name, icon?, color?, frequency? }
nameis required- Returns
201 { habit }
- Updates habit metadata (name, icon, color, frequency, order)
- Returns
200 { habit }
- Returns
200 { success: true }
Body: { date: "YYYY-MM-DD" }
- Marks habit as complete for that date
- Idempotent — safe to call multiple times for the same date (
$addToSet) - Awards 10 XP on first completion of that date
- Returns
200 { habit }
Body: { date: "YYYY-MM-DD" }
- Removes completion for that date
- Deducts 10 XP if the date was previously logged
- Returns
200 { habit }
- Returns up to 100 goals, sorted newest first
Body: { title, category?, target?, unit?, deadline?, status? }
titlerequiredtargetmust be non-negative number if providedstatusmust be one of:active | completed | paused- Returns
201 { goal }
statusmust be a valid enum value if provided- Awards 50 XP atomically on first
status: 'completed'transition - Returns
200 { goal }
- Returns
200 { success: true }
- Returns up to 100 projects, sorted by
updatedAtdescending
Body: { name, description?, status?, deadline? }
namerequiredstatusmust be one of:active | completed | paused | archived- Returns
201 { project }
- Standard update/delete with ownership check
Query params: q, type, source, processed, limit (default 100, max 500), skip
- Full-text search via
qwhen the$textindex exists - Returns array of captures
Body: { text, type?, source?, tags? }
textrequired (max 10,000 chars)type:thought | idea | todo | reminder(defaults tothought)source:manual | api | import | automation | future_mcp | future_agent(defaults tomanual)- Tags: max 10, each max 50 chars, lowercased
- Returns
201 { capture }
- Updatable fields:
text,type,tags,processed - Returns
200 { capture }
- Returns
200 { success: true }
Query: limit (default 100, max 365)
- Returns entries sorted by date descending
Body: { date (YYYY-MM-DD, required), title?, content?, mood? (1-5), tags? }
- Upsert-by-date — one entry per day; later write wins
- Returns the upserted entry
- Returns
200 { success: true }
- Returns last 100 workouts, sorted by date descending
Body: { name (required), duration? (0-1440 min), date? (YYYY-MM-DD), exercises? }
- Awards 25 XP per workout
- Returns
201 { workout }
- Standard update/delete with ownership check
- Returns up to 200 notes, sorted by
updatedAtdescending
- If body contains
_id: upsert (update existing note by ID) - If no
_id: create new note - Returns the note
- Returns
200 { success: true }
Query: from (YYYY-MM-DD), to (YYYY-MM-DD) — optional date range
- Returns up to 500 expenses, sorted by date descending
Body: { amount (required, number), date? (YYYY-MM-DD), description?, category? }
amountmust be a finite number- Returns
201 { expense }
- Standard update/delete with ownership check
- Returns up to 200 bookmarks, sorted newest first
Body: { url (required), title?, tags?, category? }
- Returns
201 { bookmark }
- Standard update/delete
- Returns up to 200 books, sorted newest first
Body: { title (required), author?, status?, notes? }
- Returns
201 { book }
- Standard update/delete
Query: q (required), types (comma-separated), limit (default 30, max 100), skip
- Keyword search across: tasks, goals, notes, journal, habits, captures, bookmarks, books, projects
- Results include:
id,type,title,subtitle,snippet,view,recordId,score - Always user-scoped —
userIdnever accepted from client
tz — minutes ahead of UTC (e.g., 330 for IST UTC+5:30). Default: 0 (UTC).
- Returns:
{ date, tasks, habits, goals, journal, inbox, projects } - No write operations
- Returns cross-collection aggregation for the given date range
- Both
startandendare required
Query: limit (default 50, max 200), skip
- Returns enriched audit log events (those with
eventTypeset) - Fields:
eventType,source,metadata,timestamp,collectionName,documentId
- Returns a full JSON export of all user data
- Sensitive fields excluded (see
EXPORT_FORMAT.md)
See BACKUP_AND_RECOVERY.md and BACKUP_FORMAT.md.
| Endpoint | Default Limit | Max Limit | Pagination Style |
|---|---|---|---|
/api/activity |
50 | 200 | limit + skip |
/api/captures |
100 | 500 | limit + skip |
/api/chat |
50 | 100 | limit + skip |
/api/goals |
100 | 100 | Hard cap |
/api/projects |
100 | 100 | Hard cap |
/api/notes |
200 | 200 | Hard cap |
/api/bookmarks |
200 | 200 | Hard cap |
/api/books |
200 | 200 | Hard cap |
/api/expenses |
500 | 500 | Hard cap |
/api/journal |
100 | 365 | limit query param |
/api/workouts |
100 | 100 | Hard cap |
/api/search |
30 | 100 | limit + skip |
"Hard cap" = a single .limit() call with no pagination API. Suitable for personal data volumes.
| Action | XP |
|---|---|
| Complete a task | +15 |
| Log a habit | +10 |
| Unlog a habit | -10 |
| Log a workout | +25 |
| Complete a goal | +50 |
XP awards use atomic findOneAndUpdate with _xpAwarded: { $ne: true } to prevent double-awarding on retries.
- No PAT (Personal Access Token) system — MCP will need long-lived tokens
- No webhook support — events are in the AuditLog but not pushed to external systems
- Search is keyword-only — no semantic/vector search
- Backup scheduler is in-memory — lost on process restart
- Single-tenant per deployment — no multi-organization support