API Layer
~40 API routes organized by domain. Auth middleware patterns, admin client usage, backend proxy, stub endpoints, and snake_case conversion at the boundary.
All external service calls (Supabase writes, Stripe API calls, backend proxying) go through Next.js API route handlers. Pages never call external services directly. This keeps the trust boundary at the API layer and makes it possible to add auth checks, logging, and error handling in a single place per domain.
Route Organization
API routes are grouped by domain under app/api/:
api/
├── auth/
│ ├── [...nextauth]/ NextAuth.js stubs (unused)
│ └── me/ GET - current user session info
├── admin/
│ ├── stats/ GET - dashboard aggregate counts
│ ├── users/ GET - paginated user list with filters
│ ├── books/ GET, PUT, DELETE - book moderation
│ ├── categories/ GET, POST, PUT, DELETE - category CRUD
│ ├── comments/ GET, PUT, DELETE - comment moderation
│ ├── reports/ GET - report listing + resolve/dismiss
│ ├── notifications/ GET, POST, PUT, DELETE - system notifications
│ └── settings/ GET, PUT - platform-wide settings
├── books/
│ ├── route.ts GET - list all books
│ ├── search/ GET - search by query
│ ├── upload/ POST - multipart book upload
│ └── [bookId]/
│ ├── route.ts GET - single book by ID
│ ├── comments/ GET, POST - list and add comments
│ ├── characters/ GET - book's characters
│ ├── rating/ GET - average rating
│ ├── access/ GET - purchase check
│ ├── status/ GET - processing status
│ ├── files/ GET - list book files
│ ├── cover/ GET, DELETE - (stubbed)
│ ├── cover/upload/ POST - (stubbed)
│ └── files/upload/ POST - (stubbed)
├── categories/
│ ├── route.ts GET - all categories
│ └── [slug]/
│ ├── route.ts GET - category + its books
│ └── image/ GET, DELETE, POST - (stubbed)
├── chat/
│ ├── route.ts POST - SSE proxy to backend
│ ├── sessions/ POST - create session
│ ├── session/ POST - create-or-return existing session
│ ├── session/[sessionId]/ GET, POST - fetch session, add message
│ ├── recent/ GET - user's recent conversations
│ └── preferences/ GET, POST, DELETE - per-character preferences
├── files/
│ ├── [fileId]/ GET, DELETE - (stubbed)
│ └── download/[fileId]/ GET - presigned download URL
├── library/
│ ├── route.ts GET, POST - list and add to library
│ └── [bookId]/ GET, DELETE - check and remove
├── media/
│ └── health/ GET - storage health check
├── notifications/ GET, PUT - user notifications
├── payments/
│ ├── create-intent/ POST - Stripe PaymentIntent
│ └── webhook/ POST - Stripe webhook handler
├── profile/
│ ├── route.ts GET, PUT - fetch and update profile
│ ├── image/ GET - profile image metadata
│ ├── image/upload/ POST - avatar upload to Supabase Storage
│ └── password/ PUT - change password via Supabase Auth
└── wishlist/
├── route.ts GET, POST - list and add to wishlist
└── [bookId]/ GET, DELETE - check and removeCommon Patterns
Auth middleware
Most routes obtain the user by calling auth() from auth.ts. Protected routes call requireAuth() from lib/auth-middleware.ts, which throws if no user is found. Admin routes additionally check checkIsStaff() or checkIsAdmin().
Admin client for privileged operations
Routes that need to bypass RLS (admin CRUD, book upload, role checks) use getAdminClient() - a synchronous factory that creates a Supabase client with the service-role key. This client is never used for user-facing queries that could leak data across tenants.
Snake_case conversion
Supabase stores data in snake_case columns. The TypeScript types in types/index.ts use camelCase. The conversion functions toCamelCase() and toSnakeCase() in lib/utils/transform.ts are applied at the boundary by each DB query module. This keeps the rest of the application in camelCase.
Stub endpoints
Six API endpoints are stubs that return 401 "Auth removed" - book cover GET/DELETE, book cover upload, book file upload, category image GET/DELETE, and file GET/DELETE. These are remnants of a Drizzle ORM migration that was paused. The corresponding Supabase Storage flows in lib/storage/handlers/ are also stubs. The active upload flows go through the book upload pipeline instead.
Backend Proxy
The single most important API route is POST /api/chat, which proxies chat messages to the Python backend. The flow is:
- Authenticates the user via
auth(). - Loads the session and character data from Supabase.
- Constructs the backend request body with
user_id,character_id,book_id,session_id, anduser_message. - Forwards the request to
POST /character/chaton the Python backend. - Streams the SSE response back to the client, parsing
token,done, anderrorevents.
This proxy pattern keeps the backend behind the frontend's auth layer: the Python backend never sees an unauthenticated request because it is not exposed to the public internet.
Routing and Layout
App router page tree, AppLayout shell, admin layout, public vs. authenticated routes, navigation sidebar and bottom-tab-bar.
Chat Pipeline
SSE streaming from the frontend: session lifecycle, message proxying, SSE parsing, user preferences per character, and how the frontend creates, lists, and manages chat sessions.