App Developer Docs
API Reference
All routes are prefixed with /api. Requests and responses use application/json. Cookies are sent automatically by the browser (withCredentials: true in the axios client).
Authentication: every route requires a valid session_token cookie unless marked otherwise:
- public — usable before sign-in (
/api/auth/login,/register,/request-magic-link,/magic,/setup-password). - admin — requires a session whose user has
is_admin = 1(all of/api/admin/*, the/api/education/admin/*routes). - cron — guarded by the
x-cron-keyheader (/api/push/tick).
This page documents auth, chat, and push in full, then lists the remaining application routes by domain. Most app data is stored as JSON blobs; bodies are largely pass-through ({ ...fields }) and responses return the saved row as { id, data, created_at, updated_at }.
Auth routes
POST /api/auth/request-magic-link
Request a magic sign-in link. Creates the user if the email is new.
Request body:
{ "email": "you@example.com" }
Response 200:
{ "ok": true }
Always returns 200 — even if the user doesn’t exist. This prevents email enumeration. In development, the link is printed to the server terminal instead of sent by email.
Errors: 400 if email is missing.
GET /api/auth/magic?token=<token>
Validates a magic link token and redirects.
- New user (
needs_password_setup = 1): redirects to/app/#/setup-password?token=<token> - Returning user: burns token, creates session, sets cookie, redirects to
/app/#/chat - Invalid/expired token: redirects to
/app/#/login?error=expired - Missing token param: redirects to
/app/#/login?error=missing-token
Redirects target the SPA base. In production the Vue app is mounted at
/app/(root is the Astro marketing site), so redirects are prefixed with/app; in development the SPA is served at root, so the prefix is empty. This is handled byappHash()inauth.js.
POST /api/auth/setup-password
Complete first-time password setup. Requires a valid magic token.
Request body:
{
"token": "abc123...",
"password": "atleast8chars"
}
Response 200:
{
"id": 1,
"email": "you@example.com",
"display_name": "you@example.com",
"needs_password_setup": false,
"theme": "dark"
}
Sets session_token cookie. Burns the magic token.
Errors: 400 for invalid token, password shorter than 8 characters, or password already set.
POST /api/auth/login
Sign in with email and password.
Request body:
{ "email": "you@example.com", "password": "yourpassword" }
Response 200:
{
"id": 1,
"email": "you@example.com",
"display_name": "you@example.com",
"needs_password_setup": false,
"theme": "dark"
}
Sets session_token cookie.
Errors: 401 for wrong password or unknown email. 400 if fields are missing.
POST /api/auth/register
Create an account with an identifier + password and log in immediately — no email round-trip. The identifier may be a real email (magic-link recovery stays available) or a plain username.
Request body:
{ "email": "you@example.com", "password": "atleast8chars" }
Response 201: the user payload (same shape as /login), with session_token cookie set.
Errors: 400 for missing fields or a password shorter than 8 characters. 409 if the identifier is already taken.
POST /api/auth/logout
End the current session.
Response 200:
{ "ok": true }
Deletes the session row and clears the cookie. Safe to call even if there’s no active session.
GET /api/auth/me
Return the currently authenticated user.
Response 200:
{
"id": 1,
"email": "you@example.com",
"display_name": "you@example.com",
"needs_password_setup": false,
"theme": "dark"
}
Errors: 401 if not authenticated or session expired.
Chat route
POST /api/chat
Send a message to the AI. Requires authentication.
Request body:
{
"messages": [
{ "role": "user", "content": "I feel like I should clean but I'm not sure where to start." }
]
}
Send the full conversation history on every request — the server prepends the system prompt and forwards all messages to OpenAI. Messages are also persisted per session in chat_contexts and can be read back via the history/session endpoints below.
Response 200:
{ "reply": "Given where you are right now…" }
Errors: 401 if not authenticated. 400 if messages is empty. 500 if the OpenAI call fails (check server logs — usually a bad API key or wrong model name).
Conversation format
Each send() call sends the full history. The backend always adds the system prompt at position 0 before forwarding to OpenAI:
[system prompt] + [all user/assistant messages]
The model and system prompt are defined in backend/src/routes/chat.js. The system prompt describes the 8 dimensions and instructs the model to use state-check context when present.
Push notification routes
Web Push lets the coach reach out proactively. Routes are under /api/push.
GET /api/push/vapid-public-key
Returns the server’s VAPID public key (fetched by the service worker at runtime).
{ "key": "B...", "enabled": true }
enabled is false when VAPID keys aren’t configured — push is then a no-op.
POST /api/push/subscribe
Store a PushSubscription for the authenticated user (deduped by endpoint).
{ "subscription": { "endpoint": "https://…", "keys": { "p256dh": "…", "auth": "…" } } }
Response 201: { "ok": true, "id": 12 }. Errors: 401 unauthenticated, 400 malformed subscription.
POST /api/push/unsubscribe
Remove a subscription by endpoint. Body: { "endpoint": "https://…" }. Authenticated.
POST /api/push/test
Schedule a test notification ~60 seconds out (so the cron delivers it even with the app closed). Authenticated; requires at least one subscription.
Response 201: { "ok": true, "send_at": "…", "id": 5 }.
GET / POST /api/push/tick
The cron entry point. Guarded by the x-cron-key header (must equal CRON_SECRET). Delivers all due scheduled notifications, prunes dead subscriptions (404/410), and fires per-user morning nudges.
{ "sent": 1, "pruned": 0, "due": 1, "morning_sent": 1, "push_enabled": true }
Errors: 403 if the key is missing/wrong. GET is provided so the cron can avoid Hostinger’s 400 on body-less POSTs.
Cron command (every minute):
curl -fsS -H "x-cron-key: $CRON_SECRET" https://downshiftr.com/api/push/tick
Application routes
All routes below require a session cookie. Paths are relative to the listed prefix.
Chat extras — /api/chat
Beyond POST /api/chat (above), the chat is persisted per session in chat_contexts:
| Method | Path | Purpose |
|---|---|---|
POST | /stream | Same as POST / but streams tokens via SSE |
GET | /history | Messages for a session (?session_id=) |
DELETE | /history | Clear a session’s history |
DELETE | /history/:id | Delete one message |
GET | /sessions | List the user’s chat sessions |
PATCH | /sessions/:sessionId/rename | Rename a session |
GET | /active-agent | Which agent is active for a session |
Life goals — /api/life-goals
Hierarchical goals (directories) that hold habits.
| Method | Path | Purpose |
|---|---|---|
GET | / | List goals |
GET | /tree | Goals and habits flattened for the Habits canvas |
POST | / | Create goal ({ title, parent_id? }) |
GET / PUT / DELETE | /:id | Read / update / delete |
PATCH | /:id/move | Reparent ({ parent_id }) |
Tasks & habits — /api/tasks
One-time tasks and recurring habits. Recurring habits carry kind (activator/downshifter/neutral), interval_hours, and the nudge flag.
| Method | Path | Purpose |
|---|---|---|
GET / POST | /one-time | List / create one-time tasks |
GET / PUT / DELETE | /one-time/:id | Read / update / delete |
POST | /one-time/:id/complete | Mark complete |
GET / POST | /recurring | List / create habits |
GET / PUT / DELETE | /recurring/:id | Read / update / delete |
POST | /recurring/:id/complete | Log a completion ({ completed_at? }, backdatable) |
POST | /recurring/:id/uncomplete | Undo the latest completion |
PATCH | /recurring/:id/move | Move to a goal ({ goal_id }) |
Activation debt — /api/activation
Activator→downshifter sessions and the debt ledger.
| Method | Path | Purpose |
|---|---|---|
POST | /sessions | Open a session ({ activator_id, downshifter_id, offset_minutes? }) |
GET | /sessions · /sessions/open | List all / open sessions |
POST | /sessions/:id/resolve | Resolve (logs the downshifter, pays the debt) |
DELETE | /sessions/:id | Delete |
GET | /debt | { net_debt, open_count, overdue_count } |
GET | /graph | Learned downshifter→activator edges |
Goals & decisions
Goals here are the panic/decision goals with generated cycle phases (distinct from /api/life-goals).
| Prefix | Method | Path | Purpose |
|---|---|---|---|
/api/goals | GET/POST | / | List / create |
/api/goals | PATCH/DELETE | /:id | Update / delete |
/api/goals | PATCH | /:id/cycles | Update cycle phases |
/api/goals | POST | /:id/generate-cycles | AI-generate cycle phases |
/api/decisions | GET | / · /active | List / active decision |
/api/decisions | POST | / | Start a decision |
/api/decisions | PATCH/DELETE | /:id | Update / delete |
/api/decisions | POST | /:id/complete | Complete with a summary |
/api/decision | POST | / | One-shot state-check → decision flow |
Check-ins & training — /api/checkins
The institutional/training layer: check-in sessions, answers, and training load.
| Method | Path | Purpose |
|---|---|---|
GET | /active | Current open check-in session |
POST | /sessions · /sessions/suggest-lessons | Create / suggest lessons |
PATCH | /sessions/:id | Update a session |
POST | /sessions/:id/checkins | Add a check-in to a session |
POST | /:id/answer · /:id/skip · /:id/result · /:id/discuss | Answer / skip / record result / discuss |
POST | /recap | Recap a stale session |
GET | /metrics | Today’s computed metrics (incl. activation debt) |
POST | /recompute-training-load | Recompute training load |
Panic — /api/panic
| Method | Path | Purpose |
|---|---|---|
GET / POST | / | List / open a panic moment |
POST | /:id/message | Send a message (streams an AI reply) |
PATCH | /:id | Update (panic type, goal, cycle, resolve) |
DELETE | /:id | Delete |
Todo lists — /api/todo-lists
| Method | Path | Purpose |
|---|---|---|
GET | / · /by-date/:date · /:id | List / by date / one |
POST / PUT | / · /:id | Create / update |
POST | /:id/complete-item · /:id/uncomplete-item | Toggle an item |
User states — /api/user-states
Physical state snapshots (energy / soreness / sickness).
| Method | Path | Purpose |
|---|---|---|
GET | / · /latest | List / most recent |
POST | / | Save a state |
PUT | /:id | Correct a state |
Education — /api/education
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /modules · /modules/:slug | List / read published modules | session |
POST | /modules/:slug/progress | Record progress | session |
GET/POST/PATCH/DELETE | /admin/modules… | Manage modules | admin |
POST | /admin/modules/:id/publish · /admin/seed | Publish / seed | admin |
Routines — /api/routines
Saved quick-action prompts.
| Method | Path | Purpose |
|---|---|---|
GET / POST | / | List / create |
POST | /:id/use | Bump use count |
DELETE | /:id | Delete |
Settings & profile
| Prefix | Method | Path | Purpose |
|---|---|---|---|
/api/settings | GET/PUT | / | Read / update (display name, timezone, theme, nudge prefs) |
/api/settings | PUT/DELETE | /api-key | Set / clear per-user OpenAI key |
/api/users | GET/PUT | /me | Read / update profile |
/api/users | PUT | /me/api-key | Set per-user OpenAI key |
Help — /api/help
| Method | Path | Purpose |
|---|---|---|
GET | / · /:slug | List / read help articles |
POST / PUT / DELETE | / · /:slug | Manage (admin) |
Admin — /api/admin (admin only)
| Method | Path | Purpose |
|---|---|---|
GET | /users | List users with metrics |
PATCH / DELETE | /users/:id | Update / delete a user |
GET | /logs | Recent server logs |
GET / PUT | /debug-logging | Read / toggle debug logging |
GET/POST/PUT/DELETE | /help-articles… | Manage help articles |