S Downshiftr Open the app

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-key header (/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 by appHash() in auth.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:

MethodPathPurpose
POST/streamSame as POST / but streams tokens via SSE
GET/historyMessages for a session (?session_id=)
DELETE/historyClear a session’s history
DELETE/history/:idDelete one message
GET/sessionsList the user’s chat sessions
PATCH/sessions/:sessionId/renameRename a session
GET/active-agentWhich agent is active for a session

Life goals — /api/life-goals

Hierarchical goals (directories) that hold habits.

MethodPathPurpose
GET/List goals
GET/treeGoals and habits flattened for the Habits canvas
POST/Create goal ({ title, parent_id? })
GET / PUT / DELETE/:idRead / update / delete
PATCH/:id/moveReparent ({ 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.

MethodPathPurpose
GET / POST/one-timeList / create one-time tasks
GET / PUT / DELETE/one-time/:idRead / update / delete
POST/one-time/:id/completeMark complete
GET / POST/recurringList / create habits
GET / PUT / DELETE/recurring/:idRead / update / delete
POST/recurring/:id/completeLog a completion ({ completed_at? }, backdatable)
POST/recurring/:id/uncompleteUndo the latest completion
PATCH/recurring/:id/moveMove to a goal ({ goal_id })

Activation debt — /api/activation

Activator→downshifter sessions and the debt ledger.

MethodPathPurpose
POST/sessionsOpen a session ({ activator_id, downshifter_id, offset_minutes? })
GET/sessions · /sessions/openList all / open sessions
POST/sessions/:id/resolveResolve (logs the downshifter, pays the debt)
DELETE/sessions/:idDelete
GET/debt{ net_debt, open_count, overdue_count }
GET/graphLearned downshifter→activator edges

Goals & decisions

Goals here are the panic/decision goals with generated cycle phases (distinct from /api/life-goals).

PrefixMethodPathPurpose
/api/goalsGET/POST/List / create
/api/goalsPATCH/DELETE/:idUpdate / delete
/api/goalsPATCH/:id/cyclesUpdate cycle phases
/api/goalsPOST/:id/generate-cyclesAI-generate cycle phases
/api/decisionsGET/ · /activeList / active decision
/api/decisionsPOST/Start a decision
/api/decisionsPATCH/DELETE/:idUpdate / delete
/api/decisionsPOST/:id/completeComplete with a summary
/api/decisionPOST/One-shot state-check → decision flow

Check-ins & training — /api/checkins

The institutional/training layer: check-in sessions, answers, and training load.

MethodPathPurpose
GET/activeCurrent open check-in session
POST/sessions · /sessions/suggest-lessonsCreate / suggest lessons
PATCH/sessions/:idUpdate a session
POST/sessions/:id/checkinsAdd a check-in to a session
POST/:id/answer · /:id/skip · /:id/result · /:id/discussAnswer / skip / record result / discuss
POST/recapRecap a stale session
GET/metricsToday’s computed metrics (incl. activation debt)
POST/recompute-training-loadRecompute training load

Panic — /api/panic

MethodPathPurpose
GET / POST/List / open a panic moment
POST/:id/messageSend a message (streams an AI reply)
PATCH/:idUpdate (panic type, goal, cycle, resolve)
DELETE/:idDelete

Todo lists — /api/todo-lists

MethodPathPurpose
GET/ · /by-date/:date · /:idList / by date / one
POST / PUT/ · /:idCreate / update
POST/:id/complete-item · /:id/uncomplete-itemToggle an item

User states — /api/user-states

Physical state snapshots (energy / soreness / sickness).

MethodPathPurpose
GET/ · /latestList / most recent
POST/Save a state
PUT/:idCorrect a state

Education — /api/education

MethodPathPurposeAuth
GET/modules · /modules/:slugList / read published modulessession
POST/modules/:slug/progressRecord progresssession
GET/POST/PATCH/DELETE/admin/modules…Manage modulesadmin
POST/admin/modules/:id/publish · /admin/seedPublish / seedadmin

Routines — /api/routines

Saved quick-action prompts.

MethodPathPurpose
GET / POST/List / create
POST/:id/useBump use count
DELETE/:idDelete

Settings & profile

PrefixMethodPathPurpose
/api/settingsGET/PUT/Read / update (display name, timezone, theme, nudge prefs)
/api/settingsPUT/DELETE/api-keySet / clear per-user OpenAI key
/api/usersGET/PUT/meRead / update profile
/api/usersPUT/me/api-keySet per-user OpenAI key

Help — /api/help

MethodPathPurpose
GET/ · /:slugList / read help articles
POST / PUT / DELETE/ · /:slugManage (admin)

Admin — /api/admin (admin only)

MethodPathPurpose
GET/usersList users with metrics
PATCH / DELETE/users/:idUpdate / delete a user
GET/logsRecent server logs
GET / PUT/debug-loggingRead / toggle debug logging
GET/POST/PUT/DELETE/help-articles…Manage help articles