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_URLenv 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.
- The key is configured via the
CLIENT_API_KEYenv var on the backend. - The frontend reads it from
EXPO_PUBLIC_CLIENT_KEYand injects it automatically infrontend/src/lib/api.ts. - Requests missing the header or sending a wrong key receive
403 Forbiddenwith no further detail. OPTIONS(CORS preflight) requests are exempt — browsers cannot send custom headers on preflight.- The server refuses to start if
CLIENT_API_KEYis 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¶
- Backend — Hono
streamSSEopens a persistent HTTP connection. A per-connection Redis subscriber (viaredis.duplicate()) listens onmentorship:{id}:sessionsandmentorship:{id}:reactions. - Session mutations — Every
POST,PATCH, andDELETEon/api/sessionscallsredis.publish('mentorship:{id}:sessions', ...)as a fire-and-forget side-effect. - Call reactions —
POST /api/sessions/:id/reactionsvalidates emoji input and publishesredis.publish('mentorship:{id}:reactions', { emoji, userId, sessionId }). - Frontend —
useMentorshipSSE(mentorshipId)hook opens the stream withAuthorization,X-Client-Key, andX-Organization-Idheaders. Onsession-updateit invalidatesqueryKeys.mentorships.sessions(mentorshipId); oncall-reactionit forwards payloads to call UI callbacks for floating reaction rendering. - Heartbeat — A
pingevent is sent every 25 s to prevent Nginx/proxy idle-connection timeouts. - ACL — Only the coach, teacher, or a
PlatformAdminparticipant of that specific mentorship may connect; all others receive403.
# 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¶
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 initializedif anyOwnerorPlatformAdminalready exists500 Internal Server Errorif 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.