Studalis is a document-based AI study assistant built with Next.js, Prisma, Clerk authentication, AWS S3, and Google Gemini. The app lets users upload PDFs, extract text, create vector embeddings for retrieval, and ask AI-powered study questions about the document.
This project is organized around a simple core flow:
- A user uploads a PDF to S3.
- The backend creates a database record for the document.
- The PDF is downloaded and parsed.
- The extracted text is split into chunks and embedded.
- The user asks study questions through chat or explain actions.
- The system retrieves relevant document chunks and passes them to Gemini with a prompt.
- The answer and interaction are saved in the database.
- App pages in app
- Reusable UI in components
- Client state in store
- React Query data hooks in lib/ReactQueries
- Next.js route handlers in app/api
- Shared business logic and AI helpers in lib
- PostgreSQL database access through Prisma in lib/prisma.ts
- AWS S3 for uploaded PDFs and processed JSON output
- PostgreSQL for documents, sessions, messages, chunks, and learning interactions
- Google Gemini for embeddings and answer generation
Below is the exact API surface of the project and the files that use them.
Creates a presigned S3 upload URL and inserts a new document row in the database with status "processing".
- fileName: original file name
- fileType: content type
- Authenticates the user with Clerk.
- Builds an S3 key like uploads/-.
- Generates a signed PUT URL using AWS S3.
- Creates a Prisma document row with title, pdfKey, and processing status.
- Returns the document, uploadUrl, and key to the frontend.
- S3 client from lib/s3.ts
- Prisma client from lib/prisma.ts
Returns a temporary signed URL that the browser can use to fetch an object from S3.
- key: S3 object key
The client cannot directly access private S3 objects without a signed URL, so this route acts as a secure proxy.
Downloads a PDF from S3, extracts text page by page, splits into chunks, creates embeddings, saves chunk rows to PostgreSQL, and marks the document as ready.
- key: uploaded S3 key
- documentId: id of the Prisma document row
- Authenticates the user.
- Converts the uploaded key into a processed JSON file key.
- Fetches the PDF buffer from S3 via lib/get-pdf-buttfer.ts.
- Extracts the text via lib/s3-extract.ts.
- Splits each page into text segments using lib/chunk-text.ts.
- Creates embeddings for each chunk using lib/embeddings.ts.
- Saves the chunks and their vector data into the DocumentChunk table.
- Saves the full extracted JSON to S3.
- Updates the document status to ready.
- lib/s3.ts
- lib/get-pdf-buttfer.ts
- lib/s3-extract.ts
- lib/chunk-text.ts
- lib/embeddings.ts
- lib/prisma.ts
Returns the user’s document list ordered newest first.
- lib/ReactQueries/getDocument.tsx via the hook useAllUserDocuments
- dashboard pages that show uploaded study documents
Returns the metadata of one document if it belongs to the current user.
- lib/ReactQueries/getDocument.tsx via useDocument
Returns all chunk rows for a given document ordered by page and chunk index.
- lib/ReactQueries/getDocument.tsx via useDocumentChunks
This gives the app direct access to the raw document text segments used during retrieval and study interactions.
Returns a temporary S3 signed URL for the PDF associated with a document.
- lib/ReactQueries/getDocument.tsx via useDocumentUrl
- components/dashboard/PdfViewer.tsx indirectly through the fetched file URL
Ensures a study session exists for the current user and document.
- documentId
- lib/ReactQueries/useSession.tsx via useCreateSession
Loads the study session associated with a document and user.
- lib/ReactQueries/useSession.tsx via useSession
Returns all chat messages for a session in chronological order.
This route expects a sessionId in the path param, but the folder is named [documentId]. In practice, the frontend uses the session id as the route parameter; this is a mismatch in naming, but the logic still fetches by session id.
This is the main AI chat endpoint. It checks auth, verifies document ownership, loads or creates a session, stores the user message, retrieves the most relevant chunks, builds a study prompt, asks Gemini for a response, saves the assistant answer, and updates the study interaction data.
- documentId
- message
- action (default: CHAT)
- CHAT
- EXPLAIN
- NOTE
- FLASHCARD
- QUIZ
- SUMMARY
- lib/sessions/get-or-create-session.ts
- lib/retrieval/search-similar-chunks.ts
- lib/prompts/build-study-prompts.ts
- lib/prisma.ts
- Prisma document validation
- getOrCreateSession
- save user message
- vector search using document chunks
- buildStudyPrompt
- Gemini generateContent
- save assistant message
- optionally create LearningInteraction
- update studySession lastOpenedAt
Answers a question or highlight explanation using the most relevant document chunks. It is optimized for “explain this concept” interactions.
- documentId
- question
- lib/sessions/interaction.ts via useExplainHighlight
- components/dashboard/PdfViewer.tsx
- lib/retrieval/search-similar-chunks.ts
- lib/sessions/get-or-create-session.ts
- Prisma learning interaction records
- components/dashboard/UploadFile.tsx
- POST to app/api/upload-url/route.ts
- POST to app/api/files/view/route.ts
- POST to app/api/files/process/route.ts
- components/dashboard/PdfViewer.tsx
- Uses the explain mutation from lib/sessions/interaction.ts
- Calls app/api/explain/route.ts
- Reads file URL from signed document view logic
- components/chat/StudyChatUI.tsx
- Sends POST requests to app/api/chat/route.ts
- Loads session history from app/api/sessions/[documentId]/messages/route.ts
- lib/ReactQueries/getDocument.tsx
- Reads document metadata and chunks
- lib/ReactQueries/useSession.tsx
- Reads and creates session state
- lib/ReactQueries/useSessionMessages.tsx
- Fetches message history
Purpose: Creates the shared Prisma database client used across the app.
What it does:
- Loads DATABASE_URL from environment variables
- Builds a PrismaPg adapter for PostgreSQL
- Instantiates PrismaClient
- Exports prisma for all database access
Used by:
- Almost every route in app/api
- Most retrieval and session functions
Why it matters: This is the application’s database connection layer. Without it, routes cannot query documents, sessions, messages, or chunks.
Purpose: Utility for extracting text content from AI message parts.
What it does:
- Accepts a UIMessage from the AI SDK
- Filters message parts where type === "text"
- Joins them into a single string
Used by:
- Not actively used in the current project flow as a central integration point, but it is a generic helper for AI message formatting.
Purpose: Splits long document text into smaller overlapping chunks.
What it does:
- Takes a large string and slices it into smaller windows
- Uses a default chunk size of 1000 and overlap of 200 characters
- Moves the start position forward by chunkSize - overlap to preserve context continuity
Used by:
Why it matters: The vector search system works best when content is chunked. This makes large PDFs searchable and context-aware.
Purpose: Generates vector embeddings for text using Gemini.
What it does:
- Initializes Gemini client with the API key
- Calls ai.models.embedContent with model gemini-embedding-001
- Returns the embedding vector values
Used by:
Why it matters: This is the bridge between raw text and semantic retrieval. The database stores these vectors and compares them to user queries.
Purpose: Downloads a PDF object from S3 as a buffer.
What it does:
- Creates a GetObjectCommand for the pdf key
- Sends the command to S3
- Streams the returned body into a Buffer
Used by:
Why it matters: The app needs the raw PDF bytes before it can parse and extract text.
Purpose: Shared AWS S3 client configuration.
What it does:
- Creates an S3Client with AWS region and credentials
- Exposes it as the app-wide S3 client instance
Used by:
- app/api/upload-url/route.ts
- app/api/files/view/route.ts
- app/api/files/process/route.ts
- lib/get-pdf-buttfer.ts
Why it matters: This centralizes AWS configuration and avoids redefining the S3 client in many files.
Purpose: Extracts clean text from a PDF using pdf.js.
What it does:
- Configures the pdf.js worker for Node.js
- Opens the PDF document from a binary buffer
- Iterates page by page
- Gets text items from each page
- Joins them without losing structure
- Cleans whitespace and punctuation
- Returns an object with totalPages and page-level cleaned text
Used by:
Why it matters: This is the text extraction layer that turns PDF content into searchable document text.
Purpose: Performs semantic retrieval using vector similarity.
What it does:
- Converts a prompt or question into an embedding using Gemini
- Builds a raw SQL vector query against the DocumentChunk table
- Finds the closest matching chunks for a particular document
- Returns the most relevant chunks with distance metrics
Used by:
Why it matters: This is the core retrieval engine. It lets the assistant answer from the user’s document instead of relying only on general knowledge.
Purpose: Ensures each user-document pair has a study session.
What it does:
- Tries to find an existing studySession row
- If one exists, updates lastOpenedAt
- If one does not exist, creates a new session
Used by:
Why it matters: Sessions keep all conversation and learning interactions grouped by document.
Purpose: Builds the final prompt sent to Gemini based on the selected study action.
What it does:
- Accepts action, context, and student message
- Wraps the document context and request in a prompt template
- Generates different prompt styles for CHAT, EXPLAIN, NOTE, FLASHCARD, QUIZ, and SUMMARY
Used by:
Why it matters: This is how the app shapes the AI behavior for different educational tasks and keeps the responses consistent.
Purpose: Client-side data hooks for document queries.
Functions:
- getDocument
- getDocumentChunks
- getAllUserDocuments
- getDocumentUrl
- useDocument
- useDocumentChunks
- useAllUserDocuments
- useDocumentUrl
What they do:
- Fetch document metadata from the API
- Fetch all chunks for a document
- Fetch all docs for the current user
- Fetch the document’s PDF signed URL
- Wrap the fetchers in React Query hooks for caching and state management
Used by:
- Dashboard/document pages
Purpose: Hooks for reading and creating sessions.
Functions:
- getSession
- createSession
- updateCurrentPage
- useSession
- useCreateSession
- useUpdateCurrentPage
Used by:
- App session UI and page state management
Notes: The updateCurrentPage mutation points to an API path that does not currently appear in the server routes folder, so it appears to be partially implemented or planned.
Purpose: Fetches the message history for a session.
What it does:
- Calls GET /api/sessions/:sessionId/messages
- Returns JSON containing sessionId and messages
- Enables the query only when sessionId is present
Used by:
Purpose: Client-side API helper for highlighted text explanations.
What it does:
- Calls POST /api/explain
- Sends documentId and selected question text
- Uses a mutation wrapper for UI interaction
Used by:
Purpose: Shared utility helper for class name merging.
What it does:
- Merges TailwindCSS class names using clsx and tailwind-merge
Used by:
- UI components throughout the app
- User uploads a PDF from components/dashboard/UploadFile.tsx
- The frontend requests a signed upload URL from app/api/upload-url/route.ts
- The PDF is uploaded to S3
- The frontend calls app/api/files/process/route.ts
- The PDF is parsed and chunked
- Embeddings are created and stored in PostgreSQL
- The document becomes ready
- The user opens the document and interacts with chat or explain features
- The system retrieves the best matching chunks for the user prompt
- Gemini answers using the document context
- The answer and interaction are saved to the database
The app relies on these main Prisma models, all connected through document and session flows:
- Document
- DocumentChunk
- StudySession
- ChatMessage
- LearningInteraction
These models are used to:
- track uploaded files
- preserve extracted text chunks
- organize study sessions
- store the dialogue history
- track actions such as EXPLAIN, SUMMARY, NOTE, FLASHCARD, and QUIZ
The app depends on environment configuration such as:
- DATABASE_URL
- GEMINI_API_KEY
- AWS_REGION
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_BUCKET_NAME
Without these values, the upload, embedding, and AI answer flows will not work correctly.
This codebase is a working prototype and has a few implementation details worth noting:
- Some API routes are intentionally thin wrappers around database and S3 logic.
- Retrieval is semantic and document-aware, based on vector similarity.
- The app uses Next.js App Router route handlers rather than a separate backend service.
- Some session route naming is inconsistent, especially the folder app/api/sessions/[documentId]/messages/route.ts, where the route param is actually a session id.
- The file name lib/get-pdf-buttfer.ts contains a typo, but the function still works as intended.
The project’s core architecture is:
- S3 stores PDF files and processed outputs
- Prisma stores user, document, session, chunk, and interaction data
- Gemini handles embeddings and text generation
- Next.js route handlers orchestrate the work
- React Query hooks provide a frontend API layer
- UI components trigger the routes and display results
This means the app is effectively a document retrieval + tutoring system: it does not just generate AI answers from general knowledge, it grounds the conversation in the user’s uploaded file.