A help center built with TanStack Start and Appwrite VectorsDB. Readers ask a question in their own words and answers are ranked by meaning rather than by keyword, so "my card got rejected" finds the article titled "What to do when a card payment is declined".
Companion repo for the tutorial on the Appwrite blog.
- Stores each article as one VectorsDB document: an
embeddingsvector plus ametadataobject holding the title, body, category, and timestamp. - Generates embeddings with Appwrite's built-in
bge-smallmodel, so no external embedding provider is needed. - Ranks search results with a
vectorCosinequery and shows the returned$distanceas a similarity on a fixed scale. - Suggests related articles on each article page using the same query.
- Gates authoring behind an Appwrite email and password session, created server side so the session secret never reaches the browser.
- Re-embeds an article on every save, so an edit updates the ranking too.
- TanStack Start (React 19, Vite)
node-appwriteserver SDK- Tailwind CSS v4
In the Appwrite Console, create:
-
A project. Copy the Project ID and API endpoint from the overview page.
-
A VectorsDB database. Note its ID.
-
A collection inside it named
Articles, with the bge-small embedding model selected. The model sets the dimension, so every vector is 384 components. -
A user to sign in with, under Auth.
-
An API key with these scopes:
Scope Used for embeddings.writeTurning text into vectors documents.readReading and searching articles documents.writePublishing, editing, and deleting articles collections.writeCreating the vector index sessions.writeSigning an author in from the server
Then create the HNSW index. The index type must match the query metric, so a
cosine query needs hnsw_cosine:
import { Client, VectorsDB, VectorsDBIndexType } from 'node-appwrite'
const client = new Client()
.setEndpoint('https://fra.cloud.appwrite.io/v1')
.setProject('<PROJECT_ID>')
.setKey('<YOUR_API_KEY>')
await new VectorsDB(client).createIndex({
databaseId: '<DATABASE_ID>',
collectionId: '<COLLECTION_ID>',
key: 'embeddings_cosine',
type: VectorsDBIndexType.HnswCosine,
attributes: ['embeddings'],
})pnpm install
cp .env.example .env
# edit .env with your project values
pnpm seed # publishes ten starter articles
pnpm devThe app runs on http://localhost:3100. Authoring is at /admin.
See .env.example. Every value is read on the server only, so none are exposed
to the browser and APPWRITE_API_KEY stays secret.
| Variable | Example |
|---|---|
APPWRITE_ENDPOINT |
https://fra.cloud.appwrite.io/v1 |
APPWRITE_PROJECT_ID |
your project ID |
APPWRITE_API_KEY |
your API key |
APPWRITE_DATABASE_ID |
your VectorsDB database ID |
APPWRITE_COLLECTION_ID |
your collection ID |
SESSION_SECRET |
a long random string for cookie sealing |
src/
lib/
config.ts Connection details and the embedding model
vectors.server.ts Embeddings, search, and article CRUD
auth.server.ts Server-side sign in and session reads
routes/
index.tsx Search page and category listing
article.$id.tsx Article page with related answers
admin.tsx Sign in and the authoring screen
components/
ProximityRuler.tsx Plots each result on a fixed similarity scale
scripts/
seed.mjs Publishes the starter articles
Push this repo to GitHub, then in your Appwrite project open Sites, choose Create site, and connect the repository. Pick TanStack Start as the framework and confirm the build settings:
| Setting | Value |
|---|---|
| Install command | npm install |
| Build command | npm run build |
| Output directory | ./dist |
Add every variable from the table above under the site's environment variables, then deploy.
| Command | What it does |
|---|---|
pnpm dev |
Start the dev server on port 3100 |
pnpm build |
Build for production |
pnpm start |
Serve the production build |
pnpm seed |
Publish the starter articles |
pnpm check |
Type-check with tsc --noEmit |
MIT.