Skip to main content
Prism is a Next.js application built on three external services. Appwrite handles authentication and file storage, Qdrant stores and queries 768-dimensional vector embeddings, and Google Gemini provides embedding generation, multimodal analysis, and language model responses. Every feature in Prism — search, chat, recommendations — runs through some combination of these three.

Service map

Appwrite

Authentication (email/password + Google OAuth), file storage, and plan management via user labels

Qdrant

Vector database storing 768-dimensional embeddings for every document chunk, with cosine similarity search

Google Gemini

text-embedding-004 for embeddings, gemini-2.5-flash for chat, RAG responses, and image analysis

Data flows

Upload and indexing

When a user uploads a file, Prism runs the following pipeline:
1

Store the file

The client calls uploadDocument in lib/appwrite.ts, which writes the file to Appwrite Storage with permissions scoped to the uploading user (read, write, delete for user:{userId} only).
2

Download and extract text

The /api/documents/index route retrieves the file from Appwrite Storage using the server-side node-appwrite client. detectFileType identifies the format, then extractText in lib/document-processor.ts extracts the content — PDF via pdf2json, DOCX via mammoth, plain text for Markdown/TXT/code files, and Gemini Vision for images.
3

Chunk the text

chunkText splits the extracted text into overlapping segments: maxChunkSize=1000 characters and overlap=200 characters. The splitter prefers sentence and paragraph boundaries over hard character cuts. Markdown files use an additional header-aware path (splitByHeaders=true).
4

Generate embeddings

batchGenerateEmbeddings in lib/gemini.ts calls Gemini’s text-embedding-004 model on all chunks in parallel, processing up to 100 texts per batch. Each chunk produces a float[768] vector.
5

Index into Qdrant

batchIndexChunks in lib/qdrant.ts validates that every embedding is exactly 768 dimensions, assigns each chunk a UUID point ID, and upserts the batch into the prism_documents collection. The payload stored alongside each vector includes documentId, chunkIndex, chunkText, documentName, documentType, userId, uploadDate, and category.
1

Embed the query

The /api/search/semantic route receives the user’s query string and calls generateEmbedding to produce a 768-dimensional vector via Gemini’s text-embedding-004.
2

Search Qdrant

searchSimilarChunks queries the prism_documents collection filtered by userId (and optionally documentType), using cosine similarity with the query vector. The default score threshold for the search endpoint is 0.5.
3

Return grouped results

The route groups matching chunks by documentId and returns them sorted by the highest per-document score, along with the maxScore, documentName, documentType, and category for each document.

RAG chat

1

Embed the latest message

The /api/chat route embeds the last user message with Gemini text-embedding-004.
2

Retrieve context from Qdrant

searchSimilarChunks fetches up to 5 chunks for the user’s account with scoreThreshold=0.4. The lower threshold compared to standalone search ensures relevant context is included even when phrasing is indirect.
3

Inject context into the prompt

Each retrieved chunk is prepended to the user message in the format [Source N: {documentName}]\n{chunkText}. The full conversation history is preserved for multi-turn interactions.
4

Stream the Gemini response

generateChatResponse streams the response token-by-token via generateContentStream. The route encodes each token as a Server-Sent Event (data: {"chunk": "..."}\n\n). Source metadata is emitted first as data: {"sources": [...]}\n\n so the client can render citations before the answer arrives.

Collection structure

All vector data lives in a single Qdrant collection named prism_documents.
ParameterValue
Vector size768
Distance metricCosine
Indexing threshold10,000 points
Four payload fields are indexed as keywords to enable efficient filtered search:
FieldTypePurpose
userIdkeywordScopes every search and delete to the document owner
documentIdkeywordEnables chunk-level deletes when a file is removed
documentTypekeywordSupports document type filtering in search
categorykeywordSupports category-based filtering

Document chunking

All text content passes through chunkText in lib/document-processor.ts before embedding.
  • Max chunk size: 1,000 characters
  • Overlap: 200 characters (shared between adjacent chunks to preserve context across boundaries)
  • Boundary detection: the splitter looks for the last sentence boundary (. ) or paragraph break (\n) within the target window before making a cut, so chunks rarely end mid-sentence
  • Markdown: chunkMarkdown adds an extra header-aware pass that keeps section headings attached to their content
The overlap means consecutive chunks share 200 characters. This improves retrieval quality for queries that span a chunk boundary — both the preceding and following chunk contain enough context for the embedding model to produce a meaningful vector.

Environment variables

The following environment variables must be set for the three services to function:
# Appwrite (client-side)
NEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
NEXT_PUBLIC_APPWRITE_PROJECT_ID=<your-project-id>
NEXT_PUBLIC_APPWRITE_BUCKET_ID=<your-bucket-id>

# Appwrite (server-side)
APPWRITE_API_KEY=<server-side-api-key>
NEXT_PUBLIC_APPWRITE_DATABASE_ID=<your-database-id>
NEXT_PUBLIC_APPWRITE_CHAT_HISTORY_COLLECTION_ID=<your-collection-id>

# Qdrant
QDRANT_CLUSTER_URL=<your-cluster-url>
QDRANT_API_KEY=<your-api-key>

# Gemini
NEXT_PUBLIC_GEMINI_API_KEY=<your-gemini-api-key>
APPWRITE_API_KEY is a server-side secret used only in Next.js API routes. Never expose it to the browser. NEXT_PUBLIC_GEMINI_API_KEY is accessible client-side — restrict its usage in the Google AI Studio console to your deployment domain.

Build docs developers (and LLMs) love