# Backend Plan — Adiuva Cloud API > **Separate repository.** This document defines the FastAPI backend that the Electron app communicates with. > > The backend owns: orchestration logic, chat agent intelligence, prompt IP, auth, billing, E2E backup blob storage, cloud storage (encrypted blobs), cloud vector store, and plugin marketplace. > The backend NEVER persists user data in plaintext. Cloud storage blobs are E2E encrypted before upload — the backend only verifies integrity, never decrypts. --- ## Project Structure ``` adiuva-api/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI entry + CORS + lifespan + router includes │ ├── core/ │ │ ├── __init__.py │ │ ├── agent_registry.py # Base classes + singleton registry │ │ ├── orchestrator.py # LLM-based intent router │ │ ├── execution_plan.py # Plan builder + cache │ │ └── plugin_loader.py # Dynamic agent loading │ ├── agents/ # Chat agents (proprietary logic + prompts) │ │ ├── __init__.py # Auto-registers all agents │ │ ├── task_agent.py │ │ ├── calendar_agent.py │ │ ├── email_agent.py │ │ └── analytics_agent.py │ ├── api/ │ │ ├── __init__.py │ │ ├── routes/ │ │ │ ├── __init__.py │ │ │ ├── chat.py # POST /chat + WS /chat/stream │ │ │ ├── plans.py # GET /plans/playbook │ │ │ ├── storage.py # CRUD cloud storage (E2E encrypted blobs) │ │ │ ├── vectors.py # Upsert/search cloud vector store │ │ │ ├── backup.py # PUT/GET /backup │ │ │ ├── plugins.py # Plugin marketplace │ │ │ ├── auth.py # Register/login/refresh │ │ │ └── billing.py # Checkout/webhook/subscription │ │ └── middleware/ │ │ ├── __init__.py │ │ ├── auth.py # JWT validation │ │ ├── rate_limit.py # Tier-aware rate limiting │ │ └── sanitizer.py # Strip prompt metadata from responses │ ├── storage/ │ │ ├── __init__.py │ │ ├── blob_store.py # S3 for E2E encrypted blobs │ │ ├── vector_store.py # Cloud vector store (Pinecone/Qdrant) │ │ └── encryption.py # Integrity verification only — NO decryption │ ├── marketplace/ │ │ ├── __init__.py │ │ ├── plugin_registry.py # Plugin catalog (metadata, versions, ratings) │ │ ├── plugin_review.py # Review queue + approval workflow │ │ └── revenue_share.py # 70/30 split tracking with Stripe Connect │ ├── billing/ │ │ ├── __init__.py │ │ ├── stripe_service.py # Stripe checkout + webhooks │ │ └── tier_manager.py # Feature matrix per tier │ └── config/ │ ├── __init__.py │ └── settings.py # Pydantic BaseSettings (env-based) ├── tests/ │ ├── __init__.py │ ├── conftest.py # Fixtures: test client, mock agents, mock LLM │ ├── test_orchestrator.py │ ├── test_agents.py │ ├── test_auth.py │ ├── test_backup.py │ ├── test_storage.py │ └── test_plugins.py ├── alembic/ # DB migrations (auth/billing/marketplace tables only) │ ├── alembic.ini │ └── versions/ ├── requirements.txt ├── Dockerfile ├── docker-compose.yml # App + PostgreSQL + Redis (dev) ├── .env.example └── README.md ``` --- ## Step-by-Step Implementation ### Step 1 — Project scaffolding ✅ - [x] Initialize repo with the directory structure above - [x] Write `requirements.txt`: ``` fastapi>=0.115.0 uvicorn[standard]>=0.34.0 langchain>=0.3.0 langchain-openai>=0.3.0 pydantic>=2.10.0 python-jose[cryptography]>=3.3.0 stripe>=11.0.0 boto3>=1.35.0 slowapi>=0.1.9 sqlalchemy>=2.0.0 asyncpg>=0.30.0 alembic>=1.14.0 bcrypt>=4.2.0 python-dotenv>=1.0.0 httpx>=0.28.0 websockets>=14.0 pytest>=8.0.0 pytest-asyncio>=0.24.0 ``` - [x] Write `app/main.py`: FastAPI app with CORS (allow `app://`, `http://localhost:*`), lifespan (init DB pool, init agent registry), include all routers under `/api/v1` - [x] Write `app/config/settings.py`: `Settings(BaseSettings)` with fields: `DATABASE_URL`, `JWT_SECRET`, `JWT_ALGORITHM` (default HS256), `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `S3_BUCKET`, `S3_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `OPENAI_API_KEY`, `CORS_ORIGINS`, `ENV` (dev/prod), `PINECONE_API_KEY`, `PINECONE_INDEX`, `QDRANT_URL`, `QDRANT_API_KEY` - [x] Write `Dockerfile`: Python 3.12 slim, multi-stage (builder + runtime), non-root user - [x] Write `docker-compose.yml`: app, postgres:16, optional redis - [x] Write `.env.example` - **Outcome:** Runnable FastAPI skeleton (returns 404 on all routes). ### Step 2 — Pydantic schemas (API contracts) ✅ - [x] Create `app/schemas.py` (mirrors `src/shared/api-types.ts` from Electron repo): - `ChatRequest`: `message: str`, `context: ChatContext`, `execution_mode: Literal['direct', 'plan']` - `ChatContext`: `user_profile: dict`, `relevant_documents: list[str]`, `recent_tasks: list[dict]`, `conversation_history: list[dict]` - `ChatResponse`: `response: str`, `actions: list[PlanAction]` - `PlanAction`: `type: Literal['create_record', 'update_record', 'delete_record', 'index_document', 'send_notification', 'call_agent']`, `table: str | None`, `data: dict | None`, `agent: str | None` - `ExecutionPlan`: `agent: str`, `steps: list[PlanStep]` - `PlanStep`: `action: str`, `prompt_template: str | None`, `variables: dict | None`, `data_from_step: int | None` - `BackupMetadata`: `version: int`, `timestamp: int`, `checksum: str`, `chunk_count: int` - `BillingTier`: `Literal['free', 'pro', 'power', 'team']` - `AuthTokens`: `access_token: str`, `refresh_token: str`, `expires_at: int` - `UserProfile`: `id: str`, `email: str`, `tier: BillingTier` - `StorageRecord`: `id: str`, `user_id: str`, `table: str`, `blob: bytes`, `checksum: str`, `created_at: int`, `updated_at: int` — blob is always E2E encrypted by client - `StorageRecordCreate`: `table: str`, `blob: bytes`, `checksum: str` - `StorageRecordUpdate`: `blob: bytes`, `checksum: str` - `VectorUpsertRequest`: `vectors: list[VectorItem]` - `VectorItem`: `id: str`, `blob: bytes`, `checksum: str` — vector + metadata encrypted by client - `VectorSearchRequest`: `query_blob: bytes`, `top_k: int = 10` - `VectorSearchResponse`: `results: list[VectorSearchResult]` - `VectorSearchResult`: `id: str`, `score: float`, `blob: bytes` - `PluginManifest`: `id: str`, `name: str`, `description: str`, `version: str`, `author: str`, `permissions: list[str]`, `category: str`, `price_cents: int = 0` - `PluginListResponse`: `plugins: list[PluginManifest]`, `total: int`, `page: int` - `PluginInstallRequest`: `plugin_id: str` - **Outcome:** All request/response models defined and validated. ### Step 3 — Agent Registry + base classes ✅ - [x] `app/core/agent_registry.py`: - `BaseAgent(ABC)`: - `user_id: str`, `shared_memory: dict`, `vector_store_context: list[str]`, `skills: list[str]` - Abstract `get_name() -> str`, `get_description() -> str` - `ChatAgent(BaseAgent)`: - Abstract `async handle(query: str, context: dict) -> str` - Abstract `get_tools() -> list` (LangChain tool definitions) - Concrete `_tool_loop(llm, messages, tools, max_iter=5) -> str` — shared tool-calling loop - `AgentRegistry` (singleton): - `_agents: dict[str, ChatAgent]` - `register(agent_class)` — decorator pattern - `get(name) -> ChatAgent` - `list_agents() -> list[dict]` — returns `[{name, description}]` for orchestrator prompt - `async call_agent(name, query, context) -> str` — for inter-agent calls - [x] Unit tests: register, get, list, call_agent with mock - **Outcome:** Pluggable agent framework. ### Step 4 — Orchestrator ✅ - [x] `app/core/orchestrator.py`: - `async classify_intent(message, context, registry) -> str`: - System prompt: "You are an intent classifier. Given the user message and context, decide which agent to route to. Available agents: {registry.list_agents()}. Respond with just the agent name." - Uses gpt-4o-mini via LangChain for low latency - Falls back to `task_agent` if no clear match - `async route_single(agent_name, message, context) -> ChatResponse`: - Instantiates agent from registry - Calls `agent.handle(message, context)` - Returns response + any actions the agent produced - `async route_pipeline(agent_names, message, context) -> ChatResponse`: - Executes agents in sequence - Each agent receives `{...context, previous_results: [...]}` - Final synthesis via LLM: "Summarize these agent results into a coherent response" - `async orchestrate(request: ChatRequest) -> ChatResponse | ExecutionPlan`: - Main entry point - Context is transparent to orchestrator — data may originate from local or cloud storage on the client side - Classifies intent - If `execution_mode == 'direct'`: route + return response - If `execution_mode == 'plan'`: route + return execution plan with template IDs - `async orchestrate_stream(request: ChatRequest) -> AsyncGenerator[str, None]`: - Same as orchestrate but yields tokens for WebSocket streaming - [x] Integration tests with mocked LLM and mocked agents - **Outcome:** Intelligent routing with single-agent and pipeline modes. ### Step 5 — Execution Plan generator ✅ - [x] `app/core/execution_plan.py`: - `PromptTemplateRegistry`: dict of `template_id -> prompt_text`. Templates are server-side only — client receives IDs. - `ExecutionPlanBuilder`: - `add_step(action, params) -> self` - `add_llm_step(template_id, variables) -> self` - `add_data_step(action, data_from_step) -> self` - `build() -> ExecutionPlan` — validates step references - `PlanCache`: - In-memory LRU (maxsize=1000) - `cache_plan(key, plan)`, `get_plan(key)`, `get_all_playbooks() -> list[ExecutionPlan]` - Playbooks are pre-built plans for common operations (e.g., "create task from email", "generate weekly report") - **Outcome:** Plans are cacheable as playbooks. Prompt IP never leaves the server. ### Step 6 — Chat Agents ✅ - [x] `app/agents/task_agent.py` — `@registry.register`: - Description: "Manages tasks and comments: list, create, update, delete, due-today, comments" - Tools (8): `list_tasks(project_id, status, search, order_by)`, `create_task(title, description, status, priority, assignees, due_date, project_id, is_ai_suggested, is_approved)`, `update_task(task_id, ...)`, `delete_task(task_id)`, `list_tasks_due_today()`, `list_task_comments(task_id)`, `add_task_comment(task_id, author, content)`, `delete_task_comment(comment_id)` - status: `todo|in_progress|done`; priority: `high|medium|low`; assignees: JSON-encoded string; due_date: ms timestamp - Accepts flexible context; sentinel `-1` for optional integer update fields - [x] `app/agents/checkpoint_agent.py` — `@registry.register`: - Description: "Manages project checkpoints (milestones): list, create, update, delete" - Tools (4): `list_checkpoints(project_id)`, `create_checkpoint(project_id, title, date, is_ai_suggested, is_approved)`, `update_checkpoint(checkpoint_id, ...)`, `delete_checkpoint(checkpoint_id)` - `project_id` is required for create; date is a ms timestamp; supports AI-suggestion + approval workflow - [x] `app/agents/project_agent.py` — `@registry.register`: - Description: "Manages projects: list, get, create, update, archive, delete" - Tools (6): `list_projects(client_id, include_archived)`, `list_all_projects()`, `get_project(project_id)`, `create_project(name, client_id)`, `update_project(project_id, ...)`, `delete_project(project_id)` - status: `active|archived`; prefers archive over deletion (docstring guard on delete) - [x] `app/agents/note_agent.py` — `@registry.register`: - Description: "Manages notes: list, get, create, update, delete" - Tools (5): `list_notes(project_id)`, `get_note(note_id)`, `create_note(title, content, project_id)`, `update_note(note_id, ...)`, `delete_note(note_id)` - content is Markdown; `get_note` should be called before update to preserve existing content - [x] `app/agents/__init__.py`: imports all four agent modules to trigger `@registry.register` decorators - [x] Unit tests per agent with mocked LLM (registration, names, tool counts, handle(), direct tool invocation) - **Outcome:** Four domain-specific agents matching the UI data model (Tasks, Checkpoints, Projects, Notes), all registered and tested. ### Step 7 — Storage Layer ✅ - [x] `app/storage/blob_store.py`: - `BlobStore`: `async upload`, `async download`, `async delete` (idempotent), `async list_keys` - Keys: `{user_id}/{table}/{record_id}` — backend never inspects blob content - boto3 S3 with SSE-S3 at-rest encryption; client checksum stored in S3 object metadata - [x] `app/storage/vector_store.py`: - `VectorStore`: `async upsert`, `async search`, `async delete` - Pinecone (default, `namespace=user_id`) or Qdrant (`user_id` payload filter) — runtime-configurable - 32-dim SHA-256-derived float vector; blob stored as base64 in metadata/payload - ANN on encrypted data: known accuracy trade-off, documented - [x] `app/storage/encryption.py`: - `verify_checksum(blob, checksum) -> bool` — SHA-256 + `hmac.compare_digest` (constant-time) - `reject_if_tampered(blob, checksum)` — raises `HTTP 400` on mismatch - Backend NEVER holds decryption keys - [x] `app/schemas.py`: added `StorageRecord*`, `VectorItem`, `VectorUpsertRequest`, `VectorSearch*`, `Plugin*` schemas - [x] `app/config/settings.py`: added `PINECONE_API_KEY`, `PINECONE_INDEX`, `QDRANT_URL`, `QDRANT_API_KEY` - [x] `requirements.txt`: added `moto[s3]`, `pinecone`, `qdrant-client` - [x] 37 unit tests covering encryption, BlobStore (moto), VectorStore Pinecone, VectorStore Qdrant - **Outcome:** Cloud storage layer that handles E2E encrypted blobs without ever accessing plaintext. ### Step 8 — API Routes ✅ #### 8a — Chat endpoint - [x] `app/api/routes/chat.py`: - `POST /api/v1/chat`: - Request: `ChatRequest` - Calls `orchestrate(request)` or `orchestrate()` + `build_plan()` - Response: `ChatResponse` or `ExecutionPlan` - `WebSocket /api/v1/chat/stream`: - Client sends `ChatRequest` as first JSON frame - Server yields token strings via `orchestrate_stream()` - Final frame: JSON `ChatResponse` with `{"done": true, "response": "...", "actions": [...]}` - Heartbeat ping every 30s to keep connection alive #### 8b — Plans endpoint - [x] `app/api/routes/plans.py`: - `GET /api/v1/plans/playbook`: Returns all playbooks available for the user's tier - `GET /api/v1/plans/playbook/{plan_id}`: Returns a specific plan #### 8c — Storage endpoint (cloud records) - [x] `app/api/routes/storage.py`: - `POST /api/v1/storage/records`: Create encrypted record - Request: `StorageRecordCreate` - Verifies checksum, stores blob in S3, inserts metadata row in PostgreSQL - Response: `{id: str, created_at: int}` - `GET /api/v1/storage/records`: List record metadata (no blobs) - Query params: `table: str`, `page: int`, `limit: int` - Response: `list[{id, table, checksum, created_at, updated_at}]` - `GET /api/v1/storage/records/{id}`: Download encrypted blob - Response: blob bytes + `X-Checksum` header - `PUT /api/v1/storage/records/{id}`: Update encrypted blob - Request: `StorageRecordUpdate` - `DELETE /api/v1/storage/records/{id}`: Delete record + S3 blob - All routes enforce tier cloud_storage_gb quota via `TierManager.check_quota(user_id)` #### 8d — Vectors endpoint (cloud vector store) - [x] `app/api/routes/vectors.py`: - `POST /api/v1/storage/vectors/upsert`: - Request: `VectorUpsertRequest` - Verifies checksums, delegates to `VectorStore.upsert()` - Response: `{upserted: int}` - `POST /api/v1/storage/vectors/search`: - Request: `VectorSearchRequest` - Delegates to `VectorStore.search()` - Response: `VectorSearchResponse` - `DELETE /api/v1/storage/vectors`: - Request: `{ids: list[str]}` #### 8e — Backup endpoint - [x] `app/api/routes/backup.py`: - `PUT /api/v1/backup`: Accepts binary blob + metadata headers (`X-Backup-Version`, `X-Backup-Timestamp`, `X-Backup-Checksum`). Stores in S3 keyed by `{user_id}/{timestamp}`. Enforces tier limits: - Free: 0 (no backup) - Pro: 5 GB - Power: 25 GB - Team: unlimited - `GET /api/v1/backup`: Returns latest blob for authenticated user. Supports `If-Modified-Since`. - `GET /api/v1/backup/history`: Returns list of `BackupMetadata` (no blobs). - `DELETE /api/v1/backup/{backup_id}`: Delete specific backup. #### 8f — Plugins endpoint - [x] `app/api/routes/plugins.py`: - `GET /api/v1/plugins`: - Query params: `category: str | None`, `q: str | None`, `page: int`, `sort: Literal['rating', 'installs', 'newest']` - Response: `PluginListResponse` - Available from Power tier and above - `GET /api/v1/plugins/{id}`: - Response: `PluginManifest` + ratings + install count - `POST /api/v1/plugins/{id}/install`: - Request: `PluginInstallRequest` - Records installation for the user (billing tracking, analytics) - If plugin is paid: triggers Stripe Connect charge + revenue split (70% developer, 30% platform) - Response: `{ok: true, download_url: str}` — signed S3 URL for plugin package - `DELETE /api/v1/plugins/{id}/install`: - Unregisters installation #### 8g — Auth endpoint - [x] `app/api/routes/auth.py`: - `POST /api/v1/auth/register`: `{email, password}` → bcrypt hash → insert user → return `AuthTokens` - `POST /api/v1/auth/login`: Validate credentials → return `AuthTokens` - `POST /api/v1/auth/refresh`: Rotate refresh token → return new `AuthTokens` - `GET /api/v1/auth/me`: Return `UserProfile` for current JWT #### 8h — Billing endpoint - [x] `app/api/routes/billing.py`: - `POST /api/v1/billing/checkout`: Creates Stripe checkout session → returns URL - `POST /api/v1/billing/webhook`: Handles Stripe webhooks (subscription lifecycle) - `GET /api/v1/billing/subscription`: Returns current subscription info - `DELETE /api/v1/billing/subscription`: Cancels subscription - **Outcome:** Complete REST + WebSocket API covering orchestration, storage, vectors, backup, marketplace. ### Step 9 — Middleware #### 9a — Auth middleware - [x] `app/api/middleware/auth.py`: - FastAPI dependency: `get_current_user(token: str = Depends(oauth2_scheme)) -> UserProfile` - Validates JWT signature, expiry, extracts `user_id` and `tier` - Raises `401` on invalid/expired token - Exempt routes: `/api/v1/auth/register`, `/api/v1/auth/login`, `/api/v1/billing/webhook` #### 9b — Rate limiter - [x] `app/api/middleware/rate_limit.py`: - Uses `slowapi` with `Limiter(key_func=get_user_id_from_jwt)` - Tier-based limits: - Free: 20 req/min - Pro: 60 req/min - Power: 120 req/min - Team: 200 req/seat/min - Custom 429 response with `Retry-After` header #### 9c — Sanitizer - [x] `app/api/middleware/sanitizer.py`: - Response middleware that scans response bodies - Strips: system prompt fragments, agent internal reasoning, tool schemas, routing metadata - Pattern-based detection + exact match against known prompt fingerprints - Logs sanitization events for monitoring - **Outcome:** Secure, rate-limited API with prompt IP protection. ### Step 10 — Plugin Marketplace ✅ - [x] `app/marketplace/plugin_registry.py`: - `PluginRegistry`: - `async list_plugins(category, query, page, sort) -> PluginListResponse` - `async get_plugin(plugin_id) -> PluginManifest | None` - `async submit_plugin(manifest: PluginManifest, package_s3_key: str) -> str` — returns plugin_id, sets status = 'pending_review' - `async approve_plugin(plugin_id) -> None` — admin only, sets status = 'approved' - `async reject_plugin(plugin_id, reason: str) -> None` - [x] `app/marketplace/plugin_review.py`: - `ReviewQueue`: - `async get_pending() -> list[dict]` - `async submit_review(plugin_id, reviewer_id, decision, notes) -> None` - Security checklist enforced before approval: manifest schema valid, permissions are from allowed set, no binary blobs in manifest - [x] `app/marketplace/revenue_share.py`: - `RevenueShare`: - `async record_install(plugin_id, user_id, amount_cents) -> None` - `async payout_developer(plugin_id, period) -> None` — Stripe Connect transfer: 70% to developer - `async get_earnings(developer_id, period) -> dict` - **Outcome:** Plugin marketplace with catalog, review workflow, and revenue split. ### Step 11 — Billing & Tier management ✅ - [x] `app/billing/stripe_service.py`: - `create_checkout_session(user_id, tier) -> str` - `handle_webhook(payload, sig_header) -> None`: processes `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed` - `get_subscription(user_id) -> dict | None` - `cancel_subscription(user_id) -> None` - [x] `app/billing/tier_manager.py`: - `TierManager`: - Feature matrix: ```python FEATURES = { 'free': { 'agents': 3, 'batch_active': 2, 'cloud_storage_gb': 0, 'backup_gb': 0, 'providers': 1, 'batch_builder': False, 'plugin_marketplace': False, 'sso': False, }, 'pro': { 'agents': -1, # unlimited 'batch_active': 10, 'cloud_storage_gb': 5, 'backup_gb': 5, 'providers': -1, 'batch_builder': False, 'plugin_marketplace': False, 'sso': False, }, 'power': { 'agents': -1, 'batch_active': -1, # unlimited 'cloud_storage_gb': 25, 'backup_gb': 25, 'providers': -1, 'batch_builder': True, 'plugin_marketplace': True, 'sso': False, }, 'team': { 'agents': -1, 'batch_active': -1, 'cloud_storage_gb': -1, 'backup_gb': -1, 'providers': -1, 'batch_builder': True, 'plugin_marketplace': True, 'sso': True, }, } ``` - `get_tier(user_id) -> BillingTier` - `check_feature(user_id, feature) -> bool` - `get_rate_limit(tier) -> int` - `check_quota(user_id) -> bool` — checks cloud_storage_gb current usage vs limit - [x] `app/billing/__init__.py`: exports `stripe_service` and `tier_manager` singletons - [x] `app/api/routes/billing.py`: refactored to delegate to `StripeService` - [x] `app/api/routes/storage.py` and `backup.py`: `_check_quota` now delegates to `tier_manager.enforce_quota` / `enforce_backup_quota` - **Outcome:** Stripe integration with tier-based feature gating matching Free/Pro(15€)/Power(29€)/Team(49€/seat). ### Step 12 — Database (auth/billing/marketplace only) - [x] PostgreSQL schema via Alembic: - `users`: `id UUID PK`, `email UNIQUE`, `password_hash`, `tier` (default 'free'), `stripe_customer_id`, `created_at`, `updated_at` - `refresh_tokens`: `id UUID PK`, `user_id FK`, `token_hash`, `expires_at`, `created_at` - `subscriptions`: `id UUID PK`, `user_id FK`, `stripe_subscription_id`, `tier`, `status`, `current_period_end`, `created_at` - `backup_metadata`: `id UUID PK`, `user_id FK`, `s3_key`, `version`, `timestamp`, `checksum`, `size_bytes`, `created_at` - `storage_records`: `id UUID PK`, `user_id FK`, `table_name VARCHAR`, `s3_key`, `checksum`, `size_bytes`, `created_at`, `updated_at` — metadata only, no plaintext - `plugins`: `id UUID PK`, `name`, `description`, `version`, `author_id FK`, `category`, `status` (pending_review/approved/rejected), `price_cents`, `s3_package_key`, `install_count`, `avg_rating`, `created_at` - `plugin_installations`: `id UUID PK`, `plugin_id FK`, `user_id FK`, `installed_at` - `plugin_reviews`: `id UUID PK`, `plugin_id FK`, `reviewer_id FK`, `decision`, `notes`, `reviewed_at` - `revenue_events`: `id UUID PK`, `plugin_id FK`, `user_id FK`, `amount_cents`, `developer_share_cents`, `stripe_transfer_id`, `created_at` - [x] Initial Alembic migration - [x] SQLAlchemy models in `app/models.py` - **Outcome:** Auth, billing, storage metadata, and marketplace persistence. Zero user data in plaintext. ### Step 13 — Testing & deployment ✅ - [x] `tests/conftest.py`: TestClient fixture, mock LLM fixture (`AsyncMock` returning canned responses), mock agent fixture, test DB (SQLite in-memory for speed), mock S3 (moto), mock Pinecone - [x] `tests/test_orchestrator.py`: classify_intent routing, single agent, pipeline, plan mode - [x] `tests/test_agents.py`: each agent with mocked tools - [x] `tests/test_auth.py`: register → login → access protected → refresh → expired token - [x] `tests/test_backup.py`: upload → download → history → delete, tier limit enforcement - [x] `tests/test_storage.py`: create record → list → download → update → delete, checksum rejection, quota enforcement - [x] `tests/test_plugins.py`: list plugins, install, uninstall, revenue event creation, tier gate (free user blocked) - [x] `Dockerfile` optimized for production (gunicorn + uvicorn workers) - [x] GitHub Actions CI: lint (ruff), test (pytest), build Docker image - **Outcome:** Fully tested, deployable backend. --- ## API Contract Summary | Method | Endpoint | Auth | Request | Response | |--------|----------|------|---------|----------| | POST | `/api/v1/auth/register` | No | `{email, password}` | `AuthTokens` | | POST | `/api/v1/auth/login` | No | `{email, password}` | `AuthTokens` | | POST | `/api/v1/auth/refresh` | No | `{refresh_token}` | `AuthTokens` | | GET | `/api/v1/auth/me` | JWT | — | `UserProfile` | | POST | `/api/v1/chat` | JWT | `ChatRequest` | `ChatResponse \| ExecutionPlan` | | WS | `/api/v1/chat/stream` | JWT | `ChatRequest` (first frame) | Token stream + final JSON | | GET | `/api/v1/plans/playbook` | JWT | — | `ExecutionPlan[]` | | GET | `/api/v1/plans/playbook/:id` | JWT | — | `ExecutionPlan` | | POST | `/api/v1/storage/records` | JWT | `StorageRecordCreate` | `{id, created_at}` | | GET | `/api/v1/storage/records` | JWT | `?table&page&limit` | `RecordMeta[]` | | GET | `/api/v1/storage/records/:id` | JWT | — | Binary blob | | PUT | `/api/v1/storage/records/:id` | JWT | `StorageRecordUpdate` | `{ok: true}` | | DELETE | `/api/v1/storage/records/:id` | JWT | — | `{ok: true}` | | POST | `/api/v1/storage/vectors/upsert` | JWT | `VectorUpsertRequest` | `{upserted: int}` | | POST | `/api/v1/storage/vectors/search` | JWT | `VectorSearchRequest` | `VectorSearchResponse` | | DELETE | `/api/v1/storage/vectors` | JWT | `{ids: list[str]}` | `{ok: true}` | | PUT | `/api/v1/backup` | JWT | Binary blob + headers | `{ok: true}` | | GET | `/api/v1/backup` | JWT | — | Binary blob | | GET | `/api/v1/backup/history` | JWT | — | `BackupMetadata[]` | | DELETE | `/api/v1/backup/:id` | JWT | — | `{ok: true}` | | GET | `/api/v1/plugins` | JWT | `?category&q&page&sort` | `PluginListResponse` | | GET | `/api/v1/plugins/:id` | JWT | — | `PluginManifest` + stats | | POST | `/api/v1/plugins/:id/install` | JWT | `PluginInstallRequest` | `{ok, download_url}` | | DELETE | `/api/v1/plugins/:id/install` | JWT | — | `{ok: true}` | | POST | `/api/v1/billing/checkout` | JWT | `{tier}` | `{checkout_url}` | | POST | `/api/v1/billing/webhook` | Stripe sig | Stripe event | `{ok: true}` | | GET | `/api/v1/billing/subscription` | JWT | — | Subscription info | | DELETE | `/api/v1/billing/subscription` | JWT | — | `{ok: true}` | | GET | `/api/v1/health` | No | — | `{status, version}` | --- ## Stack | Layer | Technology | |-------|-----------| | Framework | FastAPI + Uvicorn | | LLM | LangChain + langchain-openai | | Auth | PyJWT + bcrypt + OAuth2 | | Billing | stripe-python + Stripe Connect | | Blob storage | boto3 (S3) | | Vector store | Pinecone or Qdrant (configurable) | | Database | PostgreSQL + SQLAlchemy + Alembic | | Rate limiting | slowapi | | Testing | pytest + pytest-asyncio + httpx + moto (S3 mock) | | Deployment | Docker → fly.io / Railway / AWS ECS | --- ## Development Rules 1. **NEVER persist user data in plaintext.** The DB stores only auth, billing, storage metadata, and marketplace data. User context arrives in requests and is discarded. Cloud blobs are E2E encrypted client-side — backend only stores opaque bytes. 2. **NEVER expose prompts.** System prompts are composed server-side from fragments. Responses are sanitized before sending. In plan mode, `prompt_template` fields are reference IDs only. 3. **NEVER decrypt user blobs.** `app/storage/encryption.py` only verifies checksums. No decryption key ever reaches the backend. 4. **Stateless request handling.** No server-side session state. All context comes from the client + JWT. 5. **Type hints everywhere.** All functions have full type annotations. 6. **Test every agent.** Each chat agent has unit tests with mocked LLM responses. 7. **Structured logging.** JSON logs with request ID correlation. 8. **Tier gates are enforced server-side.** Never trust client-reported tier. Always fetch from DB via `TierManager.get_tier(user_id)`. 9. **One step at a time.** Implement one numbered step per session. When the step is fully done, mark all its checkboxes as `[x]` in this file and commit with message `step N complete: `.