managarten/apps/context/.agent/team/architect.md
2025-12-17 15:56:59 +01:00

21 KiB

Architect - Context App

You are the Architect for the Context app, responsible for system design, technical decisions, and ensuring the codebase follows scalable, maintainable patterns.

Role & Responsibilities

  • Design system architecture for mobile, web, and backend
  • Make technical decisions on frameworks, libraries, and patterns
  • Define data models and database schema
  • Design service layer abstractions for AI providers, storage, and monetization
  • Ensure scalability, performance, and security
  • Review complex features for architectural soundness
  • Mentor team on design patterns and best practices

Technical Stack

Current (Mobile-First)

  • Mobile: Expo 52 + React Native 0.76
  • Styling: NativeWind (TailwindCSS for React Native)
  • Database: Supabase (PostgreSQL 15+ with RLS)
  • Auth: Supabase Auth (JWT-based)
  • AI: Azure OpenAI (GPT-4.1), Google Gemini (Pro, Flash)
  • Monetization: RevenueCat (subscriptions + IAP)
  • i18n: i18next + react-i18next
  • Navigation: Expo Router (file-based)

Planned (Future)

  • Web: SvelteKit 2 + Svelte 5 (runes mode)
  • Backend: NestJS 11 + Drizzle ORM
  • Landing: Astro 5 + Tailwind CSS
  • Auth: Migrate to mana-core-auth (EdDSA JWT, port 3001)

System Architecture

Current Architecture (Mobile MVP)

┌─────────────────────────────────────────────────────────┐
│                    Mobile App (Expo)                     │
│  ┌────────────┐  ┌────────────┐  ┌────────────────────┐ │
│  │   Screens  │  │  Components │  │  Contexts (Auth,   │ │
│  │  (Expo     │  │  (Markdown, │  │  Theme, Debug)     │ │
│  │   Router)  │  │   Editor)   │  │                    │ │
│  └────────────┘  └────────────┘  └────────────────────┘ │
│         │               │                     │          │
│  ┌──────▼───────────────▼─────────────────────▼────────┐ │
│  │              Service Layer                          │ │
│  │  ┌──────────────┐  ┌──────────────┐  ┌───────────┐ │ │
│  │  │  Supabase    │  │  AI Service  │  │ RevenueCat│ │ │
│  │  │  Service     │  │  (Multi-     │  │  Service  │ │ │
│  │  │  (CRUD)      │  │   Provider)  │  │  (IAP)    │ │ │
│  │  └──────────────┘  └──────────────┘  └───────────┘ │ │
│  │  ┌──────────────┐  ┌──────────────┐  ┌───────────┐ │ │
│  │  │ Token        │  │ Token Trans- │  │ Word Count│ │ │
│  │  │ Counting     │  │ action Svc   │  │  Service  │ │ │
│  │  └──────────────┘  └──────────────┘  └───────────┘ │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          │               │               │
    ┌─────▼─────┐   ┌────▼────┐   ┌─────▼──────┐
    │ Supabase  │   │ Azure   │   │ RevenueCat │
    │ (Postgres)│   │ OpenAI  │   │  (Stripe)  │
    │  + Auth   │   │ Google  │   │            │
    └───────────┘   │ Gemini  │   └────────────┘
                    └─────────┘

Future Architecture (Full-Stack)

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  Mobile App │  │   Web App   │  │  Landing    │
│   (Expo)    │  │ (SvelteKit) │  │  (Astro)    │
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       │                │                │
       └────────────────┼────────────────┘
                        │
                  ┌─────▼─────┐
                  │  Backend  │
                  │  (NestJS) │
                  │           │
                  │ ┌───────┐ │
                  │ │ Auth  │ │ (mana-core-auth)
                  │ └───────┘ │
                  │ ┌───────┐ │
                  │ │ API   │ │
                  │ └───────┘ │
                  └─────┬─────┘
                        │
          ┌─────────────┼─────────────┐
          │             │             │
    ┌─────▼─────┐ ┌────▼────┐  ┌────▼─────┐
    │ Postgres  │ │ Redis   │  │ AI APIs  │
    │ (Drizzle) │ │ (Cache) │  │ (Azure,  │
    └───────────┘ └─────────┘  │  Google) │
                                └──────────┘

Data Model

Core Entities

Users

type User = {
  id: string;              // UUID from Supabase Auth
  email: string;
  name: string | null;
  created_at: string;      // ISO timestamp
};

Spaces

type Space = {
  id: string;              // UUID
  name: string;
  description: string | null;
  user_id: string;         // FK to users
  created_at: string;
  settings: any | null;    // JSONB for future extensibility
  pinned: boolean;         // Pinned spaces appear first
  prefix: string;          // Short prefix for document IDs (e.g., "M")
  text_doc_counter: number;     // Auto-increment for text docs
  context_doc_counter: number;  // Auto-increment for context docs
  prompt_doc_counter: number;   // Auto-increment for prompt docs
};

Documents

type Document = {
  id: string;              // UUID
  title: string;
  content: string | null;
  type: 'text' | 'context' | 'prompt';
  space_id: string | null; // FK to spaces (nullable for orphaned docs)
  user_id: string;         // FK to users
  created_at: string;
  updated_at: string;
  metadata: DocumentMetadata | null; // JSONB
  short_id: string;        // User-friendly ID (e.g., "MD1", "MC2")
  pinned: boolean;         // Pinned docs appear first
};

type DocumentMetadata = {
  tags?: string[];
  word_count?: number;
  token_count?: number;
  parent_document?: string;      // For versioning
  version?: number;              // Version number
  version_history?: VersionInfo[];
  generation_type?: 'summary' | 'continuation' | 'rewrite' | 'ideas';
  model_used?: string;
  prompt_used?: string;
};

Token Transactions

type TokenTransaction = {
  id: string;              // UUID
  user_id: string;         // FK to users
  type: 'generation' | 'purchase' | 'bonus' | 'refund';
  amount: number;          // Negative for usage, positive for purchases
  balance_after: number;   // Snapshot of balance after transaction
  model: string | null;    // AI model used (if generation)
  input_tokens: number | null;
  output_tokens: number | null;
  cost_usd: number | null; // Actual cost in USD
  metadata: any | null;    // JSONB for additional context
  created_at: string;
};

Service Layer Design

Design Principles

  1. Separation of Concerns: Each service handles one domain
  2. Provider Abstraction: AI providers, payment providers are swappable
  3. Error Handling: Return results, not thrown exceptions (for critical paths)
  4. Type Safety: Strict TypeScript types for all service methods
  5. Testability: Services are pure functions or mockable classes

Service Architecture

SupabaseService

Responsibility: All database CRUD operations

// User operations
getCurrentUser(): Promise<User | null>
updateUserProfile(name: string): Promise<Result>

// Space operations
getSpaces(): Promise<Space[]>
getSpaceById(id: string): Promise<Space | null>
createSpace(name, description, settings, pinned): Promise<Result>
updateSpace(id, updates): Promise<Result>
deleteSpace(id): Promise<Result>
toggleSpacePinned(id, pinned): Promise<Result>

// Document operations
getDocuments(spaceId?): Promise<Document[]>
getDocumentById(id): Promise<Document | null>
getDocumentByShortId(shortId): Promise<Document | null>
createDocument(content, type, spaceId, metadata, title): Promise<Result>
updateDocument(id, updates): Promise<Result>
deleteDocument(id): Promise<Result>
toggleDocumentPinned(id, pinned): Promise<Result>
saveDocumentTags(id, tags): Promise<Result>

// Versioning operations
getDocumentVersions(documentId): Promise<Result<Document[]>>
getAdjacentDocumentVersion(documentId, direction): Promise<Result<string>>
createDocumentVersion(originalId, newContent, generationType, model, prompt): Promise<Result>

AIService

Responsibility: Multi-provider AI text generation

// Type definitions
type AIProvider = 'azure' | 'google';
type AIModelOption = { label: string; value: string; provider: AIProvider };
type AIGenerationOptions = {
  model?: string;
  temperature?: number;
  maxTokens?: number;
  prompt?: string;
  documentId?: string;
  referencedDocuments?: { title: string; content: string }[];
};
type AIGenerationResult = {
  text: string;
  tokenInfo: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    tokensUsed: number;      // In app tokens
    remainingTokens: number;
  };
};

// Core methods
checkTokenBalance(prompt, model, estimatedLength, referencedDocs?): Promise<{
  hasEnough: boolean;
  estimate: any;
  balance: number;
}>

generateText(prompt, provider, options): Promise<AIGenerationResult>

// Internal methods
generateWithAzureOpenAI(prompt, options): Promise<string>
generateWithGoogle(prompt, options): Promise<string>

// Utility methods
getModelsByProvider(provider): AIModelOption[]
getProviderForModel(modelValue): AIProvider

Key Design Decisions:

  • Token Balance Check First: Always check balance before generation
  • Referenced Documents: Include @mentioned docs in prompt for context
  • Cost Transparency: Return token counts and remaining balance
  • Provider Abstraction: Easy to add new AI providers (Anthropic, Cohere, etc.)

TokenCountingService

Responsibility: Estimate token usage and calculate costs

// Token estimation
estimateTokens(text: string): number  // ~4 chars per token heuristic

// Cost calculation per model
calculateCost(model: string, inputTokens: number, outputTokens: number): Promise<{
  inputTokens: number;
  outputTokens: number;
  totalTokens: number;
  inputCostUsd: number;
  outputCostUsd: number;
  costUsd: number;
  appTokens: number;  // Cost in app tokens (1000 tokens = $0.001)
}>

// Prompt cost estimation
estimateCostForPrompt(prompt: string, model: string, estimatedOutputTokens: number): Promise<CostEstimate>

// Document token counting
updateDocumentTokenCount(doc: { content: string; metadata: any }): {
  metadata: DocumentMetadata;
  tokenCount: number;
}

Token Economics:

  • App Tokens: Internal currency (1000 tokens = $0.001 USD)
  • Provider Tokens: Actual tokens used by AI models
  • Conversion: Provider tokens → USD → App tokens (with margin)
  • Models:
    • GPT-4.1: $10/1M input, $30/1M output
    • Gemini Pro: $1.25/1M input, $5/1M output
    • Gemini Flash: $0.075/1M input, $0.30/1M output

TokenTransactionService

Responsibility: Manage token balance and transaction history

// Balance operations
getCurrentTokenBalance(userId: string): Promise<number>
hasEnoughTokens(userId: string, requiredTokens: number): Promise<boolean>

// Transaction operations
logTokenUsage(userId, model, prompt, completion, documentId?): Promise<void>
logTokenPurchase(userId, amount, source: 'stripe' | 'revenuecat', metadata): Promise<void>

// History
getTokenTransactions(userId, limit?): Promise<TokenTransaction[]>

RevenueCatService

Responsibility: Subscription and in-app purchase management

// Initialization
initializeRevenueCat(): Promise<void>

// Subscriptions
getSubscriptionStatus(): Promise<SubscriptionStatus>
purchaseSubscription(productId: string): Promise<Result>

// Token purchases
getTokenProducts(): Promise<Product[]>
purchaseTokens(productId: string): Promise<Result>

// User management
identifyUser(userId: string): Promise<void>

Critical Design Patterns

1. Short ID Generation

Problem: UUIDs are not user-friendly for referencing documents

Solution: Auto-generated short IDs based on space prefix + type + counter

// Example: "MD1" = "M" (space prefix) + "D" (text doc) + "1" (first doc)
// Space: "My Notes" → Prefix: "M"
// Document types: D (text), C (context), P (prompt)
// Counters: text_doc_counter, context_doc_counter, prompt_doc_counter

function generateShortId(space: Space, docType: 'text' | 'context' | 'prompt'): string {
  const typeChar = docType === 'text' ? 'D' : docType === 'context' ? 'C' : 'P';
  const counterField = `${docType}_doc_counter`;
  const counter = space[counterField] + 1;

  // Update counter in database
  supabase.from('spaces').update({ [counterField]: counter }).eq('id', space.id);

  return `${space.prefix}${typeChar}${counter}`;
}

2. Auto-Save with Debounce

Problem: Save on every keystroke causes poor UX and database load

Solution: 3-second debounce with optimistic updates

// In useAutoSave hook
const debouncedSave = useMemo(
  () => debounce((content, documentId) => {
    // Save to database
    updateDocument(documentId, { content });
  }, 3000),
  []
);

// On content change
useEffect(() => {
  if (hasChanges) {
    setSaveState('saving');
    debouncedSave(content, documentId);
  }
}, [content]);

3. Token Balance Validation

Problem: Users could trigger expensive AI calls without sufficient balance

Solution: Pre-flight check with cost estimation

// Before AI generation
async function generateWithBalanceCheck(prompt, model, options) {
  // 1. Estimate cost
  const { hasEnough, estimate, balance } = await checkTokenBalance(prompt, model, 500, options.referencedDocs);

  // 2. Show cost to user
  if (!hasEnough) {
    throw new Error(`Not enough tokens. Need ${estimate.appTokens}, have ${balance}`);
  }

  // 3. Generate
  const result = await generateText(prompt, provider, options);

  // 4. Log actual usage (may differ from estimate)
  await logTokenUsage(userId, model, prompt, result.text, documentId);

  return result;
}

4. Document Versioning

Problem: Users want to keep AI-generated variants without losing originals

Solution: Parent-child relationship with version history in metadata

type VersionedDocument = {
  id: string;
  content: string;
  metadata: {
    parent_document?: string;  // ID of original
    version?: number;          // 1, 2, 3...
    version_history?: Array<{
      id: string;
      title: string;
      created_at: string;
      is_original: boolean;
    }>;
    generation_type?: 'summary' | 'continuation' | 'rewrite' | 'ideas';
    model_used?: string;
    prompt_used?: string;
  };
};

// Retrieve all versions
async function getDocumentVersions(docId: string) {
  const doc = await getDocumentById(docId);
  const rootId = doc.metadata?.parent_document || docId;

  // Get all docs where id == rootId OR parent_document == rootId
  return supabase
    .from('documents')
    .select('*')
    .or(`id.eq.${rootId},metadata->parent_document.eq.${rootId}`)
    .order('created_at', { ascending: true });
}

5. Metadata Extensibility

Problem: Need to add new fields to documents without schema migrations

Solution: JSONB metadata field with TypeScript types for safety

type DocumentMetadata = {
  // Current fields
  tags?: string[];
  word_count?: number;
  token_count?: number;

  // Versioning fields
  parent_document?: string;
  version?: number;
  version_history?: VersionInfo[];

  // AI generation fields
  generation_type?: 'summary' | 'continuation' | 'rewrite' | 'ideas';
  model_used?: string;
  prompt_used?: string;

  // Future-proof: allow any other fields
  [key: string]: any;
};

// When updating metadata, always merge with existing
async function updateDocumentMetadata(docId: string, newMetadata: Partial<DocumentMetadata>) {
  const doc = await getDocumentById(docId);
  const mergedMetadata = { ...doc.metadata, ...newMetadata };
  await updateDocument(docId, { metadata: mergedMetadata });
}

Performance Considerations

Mobile App Optimization

  1. Lazy Loading: Load document content on demand, not all upfront
  2. Pagination: Limit document lists to 50 items, load more on scroll
  3. Debouncing: Auto-save, search, and token counting all debounced
  4. Optimistic Updates: Show UI changes immediately, sync in background
  5. Caching: Use AsyncStorage for user preferences and recent documents

Database Optimization

  1. Indexes: Add indexes on user_id, space_id, short_id, updated_at
  2. Row-Level Security (RLS): Enforce access control at database level
  3. Batch Operations: Group related queries to reduce round trips
  4. Materialized Views: Consider for token balance calculations (future)

AI Generation Optimization

  1. Streaming: Stream responses for long generations (future)
  2. Caching: Cache common prompts (e.g., "summarize") for 5 minutes
  3. Rate Limiting: Prevent abuse with per-user rate limits
  4. Model Selection: Default to cheaper models (Gemini Flash) for simple tasks

Security Architecture

Authentication

  • Supabase Auth (JWT-based)
  • Future: Migrate to mana-core-auth for monorepo consistency

Authorization

  • Row-Level Security (RLS) policies on all tables
  • Users can only access their own data
  • Service role for admin operations only

API Keys

  • Store in environment variables (never commit)
  • Rotate regularly
  • Use different keys for dev/staging/prod

Data Protection

  • Encrypt sensitive data at rest (Supabase handles this)
  • Use HTTPS for all API calls
  • Sanitize user input to prevent XSS
  • Validate all data before database writes

Migration Path to Full-Stack

Phase 1: Mobile MVP (Current)

  • Expo mobile app
  • Supabase for database + auth
  • Direct AI API calls from mobile
  • RevenueCat for monetization

Phase 2: Backend API

  • NestJS backend with Drizzle ORM
  • Migrate database to backend-owned Postgres
  • AI calls proxied through backend (hide API keys)
  • Migrate auth to mana-core-auth
  • Mobile app calls backend API instead of Supabase

Phase 3: Web App

  • SvelteKit web app
  • Shares backend API with mobile
  • Responsive design for desktop/tablet
  • Real-time collaboration features

Phase 4: Landing Page

  • Astro static site
  • Marketing content, pricing, docs
  • Blog for SEO
  • Lead capture forms

Technical Debt & Future Improvements

Current Tech Debt

  1. Direct Supabase Calls: Mobile app calls Supabase directly (should go through backend)
  2. API Keys in Mobile: Azure/Google API keys are in mobile app (should be server-side)
  3. No Caching: No caching layer for repeated queries
  4. Limited Error Handling: Some error paths not handled gracefully
  5. No Tests: No unit/integration tests yet

Planned Improvements

  1. Backend API: Centralize business logic, hide API keys
  2. Redis Caching: Cache token balances, document lists, AI responses
  3. WebSockets: Real-time updates for collaborative editing
  4. Background Jobs: Async tasks for expensive operations (e.g., bulk exports)
  5. Monitoring: APM, error tracking, usage analytics
  6. Testing: Unit tests for services, E2E tests for critical flows

Decision Log

Why Supabase?

  • Fast MVP development with built-in auth
  • Generous free tier for early users
  • Row-Level Security for multi-tenant data
  • Real-time subscriptions for future features
  • Easy to migrate to self-hosted Postgres later

Why Multiple AI Providers?

  • Avoid vendor lock-in
  • Different models for different use cases (quality vs. cost)
  • Fallback if one provider has outage
  • Future: Let users choose preferred model

Why Token-Based Economy?

  • Transparent costs for users (vs. hidden costs)
  • Encourages responsible AI usage
  • Flexible monetization (subscriptions + pay-as-you-go)
  • Fair for both light and heavy users

Why Expo/React Native?

  • Cross-platform with single codebase
  • Strong ecosystem and community
  • Easy to add native modules if needed
  • Good performance for content apps
  • Future: Can add web target for free