API contract
All routes are served by the Next.js app. Auth is either:
Cookie: ll_session=<jwt>(web) — set by the OAuth callback.Authorization: Bearer <jwt>(mobile) — JWT delivered via thelanglearn://auth/callback?token=…redirect.
The JWT is signed HS256 with AUTH_SECRET and contains { userId, hfUsername }. TTL is 7 days.
Auth
GET /api/auth/login
Starts the HuggingFace OAuth dance.
Query params:
client=mobile— if set, the callback will 302 tolanglearn://auth/callback?token=…instead of setting a cookie. Otherwise, cookie + 302 to/practice.
GET /api/auth/callback/huggingface
Handles HF's redirect back. Validates state + verifier cookies, exchanges code, upserts user, mints JWT. Never call directly.
POST /api/auth/logout
Clears the session cookie. For mobile, the client just drops the stored JWT.
Profile
GET /api/me
{
"user": { "id": "uuid", "hfUsername": "alice", "email": "a@x.y", "avatarUrl": "…" },
"profile": { "nativeLang": "en", "targetLang": "es", "level": "A2" } | null
}
PUT /api/me/profile
Body:
{ "nativeLang": "en", "targetLang": "es", "level": "A2" }
Responses: 200 with { profile }, 400 on validation errors.
Mobile bearer-token requests also receive { token } with a refreshed JWT carrying the updated profile.
Stories
GET /api/stories
Lists stories matching the user's profile (targetLang + level). Response:
{ "stories": [{ "id", "title", "targetLang", "level", "createdAt" }], "profile": {...} }
POST /api/stories
Generates a new story for the user's profile.
Body (optional): { "topic": "a walk in the park" }.
Response: { "story": { id, title, content, vocab, ... } }.
GET /api/stories/:id
{ "story": { ... full story ... } }
POST /api/stories/:id/read
Marks the story as read by the current user.
Translate
POST /api/translate
{ "text": "perro", "from": "es", "to": "en" }
Response: { "translation": "dog", "cached": true? }.
Vision
POST /api/vision/analyze
Multipart form with field image (≤ 8 MB). Response:
{
"id": "uuid",
"caption": "a cat sitting on a couch",
"objects": [
{ "label": "cat", "translation": "gato", "box": [x1,y1,x2,y2], "score": 0.92 }
],
"sentences": [
{ "target": "El gato está sobre el sofá.", "gloss": "The cat is on the sofa." }
],
"imageUrl": "https://…" // empty if blob storage isn't configured
}
Error shape
All failures return { "error": "code", "message"?: "…" } with an appropriate HTTP status.
Common codes: unauthenticated (401), invalid_input (400), no_profile (400), inference_failed (502), image_too_large (413).