Skip to main content
Prism uses Appwrite for two core responsibilities: managing user identity (authentication and plan tiers) and storing document files. The client SDK (appwrite) runs in the browser for auth and file operations initiated by the user. The server SDK (node-appwrite) runs in Next.js API routes for privileged operations that need a full API key.
import { Client, Account, Storage } from 'appwrite';

const client = new Client()
  .setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://cloud.appwrite.io/v1')
  .setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '');

export const account = new Account(client);
export const storage = new Storage(client);

Authentication

Prism supports two sign-in methods, both backed by Appwrite’s Account service.
Registration calls account.create to create the user record, then immediately opens a session with account.createEmailPasswordSession. After a plan label is assigned (see Plan system), the session is deleted — users must sign in explicitly rather than being dropped straight into the app.
// Registration flow
const user = await account.create('unique()', email, password, name);
await account.createEmailPasswordSession(email, password);
await fetch('/api/users/assign-plan', { method: 'POST', body: JSON.stringify({ userId: user.$id }) });
await account.deleteSession('current');
Sign in is a single call:
await account.createEmailPasswordSession(email, password);

Plan system

Prism’s plan tiers are stored as Appwrite user labels. Labels are strings attached to the user object and readable via user.labels. The server-side /api/users/assign-plan route uses the node-appwrite SDK with a full API key to set labels — the client SDK cannot modify labels.

Plan tiers

export const PLAN_LIMITS = {
  free:       { storage: 5  * 1024 * 1024 * 1024, documents: 10,       displayStorage: '5 GB'      },
  pro:        { storage: 15 * 1024 * 1024 * 1024, documents: 500,      displayStorage: '15 GB'     },
  enterprise: { storage: Infinity,                documents: Infinity, displayStorage: 'Unlimited' },
  admin:      { storage: Infinity,                documents: Infinity, displayStorage: 'Unlimited' },
};
PlanStorageDocuments
free5 GB10
pro15 GB500
enterpriseUnlimitedUnlimited
adminUnlimitedUnlimited

Reading the plan

getUserPlan inspects user.labels and returns the highest matching tier. Labels are checked in priority order: adminenterpriseprofree. If the user has no plan label (for example, a legacy account), the function defaults to free.
export const getUserPlan = (
  user: Models.User<Models.Preferences> | null
): 'free' | 'pro' | 'enterprise' | 'admin' => {
  if (!user || !user.labels) return 'free';
  if (user.labels.includes('admin'))      return 'admin';
  if (user.labels.includes('enterprise')) return 'enterprise';
  if (user.labels.includes('pro'))        return 'pro';
  if (user.labels.includes('free'))       return 'free';
  return 'free';
};
getCurrentUser in lib/appwrite.ts also self-heals: if the returned user has no plan label, it calls /api/users/assign-plan automatically to assign the free label, then re-fetches the user.
Upgrading a user’s plan requires setting the appropriate Appwrite label on the user account via the server-side API. There is no client-facing upgrade flow built into lib/appwrite.ts — plan changes are an administrative operation.

File storage

All documents are stored in a single Appwrite Storage bucket identified by NEXT_PUBLIC_APPWRITE_BUCKET_ID. Each file gets per-user read, write, and delete permissions at upload time, so no user can access another user’s files even within the same bucket.

uploadDocument

async function uploadDocument(
  file: File,
  userId: string,
  onProgress?: (progress: number) => void
): Promise<Models.File>
Creates a unique file ID with ID.unique() and calls storage.createFile with explicit permission strings:
[
  `read("user:${userId}")`,
  `write("user:${userId}")`,
  `delete("user:${userId}")`
]
If onProgress is provided, it is called with a percentage (0–100) derived from chunksUploaded / chunksTotal. After upload completes, the caller is responsible for triggering indexing via /api/documents/index.

Other file operations

FunctionDescription
listDocumentsLists all files in the bucket visible to the current session
deleteDocumentDeletes the file from Storage, then calls /api/documents/delete to remove Qdrant chunks
getFilePreviewReturns a preview URL via storage.getFilePreview
downloadDocumentReturns a download URL via storage.getFileDownload
renameDocumentCalls /api/documents/rename (server-side rename via node-appwrite)
deleteDocument removes the file from Appwrite Storage first, then sends the Qdrant deletion request to /api/documents/delete. If the Qdrant call fails (for example, a network error), the file is gone from storage but its vector chunks remain in Qdrant. The mismatch is logged as a warning. See Qdrant integration — chunk deletion for details.

Session management

getCurrentUser calls account.get() to retrieve the active session’s user object. It returns null if no session exists — no error is thrown. Components use this to determine the authentication state on mount. Logging out deletes only the current session:
await account.deleteSession('current');
This leaves any other active sessions (for example, on other devices) intact.

Build docs developers (and LLMs) love