Skip to main content
Teak follows a client-server architecture with multiple client applications (Web, Mobile, Desktop, Extension) connecting to a centralized Convex backend for real-time data synchronization and AI processing.

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                        Client Applications                       │
├──────────────┬──────────────┬──────────────┬───────────────────┤
│   Web App    │  Mobile App  │ Desktop App  │ Browser Extension │
│  (Next.js)   │   (Expo)     │   (Tauri)    │      (Wxt)        │
└──────┬───────┴──────┬───────┴──────┬───────┴───────┬───────────┘
       │              │              │               │
       └──────────────┴──────────────┴───────────────┘

              ┌────────────────────────┐
              │  ConvexClientProvider  │
              │ + QueryCacheProvider   │
              └────────────┬───────────┘

              ┌────────────────────────┐
              │    Convex Backend      │
              ├────────────────────────┤
              │  • Real-time Queries   │
              │  • Mutations           │
              │  • Actions             │
              │  • Workflows           │
              │  • File Storage        │
              └────────────┬───────────┘

              ┌────────────────────────┐
              │   Database Tables      │
              │  • cards               │
              │  • apiKeys             │
              │  • desktopAuthCodes    │
              │  • _storage (system)   │
              └────────────────────────┘

Client Applications

Teak provides multiple client surfaces for different use cases:

Web App

Next.js application with App Router and shadcn/ui components

Mobile App

Expo React Native app for iOS and Android

Desktop App

Tauri v2 application with Rust backend and React frontend

Browser Extension

Chrome extension built with Wxt for quick link saving

Real-time Data Synchronization

All client applications use Convex’s real-time data layer with optimized query caching:

ConvexQueryCacheProvider

The ConvexQueryCacheProvider from convex-helpers/react/cache wraps all client applications to provide:
  • Real-time updates: Changes propagate automatically to all connected clients
  • Optimistic updates: UI updates immediately while mutations process
  • Smart caching: Queries are cached and deduplicated across components
  • Automatic revalidation: Cache invalidates when underlying data changes
import { ConvexProvider } from "convex/react";
import { ConvexQueryCacheProvider } from "convex-helpers/react/cache";
import { ConvexClientProvider } from "@teak/convex";

function App({ children }) {
  return (
    <ConvexClientProvider>
      <ConvexQueryCacheProvider>
        {children}
      </ConvexQueryCacheProvider>
    </ConvexClientProvider>
  );
}

Query Pattern

Components use the useQuery hook to subscribe to real-time data:
import { useQuery } from "convex/react";
import { api } from "@teak/convex";

function CardsList() {
  const cards = useQuery(api.cards.list, { limit: 50 });
  
  if (!cards) return <LoadingSpinner />;
  
  return (
    <div>
      {cards.map(card => (
        <CardPreview key={card._id} card={card} />
      ))}
    </div>
  );
}

Mutation Pattern

Mutations trigger optimistic UI updates and real-time synchronization:
import { useMutation } from "convex/react";
import { api } from "@teak/convex";

function AddCardButton() {
  const createCard = useMutation(api.cards.create);
  
  const handleAdd = async () => {
    await createCard({
      content: "New idea",
      type: "text"
    });
    // UI updates automatically via real-time query subscription
  };
  
  return <button onClick={handleAdd}>Add Card</button>;
}

Authentication Flow

Teak uses Better Auth integrated with Convex:
┌──────────────┐
│   Client     │
└──────┬───────┘

       │ 1. Sign in/Sign up

┌──────────────────┐
│  Better Auth     │
│  (auth.ts)       │
└──────┬───────────┘

       │ 2. Create session

┌──────────────────┐
│ Session Token    │
└──────┬───────────┘

       │ 3. Include in requests

┌──────────────────┐
│ Convex Backend   │
│ (@convex-dev/    │
│  better-auth)    │
└──────┬───────────┘

       │ 4. Verify & authorize

┌──────────────────┐
│ Protected Query/ │
│ Mutation/Action  │
└──────────────────┘

Session Management

  • Better Auth manages user sessions and tokens
  • @convex-dev/better-auth integration validates sessions in Convex functions
  • Sessions automatically flow from client to backend
  • All queries/mutations have access to authenticated user context
import { mutation } from "./_generated/server";
import { getCurrentUser } from "@convex-dev/better-auth/convex";

export const createCard = mutation({
  args: { content: v.string(), type: cardTypeValidator },
  returns: v.id("cards"),
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    if (!user) throw new Error("Unauthorized");
    
    return await ctx.db.insert("cards", {
      userId: user.id,
      content: args.content,
      type: args.type,
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
  },
});

Convex Backend

The Convex backend (packages/convex/) provides:

Function Types

Read-only operations that return data. Queries are:
  • Automatically cached by the client
  • Real-time reactive (re-run when data changes)
  • Strongly typed with validators
export const list = query({
  args: { limit: v.optional(v.number()) },
  returns: v.array(cardValidator),
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    if (!user) return [];
    
    return await ctx.db
      .query("cards")
      .withIndex("by_user_deleted", (q) => 
        q.eq("userId", user.id).eq("isDeleted", false)
      )
      .order("desc")
      .take(args.limit ?? 50);
  },
});
Write operations that modify data. Mutations:
  • Run transactionally
  • Can schedule workflows and actions
  • Trigger automatic query revalidation
export const update = mutation({
  args: { id: v.id("cards"), content: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx);
    if (!user) throw new Error("Unauthorized");
    
    await ctx.db.patch(args.id, {
      content: args.content,
      updatedAt: Date.now(),
    });
    
    return null;
  },
});
Operations that can call external APIs and services. Actions:
  • Run in Node.js environment
  • Can make HTTP requests
  • Can call queries and mutations
  • Used for AI processing and external integrations
"use node";

import { action } from "./_generated/server";

export const generateSummary = action({
  args: { cardId: v.id("cards") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Call external AI service
    const card = await ctx.runQuery(internal.cards.get, { id: args.cardId });
    const summary = await callAIService(card.content);
    
    // Save result
    await ctx.runMutation(internal.cards.updateMetadata, {
      id: args.cardId,
      aiSummary: summary,
    });
    
    return null;
  },
});
Long-running multi-step processes with automatic retries. See Workflows for details.

File Storage

Convex provides built-in file storage via the _storage system table:
// Upload file
const storageId = await ctx.storage.store(blob);

// Get public URL (expires after 1 hour)
const url = await ctx.storage.getUrl(storageId);

// Get metadata from _storage table
const metadata = await ctx.db.system.get(storageId);
// Returns: { _id, _creationTime, contentType, sha256, size }
Storage URLs are signed and expire after 1 hour. Always generate fresh URLs when needed.

Import Patterns

Teak uses TypeScript path aliases for clean imports:
// API references
import { api } from "@teak/convex";
import { internal } from "@teak/convex";

// Types
import type { Doc, Id } from "@teak/convex/_generated/dataModel";

// Shared constants and utilities
import { CARD_TYPES } from "@teak/convex/shared/constants";
import { cardTypeValidator } from "@teak/convex/schema";

Card Types

Teak supports eight card types defined in schema.ts:packages/convex/schema.ts:6-15:
export const cardTypes = [
  "text",      // Plain text notes
  "link",      // URLs with metadata
  "image",     // Images with palette extraction
  "video",     // Videos with thumbnails
  "audio",     // Audio recordings
  "document",  // PDFs and documents
  "palette",   // Color palettes
  "quote",     // Quoted text
] as const;
Each type has different processing requirements handled by the AI Processing Pipeline.

Build docs developers (and LLMs) love