Skip to content

API Endpoints

Detailed API Reference

A comprehensive API reference with full endpoint details, query parameters, request/response schemas, and error codes is available in the API Reference section.

This page serves as a quick lookup table. Refer to the dedicated pages for complete documentation.

This page documents the Hono REST API endpoints for the Coaching App backend.

Base URL

  • Development: http://localhost:3001
  • Production: Set via BETTER_AUTH_URL env var

All routes are prefixed with /api.

Client API Key

Every request (from any client — web, iOS, Android) must include the shared app secret in the X-Client-Key header. This key identifies first-party clients and is validated before any route or user authentication runs.

X-Client-Key: cak_<your_key>
  • The key is configured via the CLIENT_API_KEY env var on the backend.
  • The frontend reads it from EXPO_PUBLIC_CLIENT_KEY and injects it automatically in frontend/src/lib/api.ts.
  • Requests missing the header or sending a wrong key receive 403 Forbidden with no further detail.
  • OPTIONS (CORS preflight) requests are exempt — browsers cannot send custom headers on preflight.
  • The server refuses to start if CLIENT_API_KEY is absent or shorter than 32 characters.

Generating a new key

node -e "
const c = require('crypto');
const A = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let n = BigInt('0x' + c.randomBytes(32).toString('hex')), e = '';
while (n > 0n) { e = A[Number(n % 58n)] + e; n /= 58n; }
console.log('cak_' + e);
"

Set the same value in both backend/.env (CLIENT_API_KEY) and frontend/.env (EXPO_PUBLIC_CLIENT_KEY).

Authentication

Authentication is handled by Better-auth. Session cookie is set on sign-in.

Method Path Description
POST /api/auth/sign-up/email Register new user
POST /api/auth/sign-in/email Sign in
POST /api/auth/sign-out Sign out
GET /api/auth/session Get current session
POST /api/auth/forget-password Request password reset email
POST /api/auth/reset-password Reset password with token
POST /api/auth/resend-activation Resend activation email (60s cooldown per email)
POST /api/auth/refresh-token Refresh session

Me

Requires authentication.

Method Path Description
GET /api/me Current user + profile
PATCH /api/me Update own profile
GET /api/me/organizations User's organizations

Users

Requires manager role or above.

Method Path Description
GET /api/users List users (org-scoped)
GET /api/users/:id Get user by ID
PATCH /api/users/:id Update user
DELETE /api/users/:id Delete user

Organizations

Method Path Min Role Description
GET /api/organizations manager List organizations
POST /api/organizations platform_admin Create organization
GET /api/organizations/:id manager Get organization
PATCH /api/organizations/:id organization_admin Update organization
DELETE /api/organizations/:id platform_admin Archive organization
POST /api/organizations/:id/users organization_admin Add user to org
PATCH /api/organizations/:id/users/:userId organization_admin Update user role or re-enable membership (membershipStatus: 'active')
DELETE /api/organizations/:id/users/:userId organization_admin Soft-disable user's org membership (sets membershipStatus = 'disabled'; does not delete the row)
GET /api/organizations/:id/invites manager List pending invites for the organization (manager sees coach/teacher only)
POST /api/organizations/:id/invites manager Create or resend org invite (OrganizationAdmin can invite all roles; Manager only coach/teacher)

Soft-disable vs. hard delete

DELETE /api/organizations/:id/users/:userId sets the member's membershipStatus to disabled rather than removing the row. The user retains their profile and all associated content, but loses org-scoped access immediately (the tenant middleware only grants orgRole for active memberships). To restore access, call PATCH on the same path with { "membershipStatus": "active" }.

Invites

Invite onboarding supports two modes:

  • New account: user signs up from invite email, then manually accepts/rejects pending invite after sign in.
  • Existing account: invite page detects account and shows immediate sign-in CTA.
Method Path Auth Description
GET /api/invites/:token Public Resolve invite token metadata (isAvailable, hasAccount, role, org, expiry)
POST /api/invites/accept Public Legacy flow: create account and accept invite in one step
GET /api/invites/my-pending Authenticated List caller pending invites by email
POST /api/invites/:id/accept Authenticated Accept one pending invite manually
POST /api/invites/:id/decline Authenticated Decline one pending invite

Invite-first gate

When an authenticated user has pending invites, protected app endpoints are blocked with 403 until all pending invites are accepted or declined. Invite endpoints remain accessible so the user can complete onboarding.

Mentorships

Requires authentication. Org-scoped.

Method Path Min Role Description
GET /api/mentorships coach List mentorships visible to caller
POST /api/mentorships coach Create mentorship
GET /api/mentorships/:id coach Get mentorship details
PATCH /api/mentorships/:id coach Update notes / status
GET /api/mentorships/:id/files coach List files attached to mentorship
GET /api/chat/conversations teacher List conversations (newest first)
POST /api/mentorships/:id/chat/token teacher Mint Agora RTM token for chat
GET /api/mentorships/:id/chat/messages teacher List chat messages (cursor paginated)
POST /api/mentorships/:id/chat/messages teacher Send a chat message
PATCH /api/mentorships/:id/chat/read-receipt teacher Update last-read message pointer
GET /api/mentorships/:id/chat/unread teacher Get unread message count

Sessions

Requires authentication. Scoped to a mentorship.

Method Path Min Role Description
GET /api/sessions coach List sessions (filter by mentorshipId)
POST /api/sessions coach Schedule a new session
GET /api/sessions/:id coach Get session details
PATCH /api/sessions/:id coach Update session (status, notes, time)
DELETE /api/sessions/:id coach Cancel / delete a session
GET /api/sessions/:id/agora-token coach Get Agora RTC token for a video session
POST /api/sessions/:id/reactions coach Send in-call emoji reaction

Real-time Events (SSE)

Server-Sent Events stream for live session list updates. Uses react-native-sse on the frontend (supports custom headers on all platforms).

Method Path Min Role Description
GET /api/events/mentorships/:mentorshipId/events coach SSE stream for a mentorship's sessions

How it works

  1. Backend — Hono streamSSE opens a persistent HTTP connection. A per-connection Redis subscriber (via redis.duplicate()) listens on mentorship:{id}:sessions and mentorship:{id}:reactions.
  2. Session mutations — Every POST, PATCH, and DELETE on /api/sessions calls redis.publish('mentorship:{id}:sessions', ...) as a fire-and-forget side-effect.
  3. Call reactionsPOST /api/sessions/:id/reactions validates emoji input and publishes redis.publish('mentorship:{id}:reactions', { emoji, userId, sessionId }).
  4. FrontenduseMentorshipSSE(mentorshipId) hook opens the stream with Authorization, X-Client-Key, and X-Organization-Id headers. On session-update it invalidates queryKeys.mentorships.sessions(mentorshipId); on call-reaction it forwards payloads to call UI callbacks for floating reaction rendering.
  5. Heartbeat — A ping event is sent every 25 s to prevent Nginx/proxy idle-connection timeouts.
  6. ACL — Only the coach, teacher, or a PlatformAdmin participant of that specific mentorship may connect; all others receive 403.
# Response headers
Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no

# Event payload
event: session-update
data: {"action":"updated","mentorshipId":"<uuid>"}

event: call-reaction
data: {"emoji":"🎉","userId":"<uuid>","sessionId":"<uuid>"}

event: ping
data:

Files

Requires authentication. File uploads use a presigned PUT flow — the client uploads directly to S3/R2 without proxying through the backend.

Upload flow

1. POST /api/files/presign     → { presignedUrl, storageKey }
2. PUT  <presignedUrl>          → upload directly to S3/R2 (with XHR progress)
3. POST /api/files/register    → { fileId }
4. POST /api/files/:id/attach  → attach the file to an object
Method Path Min role Description
POST /api/files/presign coach Request a presigned PUT URL (15 min expiry)
POST /api/files/register coach Create the DB record after a direct S3 PUT
POST /api/files/:fileId/attach coach Attach a registered file to a mentorship object
DELETE /api/files/:fileId/attach/:objectType/:objectId coach Detach file; triggers orphan cleanup if last ref
GET /api/files/:fileId coach File metadata + short-lived presigned GET URL
DELETE /api/files/:fileId manager Hard-delete file and remove from storage
GET /api/files/mentorship/:mentorshipId/attachments coach List files attached to a mentorship

Presign request

// POST /api/files/presign
{
  "filename": "report.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 204800,
  "prefix": "mentorships"   // optional storage key prefix
}

// Response 200
{
  "data": {
    "presignedUrl": "https://...",
    "storageKey": "mentorships/uuid.pdf"
  }
}

Register request

// POST /api/files/register
{
  "storageKey": "mentorships/uuid.pdf",
  "filename": "report.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 204800
}

// Response 201
{ "data": { "fileId": "uuid" } }

R2 bucket CORS

The R2 bucket must have a CORS rule allowing PUT from the frontend origin. Configure this in the Cloudflare dashboard → R2 → bucket → Settings → CORS.

Setup

Used only for bootstrap. This endpoint is self-disabling after first success.

Method Path Description
POST /api/setup/first-run Create first platform admin when no platform-level user exists

System

Operational endpoint for dependency and environment checks.

Method Path Description
GET /api/health Health checks for postgres/redis/s3/mail/agora plus env coverage

Request / Response Format

Authentication

// POST /api/auth/sign-in/email
{
  "email": "user@example.com",
  "password": "Secret1234!"
}

// Response 200
{
  "token": "...",  // session token
  "user": {
    "id": "...",
    "email": "user@example.com",
    "name": "User Name"
  }
}

Error Format

// 4xx / 5xx
{
  "error": "Unauthorized",
  "message": "Invalid credentials"
}

Setup First Run

// POST /api/setup/first-run
{
  "email": "hola@jorgeyau.com",
  "password": "StrongPassword123!",
  "name": "Jorge Yau"
}

// Response 201
{
  "data": {
    "id": "U89P87yndMQdRuvIfMszJCanBDFFhhts",
    "email": "hola@jorgeyau.com",
    "role": "PlatformAdmin"
  }
}

Expected failure states:

  • 409 Already initialized if any Owner or PlatformAdmin already exists
  • 500 Internal Server Error if database schema is not migrated yet

Notes

  • All user-facing flows go through the Hono REST API. Never bypass the API layer.
  • Password reset endpoints are used by the frontend and send email via the configured mail service.
  • Organization endpoints enforce role-based access in backend route handlers.