+
# Rio — AI-Powered Figma Plugin
-**Rio** is a full-stack Figma plugin that supercharges the design workflow with conversational AI generation, JSON-based design import/export, a personal UI component library, and persistent design versioning. It is backed by a production-grade Node.js API with Stripe billing, Google OAuth, and multi-provider AI support.
+**Conversational AI design generation · JSON import/export · Personal UI library · Stripe billing**
+
+[](https://nodejs.org)
+[](https://www.typescriptlang.org)
+[](https://react.dev)
+[](https://www.postgresql.org)
+[](https://expressjs.com)
+[](https://stripe.com)
+[](https://aws.amazon.com/s3)
+[](LICENSE)
+
+
---
## Table of Contents
-1. [Features](#features)
-2. [Architecture](#architecture)
-3. [Tech Stack](#tech-stack)
-4. [Project Structure](#project-structure)
-5. [Database Schema](#database-schema)
-6. [API Reference](#api-reference)
-7. [Authentication](#authentication)
-8. [Environment Variables](#environment-variables)
-9. [Development Setup](#development-setup)
-10. [Scripts](#scripts)
-11. [Deployment](#deployment)
-12. [Diagrams](#diagrams)
+1. [Overview](#1-overview)
+2. [Features](#2-features)
+3. [System Architecture](#3-system-architecture)
+4. [Clean Architecture Layers](#4-clean-architecture-layers)
+5. [Tech Stack](#5-tech-stack)
+6. [Project Structure](#6-project-structure)
+7. [Database Schema & ERD](#7-database-schema--erd)
+8. [API Reference](#8-api-reference)
+9. [Workflow Diagrams](#9-workflow-diagrams)
+ - 9.1 [Authentication Flow](#91-authentication-flow)
+ - 9.2 [AI Design Generation Flow](#92-ai-design-generation-flow)
+ - 9.3 [Payment & Points Flow](#93-payment--points-flow)
+ - 9.4 [Subscription Flow](#94-subscription-flow)
+ - 9.5 [UI Library Component Flow](#95-ui-library-component-flow)
+ - 9.6 [Plugin Build Pipeline](#96-plugin-build-pipeline)
+ - 9.7 [Request Middleware Pipeline](#97-request-middleware-pipeline)
+10. [UML Class Diagram](#10-uml-class-diagram)
+11. [AI Models & Pricing](#11-ai-models--pricing)
+12. [Monetization Model](#12-monetization-model)
+13. [Security & Rate Limiting](#13-security--rate-limiting)
+14. [Environment Variables](#14-environment-variables)
+15. [Development Setup](#15-development-setup)
+16. [Scripts Reference](#16-scripts-reference)
+17. [Deployment](#17-deployment)
+18. [Points Lifecycle & Deduction Logic](#18-points-lifecycle--deduction-logic)
+19. [Node Creation Pipeline](#19-node-creation-pipeline-figma-canvas)
+20. [Export Pipeline](#20-export-pipeline-figma--json)
+21. [Error Handling Architecture](#21-error-handling-architecture)
+22. [Full System State Machine](#22-full-system-state-machine)
---
-## Features
+## 1. Overview
-| Feature | Description |
-|---|---|
-| **AI Generate** | Chat with an AI to generate new designs or modify an existing selection |
-| **Paste JSON** | Convert any structured JSON object into a live Figma design |
-| **Export to JSON** | Export a Figma selection to a clean, structured JSON for handoff |
-| **Design Versions** | Save and restore design snapshots to a personal cloud database |
-| **UI Library** | Organise reusable design components in projects with S3-hosted preview images |
-| **Prototype** | Generate Figma prototype connections with AI |
-| **Points & Subscriptions** | Stripe-powered pay-per-use points and monthly subscription plans |
+**Rio** is a production-grade full-stack Figma plugin that bridges the gap between natural language and pixel-perfect design. It combines a **conversational AI engine** (supporting 7+ model providers), a **personal cloud UI component library**, and a **Stripe-powered billing system** into a single Figma plugin.
----
+The system comprises two independent sub-projects that communicate over HTTP:
+
+| Sub-project | Role |
+| --------------- | ----------------------------------------------------------- |
+| `backend/` | Node.js REST API — auth, AI orchestration, billing, storage |
+| `figma-plugin/` | Figma sandbox + React iframe — design creation and UI |
-## Architecture
+---
-The system is composed of three layers:
+## 2. Features
+
+| Feature | Description | API Endpoint |
+| ------------------------ | ------------------------------------------------------------------- | ---------------------------------------------- |
+| **AI Generate** | Chat with AI to generate new Figma designs from natural language | `POST /api/designs/generate-from-conversation` |
+| **AI Edit** | Modify a selected Figma node with a follow-up prompt | `POST /api/designs/edit-with-ai` |
+| **AI Variant** | Generate a design variant from an existing Figma selection | `POST /api/designs/generate-based-on-existing` |
+| **AI Prototype** | Auto-generate Figma prototype connections | `POST /api/designs/generate-prototype` |
+| **Paste JSON** | Convert any structured JSON blob into live Figma nodes | Plugin-side only |
+| **Export JSON** | Export any Figma selection to a clean, portable JSON | Plugin-side only |
+| **Design Versions** | Save & restore design snapshots to the cloud | `GET/POST /api/design-generations` |
+| **UI Library** | Manage reusable components in named projects with S3 image previews | `CRUD /api/ui-library` |
+| **Points (Pay-per-use)** | Purchase AI generation credits with Stripe one-time checkout | `POST /api/payments/create-checkout` |
+| **Subscriptions** | Monthly subscription plans with daily point allocations | `POST /api/subscriptions/create-checkout` |
+| **Google OAuth** | One-click login via Google — JWT issued on completion | `GET /auth/google` |
-```
-┌─────────────────────────────────────────────┐
-│ Figma Plugin (UI) │
-│ React 19 · Clean Architecture · esbuild │
-├─────────────────────────────────────────────┤
-│ Backend API │
-│ Express.js · TypeScript · TypeORM · Postgres│
-├─────────────────────────────────────────────┤
-│ External Services │
-│ OpenAI · Claude · Gemini · Stripe · S3 │
-└─────────────────────────────────────────────┘
-```
+---
-### Plugin Clean Architecture Layers
+## 3. System Architecture
```
-Presentation → Application → Domain → Infrastructure
-(React UI) (Use Cases) (Entities) (Figma API, HTTP)
+┌──────────────────────────────────────────────────────────────────────┐
+│ FIGMA DESKTOP APP │
+│ ┌─────────────────────────────┐ ┌────────────────────────────┐ │
+│ │ Plugin Sandbox (main.ts) │◄──│ Plugin UI (React iframe) │ │
+│ │ TypeScript · Figma API │ │ React 19 · esbuild │ │
+│ │ Node creators/exporters │ │ Zustand · react-toastify │ │
+│ └──────────────┬───────────────┘ └────────────┬───────────────┘ │
+│ │ postMessage() │ HTTP fetch() │
+└─────────────────┼───────────────────────────────-┼───────────────────┘
+ │ │
+ │ ┌────────────▼──────────────────┐
+ │ │ BACKEND API (Render) │
+ │ │ Express 5 · TypeORM · Node 18 │
+ │ │ Clean Architecture (DDD) │
+ │ └────────────┬──────────────────┘
+ │ │
+ ┌─────────▼─────────────────────────────────▼─────────────────┐
+ │ EXTERNAL SERVICES │
+ │ ┌───────────┐ ┌─────────┐ ┌──────┐ ┌──────┐ ┌──────────┐ │
+ │ │ PostgreSQL │ │ AWS S3 │ │Stripe│ │Google│ │ AI APIs │ │
+ │ │ (TypeORM) │ │(images) │ │(pay) │ │OAuth │ │7 Providers│ │
+ │ └───────────┘ └─────────┘ └──────┘ └──────┘ └──────────┘ │
+ └─────────────────────────────────────────────────────────────┘
```
-- **Domain** — pure entities, value objects, repository interfaces (no framework dependencies)
-- **Application** — use cases that orchestrate the domain (GenerateDesign, ImportDesign, ExportDesign, etc.)
-- **Infrastructure** — Figma API adapters, node creators, exporters, mappers
-- **Presentation** — React UI, message handlers, contexts, hooks
-
-### Data Flows
+---
-**AI Generation**
-```
-User prompt → ChatInterface → POST /api/designs/generate-from-conversation
-→ Claude/OpenAI/Gemini → structured JSON + HTML preview
-→ Plugin main thread → Figma canvas
+## 4. Clean Architecture Layers
+
+Both the **backend** and the **Figma plugin** follow Clean Architecture with strict layer separation.
+
+### 4.1 Backend Layers
+
+```mermaid
+flowchart TB
+ subgraph Presentation["Presentation Layer"]
+ direction LR
+ R[Routes] --> C[Controllers]
+ M[Middleware] --> R
+ end
+
+ subgraph Application["Application Layer"]
+ direction LR
+ UC[Use Cases] --> DTO[DTOs]
+ end
+
+ subgraph Domain["Domain Layer (Pure)"]
+ direction LR
+ E[Entities] --- RI[Repository\nInterfaces]
+ RI --- SI[Service\nInterfaces]
+ end
+
+ subgraph Infrastructure["Infrastructure Layer"]
+ direction LR
+ DB[(TypeORM\nRepositories)] --- SV[Services\nAI · Stripe · S3 · JWT]
+ SV --- CFG[Config\nenv · AI models · Plans]
+ end
+
+ Presentation -->|calls| Application
+ Application -->|depends on| Domain
+ Infrastructure -->|implements| Domain
+
+ style Domain fill:#1e3a5f,color:#fff
+ style Application fill:#1a4a2e,color:#fff
+ style Presentation fill:#3d1a1a,color:#fff
+ style Infrastructure fill:#2d2d00,color:#fff
```
-**Export**
-```
-Figma selection → NodeExporter → structured JSON
-→ Copy / Download / Save to DB
+### 4.2 Plugin Layers
+
+```mermaid
+flowchart TB
+ subgraph P["Presentation (React UI)"]
+ App["App.tsx"] --> Screens["LoginScreen\nHomeScreen"]
+ Screens --> Sections["AiSection · ExportSection\nPasteJsonSection · ProjectsSection"]
+ Sections --> Hooks["useApiClient\nusePluginMessage\nuseDropdown"]
+ end
+
+ subgraph A["Application (Use Cases)"]
+ UC1["ExportSelectedUseCase"]
+ UC2["ImportDesignUseCase"]
+ UC3["ImportAiDesignUseCase"]
+ SVC["DesignDataParser\nNodeCounter\nAccessControl"]
+ end
+
+ subgraph D["Domain (Pure Entities)"]
+ EN["DesignNode\nFill · Effect\nConstraints"]
+ IF["NodeRepository\nUIPort\nNotificationPort"]
+ VO["Color\nTypography"]
+ end
+
+ subgraph I["Infrastructure (Figma API)"]
+ CR["Creators\nframe · rect · text\nshape · component"]
+ EX["NodeExporter"]
+ MP["Mappers\nfill · effect · nodeType"]
+ end
+
+ P --> A --> D
+ I -->|implements| D
+
+ style D fill:#1e3a5f,color:#fff
+ style A fill:#1a4a2e,color:#fff
+ style P fill:#3d1a1a,color:#fff
+ style I fill:#2d2d00,color:#fff
```
-**Version Restore**
-```
-User clicks version → GET /api/designs/:id → JSON
-→ Plugin main thread → Figma canvas
+### 4.3 Dependency Injection
+
+```mermaid
+flowchart LR
+ Bootstrap["index.ts\n(Composition Root)"] -->|instantiates| DB["DataSource\n(TypeORM)"]
+ Bootstrap -->|creates| Repos["Repositories\nUser · Design\nPayment · Subscription\nUILibrary · ClientError"]
+ Bootstrap -->|creates| Services["Services\nAI · JWT · Auth\nStripe · S3 · Points"]
+ Bootstrap -->|wires into| UseCases["Use Cases"]
+ UseCases -->|injected into| Controllers["Controllers"]
+ Controllers -->|mounted on| Routes["Express Routes"]
```
---
-## Tech Stack
+## 5. Tech Stack
### Backend
-| Category | Technology |
-|---|---|
-| Runtime | Node.js |
-| Framework | Express.js v5 |
-| Language | TypeScript 5.8 |
-| ORM | TypeORM v0.3 |
-| Database | PostgreSQL |
-| Auth | Google OAuth 2 + JWT (30-day expiry) |
-| Payment | Stripe SDK v20 |
-| Storage | AWS S3 (`@aws-sdk/client-s3`) |
-| AI Providers | OpenAI, Claude, Gemini, Mistral, Hugging Face, POE, Open Router |
-| Validation | Joi · express-validator · Zod |
-| Docs | Swagger / OpenAPI (`swagger-ui-express`) |
+| Category | Technology | Version | Notes |
+| -------------- | -------------------------------------------------------------------- | --------------------- | ------------------------ |
+| Runtime | Node.js | 18+ LTS | |
+| Framework | Express.js | v5.1 | |
+| Language | TypeScript | 5.8 | Strict mode |
+| ORM | TypeORM | 0.3.28 | Decorators |
+| Database | PostgreSQL | 14+ | |
+| Authentication | Google OAuth 2.0 + JWT | 30-day expiry | figma.clientStorage |
+| Payments | Stripe SDK | v20.3.1 | One-time + subscriptions |
+| Storage | AWS S3 | SDK v3.999 | Component previews |
+| Validation | Joi · express-validator · Zod | Latest | Layered checks |
+| Rate Limiting | express-rate-limit | 8.3.1 | Per-route limits |
+| Docs | Swagger / OpenAPI | swagger-ui-express v5 | `/api-docs` |
+| AI Providers | OpenAI · Claude · Gemini · Mistral · HuggingFace · POE · Open Router | Multiple | 7+ providers |
+| Compression | compression | Latest | gzip level 6 |
+| Additional | axios · bcryptjs · jsonwebtoken · dayjs · cors | Latest | |
### Figma Plugin
-| Category | Technology |
-|---|---|
-| Language | TypeScript 5.8 |
-| UI | React 19 + React-DOM 19 |
-| Build | esbuild v0.27 |
-| Icons | lucide-react |
-| Notifications | react-toastify |
-| Figma typings | `@figma/plugin-typings` v1.119 |
+| Category | Technology | Version | Notes |
+| ------------- | --------------------- | ------- | ------------------ |
+| Language | TypeScript | 5.8 | Path aliases |
+| UI Framework | React + React-DOM | 19.2.4 | |
+| Bundler | esbuild | 0.27.1 | Single HTML output |
+| Icons | lucide-react | 0.563.0 | |
+| Notifications | react-toastify | Latest | |
+| Figma Typings | @figma/plugin-typings | 1.119 | |
---
-## Project Structure
+## 6. Project Structure
```
task-creator/
-├── backend/
+│
+├── backend/ # Node.js REST API
│ ├── public/
-│ │ ├── prompt/ # AI system prompt templates
-│ │ └── pages/ # Static HTML pages
+│ │ ├── pages/home.html # Landing page
+│ │ └── prompt/design/ # AI system prompt templates
│ └── src/
-│ ├── index.ts # Bootstrap & DI composition root
-│ ├── application/
-│ │ ├── dto/ # Request / response DTOs
-│ │ └── use-cases/ # All business use cases
-│ ├── domain/
-│ │ ├── entities/ # Core domain entities
-│ │ ├── repositories/ # Repository interfaces
-│ │ └── services/ # Service interfaces
+│ ├── index.ts # Bootstrap & DI composition root
+│ │
+│ ├── application/ # Business logic (no framework)
+│ │ ├── dto/ # Request / response DTOs
+│ │ ├── errors/ # Custom HTTP error classes
+│ │ └── use-cases/
+│ │ ├── auth/ # GoogleSignIn
+│ │ ├── design/ # GenerateDesign · EditDesign · Prototype
+│ │ ├── design-generation/ # ListDesigns · GetDesign · DeleteDesign
+│ │ ├── payment/ # CreateCheckout · HandleWebhook · GetBalance
+│ │ ├── subscription/ # CreateSub · CancelSub · GetStatus
+│ │ ├── ui-library/ # CRUD projects & components
+│ │ └── client-error/ # LogClientError
+│ │
+│ ├── domain/ # Pure domain (no deps)
+│ │ ├── entities/ # User · DesignGeneration · PaymentTransaction
+│ │ │ # Subscription · UILibraryProject · UILibraryComponent
+│ │ ├── repositories/ # Repository interfaces (contracts)
+│ │ └── services/ # IAiDesignService · IPointsService · IStripeService
+│ │ # IJwtService · IS3Service
+│ │
│ └── infrastructure/
-│ ├── config/ # env, CORS, Swagger, data-source
+│ ├── config/
+│ │ ├── env.config.ts # All env vars with defaults
+│ │ ├── ai-models.config.ts # Model registry + pricing
+│ │ ├── points-packages.config.ts # Stripe one-time packages
+│ │ └── subscription-plans.config.ts # Monthly plan tiers
│ ├── database/
-│ │ ├── entities/ # TypeORM entity classes
-│ │ └── migrations/ # Database migrations
-│ ├── repository/ # TypeORM implementations
-│ ├── services/ # AI, Stripe, S3, JWT, Auth services
+│ │ ├── data-source.ts # TypeORM connection
+│ │ ├── entities/ # UserEntity · DesignGenerationEntity · …
+│ │ └── migrations/ # Sequential SQL migrations
+│ ├── repository/ # TypeORM implementations of domain interfaces
+│ ├── services/
+│ │ ├── ai/ # AiGenerateDesignService · IconExtractor · CostCalculator
+│ │ ├── auth/ # GoogleAuthService · JwtService · TokenStore
+│ │ ├── payment/ # StripeService · PointsService
+│ │ └── storage/ # S3Service
│ └── web/
-│ ├── controllers/ # Route handlers
-│ ├── middleware/ # auth, validation, error
-│ ├── routes/ # Express routers
-│ └── server.ts # Express app factory
+│ ├── controllers/ # Route handlers (inject services)
+│ ├── middleware/ # auth · validation · rateLimiter · concurrencyLimiter · logger
+│ ├── routes/ # Express routers per domain
+│ ├── validation/ # Joi & express-validator schemas
+│ ├── docs/ # Swagger YAML / JSON
+│ └── server.ts # Express app factory
│
-├── figma-plugin/
-│ ├── manifest.json # Figma plugin manifest
-│ ├── build.js # esbuild configuration
-│ ├── .env # Local backend URL
-│ ├── .env.production # Production backend URL
+├── figma-plugin/ # Figma plugin
+│ ├── manifest.json # Plugin manifest (id · api · permissions)
+│ ├── build.js # esbuild config (UI + code bundles)
+│ ├── .env # BACKEND_URL=http://localhost:5000
+│ ├── .env.production # BACKEND_URL=https://...onrender.com
│ └── src/
-│ ├── main.ts # Plugin entry point
+│ ├── main.ts # Plugin entry & composition root
│ ├── application/
-│ │ ├── services/ # design-data-parser, node-counter
-│ │ └── use-cases/ # export-all, export-selected,
-│ │ # import-design, import-ai-design
+│ │ ├── services/ # DesignDataParser · NodeCounter · AccessControl
+│ │ └── use-cases/ # ExportAll · ExportSelected · ImportDesign · ImportAiDesign
│ ├── domain/
-│ │ ├── entities/ # DesignNode, Fill, Effect, Constraints
-│ │ ├── interfaces/ # NodeRepository, UIPort, NotificationPort
-│ │ └── value-objects/ # Color, Typography
+│ │ ├── entities/ # DesignNode · Fill · Effect · Constraints
+│ │ ├── interfaces/ # NodeRepository · UIPort · NotificationPort
+│ │ └── value-objects/ # Color · Typography
│ ├── infrastructure/
│ │ ├── figma/
-│ │ │ ├── creators/ # frame, rect, text, shape, component nodes
-│ │ │ ├── exporters/ # node.exporter.ts
-│ │ │ └── figma-node.repository.ts
-│ │ ├── mappers/ # fill, effect, node-type mappers
-│ │ └── services/ # error-reporter, image-optimizer
+│ │ │ ├── creators/ # FrameCreator · RectCreator · TextCreator
+│ │ │ │ # ShapeCreator · ComponentCreator
+│ │ │ ├── exporters/ # NodeExporter (Figma → JSON)
+│ │ │ ├── figma-node.repository.ts
+│ │ │ ├── figma-ui.port.ts
+│ │ │ ├── figma-notification.port.ts
+│ │ │ └── selection-change.handler.ts
+│ │ ├── mappers/ # EffectMapper · FillMapper · NodeTypeMapper
+│ │ └── services/ # ErrorReporter · ImageOptimizer
│ ├── presentation/
-│ │ ├── handlers/ # plugin-message.handler.ts
-│ │ └── ui/ # React components, contexts, hooks
-│ │ ├── App.tsx
-│ │ ├── contexts/ # AppContext, AuthContext
-│ │ ├── hooks/ # useApiClient, usePluginMessage,
-│ │ │ # useUILibrary, useDropdown
+│ │ ├── handlers/ # PluginMessageHandler
+│ │ └── ui/
+│ │ ├── App.tsx # Root component
+│ │ ├── screens/ # LoginScreen · HomeScreen
+│ │ ├── sections/ # AiSection · ExportSection · PasteJsonSection
+│ │ │ # ProjectsSection · PrototypePanel
│ │ ├── components/
-│ │ │ ├── tabs/ # AiTab, ExportTab, PasteJsonTab,
-│ │ │ │ # UILibraryTab, PrototypePanel
-│ │ │ ├── shared/ # Modal, DeleteConfirmModal,
-│ │ │ │ # LoadingState, CreateProjectModal
-│ │ │ └── modals/ # SaveModal, BuyPointsModal,
-│ │ │ # ProfileDropdown
-│ │ └── styles/ # base.css, component CSS files
+│ │ │ ├── shared/ # Modal · DeleteConfirmModal · LoadingState
+│ │ │ └── modals/ # SaveModal · BuyPointsModal · ProfileDropdown
+│ │ ├── hooks/ # useApiClient · usePluginMessage
+│ │ │ # useUILibrary · useDropdown · useNotify
+│ │ └── styles/ # base.css · ProjectsSection.css · …
│ └── shared/
-│ ├── constants/ # plugin-config.ts (API URL, dimensions)
-│ └── types/ # node-types.ts, shared interfaces
+│ ├── constants/ # plugin-config.ts
+│ └── types/ # node-types.ts · shared interfaces
│
-└── public/ # Architecture diagrams & assets
+└── public/ # Architecture diagrams (PNG)
+ ├── Clean Architecture UML.png
+ ├── Data Flow Sequence Diagram.png
+ └── Design Version ERD.png
```
---
-## Database Schema
-
-### Users
-| Column | Type | Notes |
-|---|---|---|
-| id | UUID PK | auto-generated |
-| figmaUserId | varchar | unique Figma identity |
-| userName | varchar | display name |
-| email | varchar | unique |
-| googleId | varchar | Google OAuth subject |
-| profilePicture | varchar | avatar URL |
-| pointsBalance | int | purchased points |
-| stripeCustomerId | varchar | Stripe customer |
-| hasPurchased | boolean | lifetime flag |
-| createdAt / updatedAt | timestamp | |
-
-### Design Versions
-| Column | Type | Notes |
-|---|---|---|
-| id | UUID PK | |
-| version | int | incrementing counter |
-| description | text | user-provided label |
-| designJson | JSONB | full Figma node tree |
-| userId | UUID FK → users | |
-
-### UI Library Projects
-| Column | Type | Notes |
-|---|---|---|
-| id | UUID PK | |
-| name | varchar | project name |
-| userId | UUID FK → users | |
-
-### UI Library Components
-| Column | Type | Notes |
-|---|---|---|
-| id | UUID PK | |
-| name | varchar | |
-| description | text | |
-| designJson | JSONB | component node tree |
-| previewImage | text | S3 image URL |
-| projectId | UUID FK → projects | |
-| userId | UUID FK → users | |
-
-### Subscriptions
-| Column | Type | Notes |
-|---|---|---|
-| id | UUID PK | |
-| planId | varchar | starter / pro / basic / premium |
-| status | enum | active · canceled · past_due · expired |
-| stripeSubscriptionId | varchar | |
-| currentPeriodStart / End | timestamp | billing window |
-| dailyPointsLimit | int | plan allocation |
-| dailyPointsUsed | int | resets daily |
-| cancelAtPeriodEnd | boolean | |
-| userId | UUID FK → users | |
-
-### Payment Transactions
-| Column | Type | Notes |
-|---|---|---|
-| id | UUID PK | |
-| stripeSessionId | varchar | checkout session |
-| stripePaymentIntentId | varchar | |
-| packageName | varchar | |
-| pointsPurchased | int | |
-| amountPaid | decimal | |
-| currency | varchar | default USD |
-| status | enum | pending · completed · failed |
-| userId | UUID FK → users | |
+## 7. Database Schema & ERD
+
+### Entity Relationship Diagram
+
+```mermaid
+erDiagram
+ USERS {
+ uuid id PK
+ varchar figmaUserId UK
+ varchar userName
+ varchar email UK
+ varchar googleId
+ varchar profilePicture
+ int pointsBalance
+ varchar stripeCustomerId UK
+ boolean hasPurchased
+ timestamp createdAt
+ timestamp updatedAt
+ }
+
+ DESIGN_GENERATIONS {
+ uuid id PK
+ uuid userId FK
+ varchar operationType
+ varchar modelId
+ text prompt
+ varchar designSystemId
+ jsonb conversationHistory
+ jsonb currentDesign
+ jsonb referenceDesign
+ jsonb resultDesign
+ jsonb resultConnections
+ varchar status
+ text errorMessage
+ text aiMessage
+ int inputTokens
+ int outputTokens
+ decimal totalCost
+ int pointsDeducted
+ timestamp createdAt
+ timestamp updatedAt
+ }
+
+ PAYMENT_TRANSACTIONS {
+ uuid id PK
+ uuid userId FK
+ varchar stripeSessionId
+ varchar stripePaymentIntentId
+ varchar packageName
+ int pointsPurchased
+ decimal amountPaid
+ varchar currency
+ varchar status
+ timestamp createdAt
+ timestamp completedAt
+ }
+
+ SUBSCRIPTIONS {
+ uuid id PK
+ uuid userId FK
+ varchar planId
+ varchar stripeSubscriptionId
+ varchar stripeCustomerId
+ varchar status
+ timestamp currentPeriodStart
+ timestamp currentPeriodEnd
+ int dailyPointsLimit
+ int dailyPointsUsed
+ varchar lastUsageResetDate
+ boolean cancelAtPeriodEnd
+ timestamp createdAt
+ timestamp updatedAt
+ }
+
+ UI_LIBRARY_PROJECTS {
+ uuid id PK
+ uuid userId FK
+ varchar name
+ timestamp createdAt
+ timestamp updatedAt
+ }
+
+ UI_LIBRARY_COMPONENTS {
+ uuid id PK
+ uuid projectId FK
+ uuid userId FK
+ varchar name
+ varchar description
+ jsonb designJson
+ text previewImage
+ timestamp createdAt
+ timestamp updatedAt
+ }
+
+ CLIENT_ERRORS {
+ uuid id PK
+ uuid userId FK
+ varchar errorType
+ text errorMessage
+ text stackTrace
+ varchar url
+ varchar userAgent
+ timestamp createdAt
+ }
+
+ USERS ||--o{ DESIGN_GENERATIONS : "has"
+ USERS ||--o{ PAYMENT_TRANSACTIONS : "has"
+ USERS ||--o{ SUBSCRIPTIONS : "has"
+ USERS ||--o{ UI_LIBRARY_PROJECTS : "owns"
+ USERS ||--o{ UI_LIBRARY_COMPONENTS : "owns"
+ USERS ||--o{ CLIENT_ERRORS : "logs"
+ UI_LIBRARY_PROJECTS ||--o{ UI_LIBRARY_COMPONENTS : "contains"
+```
+
+### Schema Details
+
+#### `design_generations`
+
+| Column | Type | Description |
+| ---------------------------------------------- | ------- | ------------------------------------------------------- |
+| operationType | `enum` | `create` · `create_by_reference` · `edit` · `prototype` |
+| conversationHistory | `JSONB` | Multi-turn chat history array |
+| currentDesign / referenceDesign / resultDesign | `JSONB` | Full Figma node trees |
+| status | `enum` | `success` · `failed` |
+| Indexes | — | `[userId]`, `[userId, operationType]`, `[createdAt]` |
+
+#### `subscriptions`
+
+| Column | Type | Description |
+| ------------------ | --------- | ---------------------------------------------- |
+| status | `enum` | `active` · `canceled` · `past_due` · `expired` |
+| dailyPointsUsed | `int` | Resets daily at midnight UTC |
+| lastUsageResetDate | `varchar` | Date string `YYYY-MM-DD` for reset tracking |
---
-## API Reference
+## 8. API Reference
-> Base URL (dev): `http://localhost:5000`
-> Base URL (prod): `https://task-creator-api.onrender.com`
+> **Base URL (dev):** `http://localhost:5000`
+> **Base URL (prod):** `https://task-creator-api.onrender.com`
+> **Interactive docs:** `GET /api-docs`
+> **Protected routes** require `Authorization: Bearer `
### Authentication — `/auth`
-| Method | Path | Description |
-|---|---|---|
-| GET | `/auth/google` | Initiate Google OAuth |
-| GET | `/auth/google/callback` | OAuth redirect callback |
-| GET | `/auth/poll` | Plugin polls for JWT after browser auth |
-| GET | `/auth/me` | Return current user profile |
+| Method | Path | Auth | Description |
+| ------ | ----------------------- | ---- | ------------------------------------- |
+| `GET` | `/auth/google` | — | Initiate Google OAuth redirect |
+| `GET` | `/auth/google/callback` | — | OAuth redirect handler — issues JWT |
+| `GET` | `/auth/poll` | — | Plugin polls until JWT is ready |
+| `GET` | `/auth/me` | ✅ | Return current user profile + balance |
-### AI Design — `/api/designs`
+### AI Design Generation — `/api/designs`
-| Method | Path | Description |
-|---|---|---|
-| POST | `/api/designs/generate-from-conversation` | Generate design from chat prompt |
-| POST | `/api/designs/edit-with-ai` | Modify selected design with AI |
-| POST | `/api/designs/generate-based-on-existing` | Generate variant of existing selection |
-| POST | `/api/designs/generate-prototype` | Generate prototype connections |
+| Method | Path | Auth | Body |
+| ------ | ----------------------------------------- | ---- | ------------------------------------------------------------ |
+| `POST` | `/api/designs/generate-from-conversation` | ✅ | `{ prompt, modelId, conversationHistory?, designSystemId? }` |
+| `POST` | `/api/designs/edit-with-ai` | ✅ | `{ prompt, modelId, currentDesign, conversationHistory? }` |
+| `POST` | `/api/designs/generate-based-on-existing` | ✅ | `{ prompt, modelId, referenceDesign }` |
+| `POST` | `/api/designs/generate-prototype` | ✅ | `{ modelId, currentDesign }` |
-### Design Versions — `/api/designs`
+### Design History — `/api/design-generations`
-| Method | Path | Description |
-|---|---|---|
-| POST | `/api/designs` | Save current design as version |
-| GET | `/api/designs` | List all versions for user |
-| GET | `/api/designs/:id` | Get a specific version |
-| DELETE | `/api/designs/:id` | Delete a version |
+| Method | Path | Auth | Description |
+| -------- | ----------------------------- | ---- | -------------------------------------- |
+| `GET` | `/api/design-generations` | ✅ | List user's design history (paginated) |
+| `GET` | `/api/design-generations/:id` | ✅ | Fetch one generation record |
+| `DELETE` | `/api/design-generations/:id` | ✅ | Delete a generation record |
### UI Library — `/api/ui-library`
-| Method | Path | Description |
-|---|---|---|
-| POST | `/api/ui-library/projects` | Create project |
-| GET | `/api/ui-library/projects` | List projects |
-| DELETE | `/api/ui-library/projects/:id` | Delete project |
-| GET | `/api/ui-library/projects/:id/components` | List components in project |
-| POST | `/api/ui-library/components` | Create component |
-| POST | `/api/ui-library/components/upload-image` | Upload S3 preview image |
-| DELETE | `/api/ui-library/components/:id` | Delete component |
+| Method | Path | Auth | Description |
+| -------- | ----------------------------------------- | ---- | ----------------------------------- |
+| `POST` | `/api/ui-library/projects` | ✅ | Create project |
+| `GET` | `/api/ui-library/projects` | ✅ | List all user's projects |
+| `DELETE` | `/api/ui-library/projects/:id` | ✅ | Delete project (cascade components) |
+| `GET` | `/api/ui-library/projects/:id/components` | ✅ | List components in project |
+| `POST` | `/api/ui-library/components` | ✅ | Create component with JSON |
+| `POST` | `/api/ui-library/components/upload-image` | ✅ | Upload S3 preview image |
+| `DELETE` | `/api/ui-library/components/:id` | ✅ | Delete component + S3 image |
### Payments — `/api/payments`
-| Method | Path | Description |
-|---|---|---|
-| POST | `/api/payments/create-checkout` | Stripe one-time checkout session |
-| POST | `/api/payments/webhook` | Stripe webhook handler |
-| GET | `/api/payments/balance` | User's current points balance |
-| GET | `/api/payments/history` | Payment transaction history |
-| GET | `/api/payments/packages` | Available point packages |
-| GET | `/api/payments/poll/:sessionId` | Poll payment completion |
+| Method | Path | Auth | Description |
+| ------ | ------------------------------- | ---- | ----------------------------------- |
+| `POST` | `/api/payments/create-checkout` | ✅ | Create Stripe one-time checkout |
+| `POST` | `/api/payments/webhook` | — | Stripe webhook (signature verified) |
+| `GET` | `/api/payments/balance` | ✅ | Current points balance |
+| `GET` | `/api/payments/history` | ✅ | Transaction history |
+| `GET` | `/api/payments/packages` | — | Available point packages |
+| `GET` | `/api/payments/poll/:sessionId` | ✅ | Poll checkout completion |
### Subscriptions — `/api/subscriptions`
-| Method | Path | Description |
-|---|---|---|
-| POST | `/api/subscriptions/create-checkout` | Subscription checkout session |
-| POST | `/api/subscriptions/cancel` | Cancel active subscription |
-| GET | `/api/subscriptions/status` | Current subscription & usage |
-| GET | `/api/subscriptions/plans` | Available plans and pricing |
+| Method | Path | Auth | Description |
+| ------ | ------------------------------------ | ---- | ---------------------------- |
+| `POST` | `/api/subscriptions/create-checkout` | ✅ | Create subscription checkout |
+| `POST` | `/api/subscriptions/cancel` | ✅ | Cancel at period end |
+| `GET` | `/api/subscriptions/status` | ✅ | Active plan + daily usage |
+| `GET` | `/api/subscriptions/plans` | — | Available plans and pricing |
+
+### Utility
+
+| Method | Path | Auth | Description |
+| ------ | --------------------- | ---- | ----------------------------- |
+| `GET` | `/api/ai-models` | ✅ | List all available AI models |
+| `GET` | `/api/design-systems` | ✅ | List available design systems |
+| `POST` | `/api/errors` | ✅ | Log a client-side error |
+
+---
+
+## 9. Workflow Diagrams
+
+### 9.1 Authentication Flow
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor User
+ participant Plugin as Figma Plugin\n(main.ts)
+ participant UI as Plugin UI\n(React)
+ participant Browser as Browser Tab
+ participant API as Backend API
+ participant Google as Google OAuth
+
+ User->>UI: Clicks "Sign in with Google"
+ UI->>Plugin: postMessage(OPEN_AUTH)
+ Plugin->>Browser: figma.openExternal(authURL)
+ Browser->>API: GET /auth/google
+ API->>Google: Redirect to Google consent screen
+ Google-->>API: GET /auth/google/callback?code=...
+ API->>Google: Exchange code for user info
+ Google-->>API: { email, name, picture, googleId }
+ API->>API: Upsert user in DB
+ API->>API: Generate JWT (30-day expiry)
+ API->>API: Store token in memory (TokenStore)
+ API-->>Browser: Redirect → success page
+ Browser-->>User: "Login successful, return to Figma"
+
+ loop Poll every 2 seconds (max 60s)
+ Plugin->>API: GET /auth/poll
+ alt Token ready
+ API-->>Plugin: { token: "eyJ..." }
+ Plugin->>Plugin: figma.clientStorage.set('token')
+ Plugin->>UI: postMessage(AUTH_SUCCESS, user)
+ else Not ready yet
+ API-->>Plugin: { token: null }
+ end
+ end
+
+ UI->>UI: Render HomeScreen
+```
+
+---
+
+### 9.2 AI Design Generation Flow
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor Designer
+ participant UI as Plugin UI (React)
+ participant Plugin as Plugin Sandbox
+ participant API as Backend API
+ participant AIOrch as AI Orchestrator\n(AiGenerateDesignService)
+ participant AIProvider as AI Provider\n(OpenAI/Claude/Gemini/...)
+ participant DB as PostgreSQL
+ participant Canvas as Figma Canvas
+
+ Designer->>UI: Types prompt, selects model, hits Generate
+ UI->>API: POST /api/designs/generate-from-conversation\n{ prompt, modelId, conversationHistory }
+
+ API->>API: Validate JWT + deduct points check
+ API->>AIOrch: execute(prompt, model, history)
+ AIOrch->>AIOrch: Build system prompt\n(load design system template)
+ AIOrch->>AIProvider: chat.completions(messages)
+
+ alt Success
+ AIProvider-->>AIOrch: { designJson, aiMessage, tokens }
+ AIOrch->>AIOrch: Extract icons from JSON
+ AIOrch->>AIOrch: Calculate cost & points
+ AIOrch-->>API: { resultDesign, inputTokens, outputTokens, cost }
+ API->>DB: INSERT design_generations (status=success)
+ API->>DB: UPDATE users SET pointsBalance -= deducted
+ API-->>UI: 200 { design, aiMessage, pointsBalance }
+ else AI Error
+ AIProvider-->>AIOrch: Error / malformed JSON
+ AIOrch-->>API: throw AiGenerationError
+ API->>DB: INSERT design_generations (status=failed)
+ API-->>UI: 422 { error: "Generation failed" }
+ end
+
+ UI->>Plugin: postMessage(IMPORT_DESIGN, { design })
+ Plugin->>Canvas: Create Figma nodes\n(FrameCreator, TextCreator, ...)
+ Canvas-->>Plugin: Nodes created
+ Plugin-->>UI: postMessage(IMPORT_COMPLETE)
+ UI->>Designer: Show success toast + updated points balance
+```
+
+---
+
+### 9.3 Payment & Points Flow
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor User
+ participant UI as Plugin UI
+ participant API as Backend API
+ participant Stripe as Stripe API
+ participant Webhook as Stripe Webhook
+ participant DB as PostgreSQL
+
+ User->>UI: Clicks "Buy Points" → selects package
+ UI->>API: POST /api/payments/create-checkout\n{ packageId }
+ API->>Stripe: sessions.create({ price, metadata })
+ Stripe-->>API: { sessionId, url }
+ API-->>UI: { checkoutUrl, sessionId }
+ UI->>UI: Open Stripe checkout in browser
+
+ User->>Stripe: Completes payment
+ Stripe->>Webhook: POST /api/payments/webhook\n(checkout.session.completed)
+ Webhook->>Webhook: Verify Stripe signature
+ Webhook->>DB: UPDATE payment_transactions SET status=completed
+ Webhook->>DB: UPDATE users SET pointsBalance += purchased
+ Webhook-->>Stripe: 200 OK
+
+ loop Poll every 2s (max 60s)
+ UI->>API: GET /api/payments/poll/:sessionId
+ alt Payment confirmed
+ API-->>UI: { status: 'completed', newBalance: 3750 }
+ UI->>UI: Update displayed points balance
+ UI->>User: "Points added successfully!"
+ else Pending
+ API-->>UI: { status: 'pending' }
+ end
+ end
+```
+
+---
+
+### 9.4 Subscription Flow
+
+```mermaid
+flowchart TD
+ A([User selects plan]) --> B[POST /api/subscriptions/create-checkout]
+ B --> C[Stripe subscription checkout]
+ C --> D{User completes\npayment?}
+ D -->|Yes| E[Stripe webhook fires\nsubscription.created]
+ D -->|No| F([Checkout abandoned])
+ E --> G[Insert subscription record\nstatus=active]
+ G --> H[Set dailyPointsLimit\nper plan tier]
+
+ H --> I([Daily AI usage])
+ I --> J{dailyPointsUsed\n< dailyPointsLimit?}
+ J -->|Yes| K[Allow AI generation\nIncrement dailyPointsUsed]
+ J -->|No| L[Return 429 Daily limit reached]
+
+ K --> M{Next calendar day?}
+ M -->|Yes| N[Reset dailyPointsUsed = 0\nUpdate lastUsageResetDate]
+ N --> I
+
+ O([User cancels]) --> P[POST /api/subscriptions/cancel]
+ P --> Q[Stripe: cancelAtPeriodEnd = true]
+ Q --> R[Subscription expires at\ncurrentPeriodEnd]
+ R --> S[status = expired]
+```
+
+---
+
+### 9.5 UI Library Component Flow
+
+```mermaid
+flowchart LR
+ subgraph Figma["Figma Canvas"]
+ SEL["User selects\nnodes"]
+ end
+
+ subgraph Plugin["Plugin Sandbox"]
+ EXP["NodeExporter\n(Figma → JSON)"]
+ IMG["ImageOptimizer\n(rasterize preview)"]
+ end
+
+ subgraph API["Backend API"]
+ direction TB
+ UP["POST /components\n(save JSON)"]
+ S3U["POST /upload-image\n(S3 upload)"]
+ LIST["GET /projects/:id/components"]
+ DEL["DELETE /components/:id\n+ S3 delete"]
+ end
+
+ subgraph Storage["External Storage"]
+ S3[("AWS S3\nbucket")]
+ PG[("PostgreSQL\nui_library_components")]
+ end
+
+ SEL --> EXP --> Plugin
+ Plugin -->|exportJson| UP --> PG
+ Plugin -->|previewPng| S3U --> S3
+ PG -->|previewImage URL| LIST
+ LIST --> SEL2["User views\ncomponent grid"]
+ DEL --> PG & S3
+```
+
+---
-### AI Models — `/api/ai-models`
+### 9.6 Plugin Build Pipeline
-| Method | Path | Description |
-|---|---|---|
-| GET | `/api/ai-models` | List all available AI models |
+```mermaid
+flowchart TD
+ A([npm run build]) --> B{Mode?}
+ B -->|--prod| C[minify: true\ndrop console logs]
+ B -->|default| D[minify: false\nkeep logs]
-### Client Errors — `/api/errors`
+ C & D --> E[esbuild: bundle UI\nentryPoint: index.jsx\nbundle: true\nloader: jsx,tsx,css,js,ts]
-| Method | Path | Description |
-|---|---|---|
-| POST | `/api/errors` | Log a client-side error |
+ E --> F[Inline CSS into\n<style> tag]
+ F --> G[Escape </script>\ninjection sequences]
+ G --> H["Write dist/ui.html\n(self-contained)"]
-All protected routes require `Authorization: Bearer ` header.
-Interactive API docs available at `/api-docs` when the server is running.
+ C & D --> I[esbuild: bundle Plugin\nentryPoint: main.ts\nplatform: browser\ntarget: es2017]
+ I --> J[Write dist/code.js]
+
+ H & J --> K([Load in Figma\nDesktop App])
+```
---
-## Authentication
+### 9.7 Request Middleware Pipeline
+
+```mermaid
+flowchart LR
+ REQ([Incoming\nHTTP Request])
+ --> COMP[compression\ngzip level 6]
+ --> LOG[Logger\nreq/res logging]
+ --> CORS[cors\nallowedDomains]
+ --> COOKIE[cookieParser]
+ --> BODY[bodyParser\nJSON 50MB]
+ --> AUTH{Protected\nroute?}
+
+ AUTH -->|Yes| JWT[authMiddleware\nverify JWT\nattach user]
+ AUTH -->|No| RATE
+
+ JWT --> RATE[Rate Limiter\nper-route limits]
+ RATE --> CONC{AI route?}
+ CONC -->|Yes| AICONC[concurrencyLimiter\nmax parallel AI]
+ CONC -->|No| VALID
+
+ AICONC --> VALID[Joi / Zod\nvalidation]
+ VALID --> CTRL[Controller\n→ Use Case]
+ CTRL --> RES([HTTP Response])
+```
+
+---
-1. **Google OAuth** — user opens the plugin, clicks "Sign in with Google", browser tab opens and completes OAuth flow.
-2. **JWT** — after OAuth the server issues a 30-day JWT.
-3. **Plugin polling** — the plugin's main thread calls `GET /auth/poll` repeatedly until a token is returned, then stores it in `figma.clientStorage`.
-4. **API requests** — the React UI's `useApiClient` hook reads the stored token and injects it as a `Bearer` header on every request.
+## 10. UML Class Diagram
+
+```mermaid
+classDiagram
+ class User {
+ +UUID id
+ +string figmaUserId
+ +string email
+ +int pointsBalance
+ +string stripeCustomerId
+ +boolean hasPurchased
+ }
+
+ class DesignGeneration {
+ +UUID id
+ +string operationType
+ +string modelId
+ +string prompt
+ +object resultDesign
+ +string status
+ +int pointsDeducted
+ }
+
+ class PaymentTransaction {
+ +UUID id
+ +string stripeSessionId
+ +string packageName
+ +int pointsPurchased
+ +decimal amountPaid
+ +string status
+ }
+
+ class Subscription {
+ +UUID id
+ +string planId
+ +string status
+ +int dailyPointsLimit
+ +int dailyPointsUsed
+ +Date currentPeriodEnd
+ +boolean cancelAtPeriodEnd
+ }
+
+ class UILibraryProject {
+ +UUID id
+ +string name
+ }
+
+ class UILibraryComponent {
+ +UUID id
+ +string name
+ +object designJson
+ +string previewImage
+ }
+
+ class AiGenerateDesignService {
+ +generateFromConversation(prompt, model, history)
+ +editWithAi(prompt, model, currentDesign)
+ +generateVariant(prompt, model, refDesign)
+ +generatePrototype(model, design)
+ -buildSystemPrompt(designSystemId)
+ -extractIcons(json)
+ -calculateCost(inputTokens, outputTokens, model)
+ }
+
+ class StripeService {
+ +createCheckoutSession(package)
+ +createSubscriptionSession(plan)
+ +handleWebhook(event)
+ +cancelSubscription(subId)
+ }
+
+ class PointsService {
+ +deductPoints(userId, amount)
+ +addPoints(userId, amount)
+ +getBalance(userId)
+ +checkDailyLimit(userId)
+ }
+
+ class JwtService {
+ +sign(payload) string
+ +verify(token) payload
+ }
+
+ class S3Service {
+ +uploadImage(buffer, key) string
+ +deleteImage(key)
+ }
+
+ User "1" --> "0..*" DesignGeneration : has
+ User "1" --> "0..*" PaymentTransaction : has
+ User "1" --> "0..1" Subscription : has
+ User "1" --> "0..*" UILibraryProject : owns
+ UILibraryProject "1" --> "0..*" UILibraryComponent : contains
+
+ AiGenerateDesignService ..> PointsService : uses
+ AiGenerateDesignService ..> DesignGeneration : creates
+ StripeService ..> PaymentTransaction : creates
+ StripeService ..> Subscription : manages
+ PointsService ..> User : updates
+```
---
-## Environment Variables
+## 11. AI Models & Pricing
+
+| Model | Provider | Input ($/1M tokens) | Output ($/1M tokens) | Max Tokens | Vision |
+| ---------------- | --------- | ------------------- | -------------------- | ---------- | ------ |
+| Devstral Latest | Mistral | $0.40 | $2.00 | 262,144 | No |
+| Gemini 2.5 Flash | Google | $0.30 | $2.50 | 1,000,000 | Yes |
+| Claude Opus 4.6 | Anthropic | $5.00 | $25.00 | 131,072 | Yes |
+| GPT-5.2 | OpenAI | $1.75 | $14.00 | 128,000 | Yes |
+| Gemini 3.1 Pro | Google | $2.00 | $12.00 | 2,000,000 | Yes |
+
+> Points are calculated as: `cost_in_USD × 500 points_per_dollar`
+
+---
+
+## 12. Monetization Model
+
+### One-Time Point Packages
+
+| Package | Points | Price | Margin |
+| ------- | ---------- | ------ | ------ |
+| Starter | 3,750 pts | $10.00 | 25% |
+| Pro | 10,000 pts | $25.00 | 20% |
+
+### Monthly Subscriptions
+
+| Plan | Price/month | Daily Points | Total/month | Margin |
+| ------- | ----------- | ------------- | ----------- | ------ |
+| Basic | $50 | 708 pts/day | ~21,250 pts | 15% |
+| Premium | $80 | 1,200 pts/day | ~36,000 pts | 10% |
+
+```mermaid
+flowchart LR
+ subgraph Billing["Billing Options"]
+ PP["Pay Per Use\n(Points Packages)"]
+ SUB["Subscription\n(Daily Allowance)"]
+ end
+
+ PP -->|One-time payment| S["Stripe Checkout\n(one-time)"]
+ SUB -->|Monthly recurring| SS["Stripe Checkout\n(subscription)"]
+
+ S --> W["Webhook\ncheckout.session.completed"]
+ SS --> W2["Webhook\nsubscription.created\n+ invoice.paid"]
+
+ W --> DB1[("Add points\nto balance")]
+ W2 --> DB2[("Create subscription\nSet dailyPointsLimit")]
+
+ DB1 --> AI[AI Generation\nDeduct from balance]
+ DB2 --> AI2[AI Generation\nDeduct from dailyUsed]
+```
+
+---
+
+## 13. Security & Rate Limiting
+
+### Rate Limits
+
+| Endpoint Group | Limit | Window | Key |
+| --------------------------------------- | ------------ | ---------- | ------------ |
+| Auth (`/auth/*`) | 50 requests | per minute | IP |
+| AI Generation (`/api/designs/*`) | 5 requests | per minute | user or IP |
+| UI Library (`/api/ui-library/*`) | 5 requests | per minute | user or IP |
+| Payments (`/api/payments/*`) | 5 requests | per minute | user or IP |
+| Subscriptions (`/api/subscriptions/*`) | 5 requests | per minute | user or IP |
+| Webhooks (`/api/payments/webhook`) | 100 requests | per minute | IP |
+
+### Concurrency Limit
+
+- Maximum **2 concurrent AI requests per user** enforced via a counting semaphore middleware (`aiConcurrencyLimiter`)
+- Returns `429` immediately if the user already has 2 in-flight AI requests
+- Slot released on response `finish` or `close` event
+- Prevents API key quota exhaustion and runaway costs
+
+### Payload Limits
+
+| Check | Limit |
+| -------------------- | --------------------------------------- |
+| JSON body parser | 50 MB |
+| Uploaded image (S3) | 5 MB |
+| MIME type validation | `image/png`, `image/jpeg`, `image/webp` |
+
+### Authentication Security
+
+- JWT verified on every protected request
+- Tokens stored only in `figma.clientStorage` (sandboxed per plugin)
+- Stripe webhook signature verified via `stripe.webhooks.constructEvent()`
+- Google OAuth state parameter for CSRF protection
+
+---
+
+## 14. Environment Variables
### Backend — `backend/.env`
@@ -374,6 +1010,7 @@ GOOGLE_CALLBACK_URL=http://localhost:5000/auth/google/callback
# JWT
JWT_SECRET=your_jwt_secret_change_in_production
+JWT_EXPIRY=30d
# Stripe
STRIPE_SECRET_KEY=sk_test_...
@@ -392,10 +1029,11 @@ AWS_S3_BUCKET=your_bucket_name
# AI Providers
OPENAI_API_KEY=sk-...
CLAUDE_API_KEY=sk-ant-...
-GEMINI_API_KEY=...
+GEMINI_API_KEY=AI...
MISTRAL_API_KEY=...
-HAMGINGFACE_API_KEY=...
+HAMGINGFACE_API_KEY=hf_...
POE_API_KEY=...
+OPEN_ROUTER_API_KEY=...
# Trello (optional)
TRELLO_API_KEY=...
@@ -406,30 +1044,36 @@ TRELLO_BOARD_ID=...
### Figma Plugin — `figma-plugin/.env`
```env
+# Local development
BACKEND_URL=http://localhost:5000
```
-> For production, set `BACKEND_URL` in `figma-plugin/.env.production`.
+```env
+# figma-plugin/.env.production
+BACKEND_URL=https://task-creator-api.onrender.com
+```
---
-## Development Setup
+## 15. Development Setup
### Prerequisites
-- Node.js 18+ (LTS)
-- PostgreSQL 14+
-- Figma Desktop App
-- Stripe CLI (for webhook testing)
+| Tool | Version | Purpose |
+| ------------- | ------- | ---------------------- |
+| Node.js | 18+ LTS | Backend & plugin build |
+| PostgreSQL | 14+ | Primary database |
+| Figma Desktop | Latest | Plugin development |
+| Stripe CLI | Latest | Webhook testing |
-### 1. Clone the repository
+### Step 1 — Clone
```bash
git clone https://github.com/Rezkaudi/task-creator.git
cd task-creator
```
-### 2. Backend
+### Step 2 — Backend
```bash
cd backend
@@ -437,95 +1081,272 @@ npm install
# Copy and fill in environment variables
cp .env.example .env
+# Edit .env with your credentials
# Run database migrations
npm run migration:run
-# Start development server
+# Start development server (nodemon + ts-node)
npm run dev
+# → API running at http://localhost:5000
+# → Swagger UI at http://localhost:5000/api-docs
```
-The API will be available at `http://localhost:5000`.
-Swagger docs: `http://localhost:5000/api-docs`.
-
-### 3. Figma Plugin
+### Step 3 — Figma Plugin
```bash
-cd figma-plugin
+cd ../figma-plugin
npm install
-# Development build (watches for changes)
+# Development build with watch mode
npm run dev
-
-# OR production build
-npm run build
+# → Outputs dist/code.js and dist/ui.html
```
-### 4. Load the plugin in Figma
+### Step 4 — Load Plugin in Figma
-1. Open the **Figma Desktop App**.
-2. Go to **Plugins → Development → Import plugin from manifest…**
-3. Select `figma-plugin/manifest.json`.
-4. Run the plugin — it will connect to your local backend.
+1. Open **Figma Desktop**
+2. Menu → **Plugins** → **Development** → **Import plugin from manifest…**
+3. Select `figma-plugin/manifest.json`
+4. Run the plugin — it will connect to `http://localhost:5000`
-### 5. Stripe webhooks (local)
+### Step 5 — Stripe Webhooks (local)
```bash
+# Forward Stripe events to local server
stripe listen --forward-to http://localhost:5000/api/payments/webhook
+
+# Test a payment event
+stripe trigger checkout.session.completed
```
---
-## Scripts
+## 16. Scripts Reference
### Backend (`backend/`)
-| Command | Description |
-|---|---|
-| `npm run dev` | Development server with nodemon + ts-node |
-| `npm run build` | Compile TypeScript and copy public assets |
-| `npm start` | Run compiled production server |
-| `npm run migration:generate` | Generate a new migration from entity changes |
-| `npm run migration:run` | Apply pending migrations |
-| `npm run migration:revert` | Revert last migration |
-| `npm run migration:show` | Show migration status |
+| Command | Description |
+| ---------------------------- | --------------------------------------- |
+| `npm run dev` | Development server — nodemon + ts-node |
+| `npm run build` | Compile TypeScript + copy public assets |
+| `npm start` | Run compiled production server |
+| `npm run migration:generate` | Generate migration from entity changes |
+| `npm run migration:run` | Apply all pending migrations |
+| `npm run migration:revert` | Revert most recent migration |
+| `npm run migration:show` | Show migration status table |
### Figma Plugin (`figma-plugin/`)
-| Command | Description |
-|---|---|
-| `npm run dev` | esbuild watch mode (local development) |
-| `npm run build` | Minified production build |
+| Command | Description |
+| --------------------- | ---------------------------------- |
+| `npm run dev` | esbuild watch mode (local dev) |
+| `npm run build` | Minified production build |
| `npm run build:local` | Unminified build for local testing |
-| `npm run clean` | Remove `dist/` folder |
-| `npm run rebuild` | Clean then build |
+| `npm run clean` | Remove `dist/` folder |
+| `npm run rebuild` | `clean` + `build` |
---
-## Deployment
+## 17. Deployment
-| Service | URL |
-|---|---|
-| Backend (Render) | `https://task-creator-api.onrender.com` |
-| Web redirect | `https://rio-app.design` |
+### Backend (Render.com)
+
+| Setting | Value |
+| ------------- | --------------------------------------------------- |
+| Build Command | `npm run build` |
+| Start Command | `npm start` |
+| Environment | All variables from [§14](#14-environment-variables) |
+| Live URL | `https://task-creator-api.onrender.com` |
**Production checklist:**
-- Set all env vars in Render dashboard (never commit secrets)
-- Set `NODE_ENV=production`
-- Use a strong random `JWT_SECRET`
-- Add production `STRIPE_SECRET_KEY` and re-register the Stripe webhook
-- Set `BACKEND_URL` in `figma-plugin/.env.production` and run `npm run build`
-- Submit the built plugin to the Figma Community or distribute `manifest.json` + `dist/`
+
+- [ ] Set all env vars in Render dashboard (never commit secrets)
+- [ ] `NODE_ENV=production`
+- [ ] Use a strong random `JWT_SECRET` (≥ 64 chars)
+- [ ] Register Stripe production webhook pointing to `https://...onrender.com/api/payments/webhook`
+- [ ] Verify AWS S3 bucket CORS policy allows Figma plugin origin
+
+### Figma Plugin
+
+```bash
+# Build for production (minified, points to production API)
+cd figma-plugin
+npm run build
+
+# Distribute via Figma Community
+# Or share manifest.json + dist/ folder
+```
+
+**Plugin checklist:**
+
+- [ ] Set `BACKEND_URL` in `.env.production`
+- [ ] Run `npm run build` (not `build:local`)
+- [ ] Update `manifest.json` version number
+- [ ] Submit to Figma Community or share `dist/` + `manifest.json`
---
-## Diagrams
+## 18. Points Lifecycle & Deduction Logic
-### Clean Architecture UML
-
+```mermaid
+flowchart TD
+ START([User triggers AI generation]) --> CHECK_SUB{Active\nsubscription?}
-### Data Flow Sequence Diagram
-
+ CHECK_SUB -->|Yes| CHECK_DAILY{dailyPointsUsed\n< dailyPointsLimit?}
+ CHECK_SUB -->|No| CHECK_BAL{pointsBalance\n> required?}
-### Entity Relationship Diagram
-
+ CHECK_DAILY -->|No| ERR1([429 — Daily limit reached\nUpgrade plan or wait for reset])
+ CHECK_DAILY -->|Yes| CALL_AI
+
+ CHECK_BAL -->|No| ERR2([402 — Insufficient points\nBuy more points])
+ CHECK_BAL -->|Yes| CALL_AI
+
+ CALL_AI[Send prompt to AI provider] --> AI_RESP{AI response\nsuccessful?}
+
+ AI_RESP -->|No| FAIL[Log failed generation\nDo NOT deduct points]
+ FAIL --> ERR3([422 — Generation failed])
+
+ AI_RESP -->|Yes| CALC[Calculate tokens used\ninput × inputRate + output × outputRate]
+ CALC --> CONV[Convert cost → points\ncost × 500 pts/dollar]
+ CONV --> DEDUCT{Source?}
+
+ DEDUCT -->|Subscription| UPD1[UPDATE subscriptions\nSET dailyPointsUsed += deducted]
+ DEDUCT -->|Balance| UPD2[UPDATE users\nSET pointsBalance -= deducted]
+
+ UPD1 & UPD2 --> SAVE[INSERT design_generations\nstatus=success, pointsDeducted]
+ SAVE --> RETURN([Return design JSON\n+ updated balance])
+```
+
+---
+
+## 19. Node Creation Pipeline (Figma Canvas)
+
+```mermaid
+flowchart TD
+ INPUT([Receive JSON from API]) --> PARSE[DesignDataParser\nvalidate & normalize]
+ PARSE --> ROOT{Root node\ntype?}
+
+ ROOT -->|FRAME| FC[FrameCreator\napply layout, padding, constraints]
+ ROOT -->|RECTANGLE| RC[RectCreator\napply fills, strokes, radius]
+ ROOT -->|TEXT| TC[TextCreator\napply font, size, weight, color]
+ ROOT -->|ELLIPSE / POLYGON| SC[ShapeCreator]
+ ROOT -->|COMPONENT| CC[ComponentCreator\nresolve from library]
+ ROOT -->|GROUP| GC[GroupCreator\nwrap children]
+
+ FC & RC & TC & SC & CC & GC --> FILL[FillMapper\nsolid · gradient · image fills]
+ FILL --> EFFECT[EffectMapper\ndrop shadow · blur · inner shadow]
+ EFFECT --> CONSTRAINT[Apply Constraints\nfixed · fill · hug parent]
+ CONSTRAINT --> CHILDREN{Has\nchildren?}
+
+ CHILDREN -->|Yes| RECURSE[Recurse into children\n← same pipeline]
+ CHILDREN -->|No| APPEND[Append node to parent\nor canvas root]
+ RECURSE --> APPEND
+
+ APPEND --> NOTIFY[figma-notification.port\nShow success toast]
+ NOTIFY --> DONE([Design rendered on canvas])
+```
+
+---
+
+## 20. Export Pipeline (Figma → JSON)
+
+```mermaid
+flowchart LR
+ SEL([User selects nodes\nin Figma]) --> HANDLER[selection-change.handler\ndetect selection]
+ HANDLER --> UC[ExportSelectedUseCase\nor ExportAllUseCase]
+ UC --> REPO[FigmaNodeRepository\nread node tree]
+
+ REPO --> EXP[NodeExporter\ntraverse node tree]
+
+ EXP --> MAP1[NodeTypeMapper\nFigma type → domain type]
+ MAP1 --> MAP2[FillMapper\npaints → Fill value objects]
+ MAP2 --> MAP3[EffectMapper\neffects → Effect value objects]
+ MAP3 --> BUILD[Build DesignNode\ndomain entity]
+
+ BUILD --> CHILDREN2{Children?}
+ CHILDREN2 -->|Yes| RECURSE2[Recurse subtree]
+ RECURSE2 --> BUILD
+ CHILDREN2 -->|No| JSON[Serialize to JSON\nclean portable schema]
+
+ JSON --> OUT{Output\nchoice}
+ OUT -->|Copy| CLIP[Copy to clipboard]
+ OUT -->|Download| FILE[Download .json file]
+ OUT -->|Save version| API[POST /api/design-generations]
+```
+
+---
+
+## 21. Error Handling Architecture
+
+```mermaid
+flowchart TD
+ subgraph Plugin["Plugin (Client Side)"]
+ ERR_UI[React error boundary\nor try/catch in hook]
+ ERR_UI --> REPORT["POST /api/errors\n{ errorType, message, stack }"]
+ ERR_UI --> TOAST[react-toastify\nshow user-friendly message]
+ end
+
+ subgraph Backend["Backend (Server Side)"]
+ direction LR
+ CTRL_ERR[Controller catches\nunhandled exception]
+ CTRL_ERR --> CUSTOM{Custom\nHTTP error?}
+ CUSTOM -->|Yes| TYPED[AppError subclass\nstatus + message]
+ CUSTOM -->|No| GENERIC[500 Internal Server Error]
+ TYPED & GENERIC --> MWARE[Error middleware\nformat JSON response]
+ MWARE --> LOG2[Logger middleware\nlog to stdout]
+ end
+
+ REPORT --> DB_ERR[("INSERT client_errors\ntable")]
+ MWARE --> RES(["HTTP error response\n{ error, message, status }"])
+```
+
+---
+
+## 22. Full System State Machine
+
+```mermaid
+stateDiagram-v2
+ [*] --> Unauthenticated : Plugin opens
+
+ Unauthenticated --> Authenticating : Click "Sign in with Google"
+ Authenticating --> Authenticated : JWT received via polling
+ Authenticating --> Unauthenticated : Timeout / error
+
+ Authenticated --> Idle : Home screen loaded
+
+ Idle --> GeneratingDesign : Submit AI prompt
+ Idle --> ExportingDesign : Click Export
+ Idle --> ImportingJSON : Paste JSON
+ Idle --> ViewingLibrary : Open UI Library
+ Idle --> ViewingHistory : Open Design Versions
+ Idle --> Purchasing : Click Buy Points
+
+ GeneratingDesign --> Idle : Success — nodes created
+ GeneratingDesign --> Idle : Failed — toast shown
+
+ ExportingDesign --> Idle : JSON copied / downloaded
+
+ ImportingJSON --> Idle : Nodes created on canvas
+
+ ViewingLibrary --> CreatingComponent : Save selection
+ CreatingComponent --> ViewingLibrary : Saved
+ ViewingLibrary --> Idle : Close
+
+ ViewingHistory --> RestoringVersion : Click version
+ RestoringVersion --> Idle : Nodes restored
+
+ Purchasing --> AwaitingPayment : Stripe checkout opened
+ AwaitingPayment --> Idle : Payment confirmed + points added
+ AwaitingPayment --> Idle : Checkout abandoned
+
+ Authenticated --> Unauthenticated : Sign out / token expired
+```
+
+---
+
+
+
+**Rio** — Built with Clean Architecture · Powered by 7+ AI providers · Secured by Stripe & Google OAuth
+
+
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 98bdb25..9facba4 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -24,6 +24,7 @@
"dotenv": "^16.4.7",
"express": "^5.1.0",
"express-async-handler": "^1.2.0",
+ "express-rate-limit": "^8.3.1",
"express-validator": "^7.3.1",
"google-auth-library": "^10.5.0",
"joi": "^18.0.2",
@@ -2864,6 +2865,23 @@
"integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==",
"license": "MIT"
},
+ "node_modules/express-rate-limit": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
+ "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
+ "dependencies": {
+ "ip-address": "10.1.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
"node_modules/express-validator": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
@@ -3448,6 +3466,14 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
diff --git a/backend/package.json b/backend/package.json
index 12cc353..bc1e80f 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -32,6 +32,7 @@
"dotenv": "^16.4.7",
"express": "^5.1.0",
"express-async-handler": "^1.2.0",
+ "express-rate-limit": "^8.3.1",
"express-validator": "^7.3.1",
"google-auth-library": "^10.5.0",
"joi": "^18.0.2",
diff --git a/backend/public/prompt/design/instructions/icon-instructions.txt b/backend/public/prompt/design/instructions/icon-instructions.txt
index fa72352..1992ffc 100644
--- a/backend/public/prompt/design/instructions/icon-instructions.txt
+++ b/backend/public/prompt/design/instructions/icon-instructions.txt
@@ -12,11 +12,14 @@ When you need icons NOT in the reference list (or when no list is provided):
Before generating the design, scan the entire layout and identify every icon you will need.
Then call searchIcons for ALL of them simultaneously in a SINGLE response — do NOT search incrementally across multiple rounds.
-Each searchIcons call returns objects with 'id' and 'url' fields.
-Use the 'url' value directly as the imageUrl — no additional tool calls needed.
+Each searchIcons call returns an array of icon ID strings (e.g. "mdi:home", "lucide:arrow-right").
+Construct the icon URL from the ID using this pattern:
+https://api.iconify.design/{prefix}/{name}.svg
+where prefix and name are the two parts split by ':'.
+Example: "mdi:home" → https://api.iconify.design/mdi/home.svg
Example result from searchIcons:
- { "icons": [{ "id": "mdi:home", "url": "https://api.iconify.design/mdi/home.svg" }, ...] }
+ { "icons": ["mdi:home", "mdi:home-outline", "mdi:home-variant"] }
Then use RECTANGLE with IMAGE fill:
diff --git a/backend/src/application/use-cases/ui-library/delete-ui-library-component.use-case.ts b/backend/src/application/use-cases/ui-library/delete-ui-library-component.use-case.ts
index 09c0986..78752c9 100644
--- a/backend/src/application/use-cases/ui-library/delete-ui-library-component.use-case.ts
+++ b/backend/src/application/use-cases/ui-library/delete-ui-library-component.use-case.ts
@@ -9,6 +9,9 @@ export class DeleteUILibraryComponentUseCase {
async execute(componentId: string, userId: string): Promise {
const component = await this.uiLibraryRepository.findComponentById(componentId, userId);
+ if (!component) {
+ throw new Error('Component not found');
+ }
await this.uiLibraryRepository.deleteComponent(componentId, userId);
diff --git a/backend/src/application/use-cases/ui-library/delete-ui-library-project.use-case.ts b/backend/src/application/use-cases/ui-library/delete-ui-library-project.use-case.ts
index b155371..4848a81 100644
--- a/backend/src/application/use-cases/ui-library/delete-ui-library-project.use-case.ts
+++ b/backend/src/application/use-cases/ui-library/delete-ui-library-project.use-case.ts
@@ -4,6 +4,10 @@ export class DeleteUILibraryProjectUseCase {
constructor(private readonly uiLibraryRepository: IUILibraryRepository) {}
async execute(projectId: string, userId: string): Promise {
+ const project = await this.uiLibraryRepository.findProjectById(projectId, userId);
+ if (!project) {
+ throw new Error('Project not found');
+ }
await this.uiLibraryRepository.deleteProject(projectId, userId);
}
}
diff --git a/backend/src/infrastructure/config/env.config.ts b/backend/src/infrastructure/config/env.config.ts
index ae54717..456fd35 100644
--- a/backend/src/infrastructure/config/env.config.ts
+++ b/backend/src/infrastructure/config/env.config.ts
@@ -52,4 +52,11 @@ export const ENV_CONFIG = {
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY!,
AWS_REGION: process.env.AWS_REGION || 'us-east-1',
AWS_S3_BUCKET: process.env.AWS_S3_BUCKET!,
+
+ // Icon & Tool Call Configuration
+ ICON_SEARCH_TIMEOUT_MS: Number(process.env.ICON_SEARCH_TIMEOUT_MS || 5000),
+ ICON_SEARCH_LIMIT: Number(process.env.ICON_SEARCH_LIMIT || 10),
+ ICON_CACHE_TTL_MS: Number(process.env.ICON_CACHE_TTL_MS || 600000), // 10 minutes
+ MAX_CONCURRENT_TOOL_CALLS: Number(process.env.MAX_CONCURRENT_TOOL_CALLS || 5),
+ MAX_TOOL_CALL_ROUNDS: Number(process.env.MAX_TOOL_CALL_ROUNDS || 3),
};
diff --git a/backend/src/infrastructure/services/ai/ai-generate-design.service.ts b/backend/src/infrastructure/services/ai/ai-generate-design.service.ts
index 5272070..4aa9f9b 100644
--- a/backend/src/infrastructure/services/ai/ai-generate-design.service.ts
+++ b/backend/src/infrastructure/services/ai/ai-generate-design.service.ts
@@ -9,6 +9,7 @@ import { ConversationMessage, DesignGenerationResult, IAiDesignService } from '.
import { iconTools } from '../../config/ai-tools.config';
import { AIModelConfig, getModelById } from '../../config/ai-models.config';
+import { ENV_CONFIG } from '../../config/env.config';
import { ToolCallHandlerService, FunctionToolCall } from './tool-call-handler.service';
import { ResponseParserService } from './response-parser.service';
@@ -297,10 +298,13 @@ export class AiGenerateDesignService implements IAiDesignService {
totalInputTokens += completion.usage?.prompt_tokens ?? 0;
totalOutputTokens += completion.usage?.completion_tokens ?? 0;
- // Handle tool calls loop
- while (completion.choices[0]?.message?.tool_calls) {
+ // Handle tool calls loop (capped to prevent runaway token costs)
+ const maxRounds = ENV_CONFIG.MAX_TOOL_CALL_ROUNDS;
+ let round = 0;
+ while (completion.choices[0]?.message?.tool_calls && round < maxRounds) {
+ round++;
const toolCalls = completion.choices[0].message.tool_calls as FunctionToolCall[];
- console.log(`--- Processing ${toolCalls.length} tool calls ---`);
+ console.log(`--- Processing ${toolCalls.length} tool calls (round ${round}/${maxRounds}) ---`);
const toolResults = await this.toolCallHandler.handleToolCalls(toolCalls);
// Add assistant message with tool calls
@@ -324,6 +328,17 @@ export class AiGenerateDesignService implements IAiDesignService {
totalOutputTokens += completion.usage?.completion_tokens ?? 0;
}
+ if (round >= maxRounds && completion.choices[0]?.message?.tool_calls) {
+ console.warn(`⚠️ Tool call loop hit max rounds (${maxRounds}). Forcing final completion without tools.`);
+ // Request one final completion without tools to get the text response
+ completion = await openai.chat.completions.create({
+ model: aiModel.id,
+ messages: messages as any,
+ });
+ totalInputTokens += completion.usage?.prompt_tokens ?? 0;
+ totalOutputTokens += completion.usage?.completion_tokens ?? 0;
+ }
+
const responseText = completion.choices[0]?.message?.content;
if (!responseText) {
diff --git a/backend/src/infrastructure/services/ai/icon-extractor.service.ts b/backend/src/infrastructure/services/ai/icon-extractor.service.ts
index 0c2c8dd..5132032 100644
--- a/backend/src/infrastructure/services/ai/icon-extractor.service.ts
+++ b/backend/src/infrastructure/services/ai/icon-extractor.service.ts
@@ -39,11 +39,11 @@ export class IconExtractorService {
*/
normalizeName(name: string): string {
return (name || '')
- // .toLowerCase()
- // .replace(/^(icon[s]?[\s/\-_]+|logo[s]?[\s/\-_]+|ic[\s/\-_]+)/i, '')
- // .replace(/([\s/\-_]+icon[s]?$|[\s/\-_]+logo[s]?$)/i, '')
- // .replace(/[\s\-_/]+/g, '')
- // .trim();
+ .toLowerCase()
+ .replace(/^(icon[s]?[\s/\-_]+|logo[s]?[\s/\-_]+|ic[\s/\-_]+)/i, '')
+ .replace(/([\s/\-_]+icon[s]?$|[\s/\-_]+logo[s]?$)/i, '')
+ .replace(/[\s\-_/]+/g, '')
+ .trim();
}
private walk(node: any, map: Map): void {
diff --git a/backend/src/infrastructure/services/ai/icon.service.ts b/backend/src/infrastructure/services/ai/icon.service.ts
index aa47f81..61352a8 100644
--- a/backend/src/infrastructure/services/ai/icon.service.ts
+++ b/backend/src/infrastructure/services/ai/icon.service.ts
@@ -1,21 +1,35 @@
// src/infrastructure/services/icon.service.ts
import { IIconService, IconSearchResult } from '../../../domain/services/IIconService';
+import { ENV_CONFIG } from '../../config/env.config';
-const ICON_SEARCH_TIMEOUT_MS = 5000;
-const ICON_SEARCH_LIMIT = 10;
+interface CacheEntry {
+ result: IconSearchResult;
+ expiry: number;
+}
export class IconService implements IIconService {
+ private cache = new Map();
async searchIcons(query: string): Promise {
- console.log(`🔍 Searching icons for: ${query}`);
+ const normalizedQuery = query.trim().toLowerCase();
+ if (!normalizedQuery) return { icons: [] };
+
+ // Check cache first
+ const cached = this.cache.get(normalizedQuery);
+ if (cached && Date.now() < cached.expiry) {
+ console.log(`⚡ Icon cache hit for: "${normalizedQuery}"`);
+ return cached.result;
+ }
+
+ console.log(`🔍 Searching icons for: "${normalizedQuery}"`);
const controller = new AbortController();
- const timeout = setTimeout(() => controller.abort(), ICON_SEARCH_TIMEOUT_MS);
+ const timeout = setTimeout(() => controller.abort(), ENV_CONFIG.ICON_SEARCH_TIMEOUT_MS);
try {
const response = await fetch(
- `https://api.iconify.design/search?query=${encodeURIComponent(query)}&limit=${ICON_SEARCH_LIMIT}`,
+ `https://api.iconify.design/search?query=${encodeURIComponent(normalizedQuery)}&limit=${ENV_CONFIG.ICON_SEARCH_LIMIT}`,
{ signal: controller.signal }
);
@@ -25,12 +39,24 @@ export class IconService implements IIconService {
const data = await response.json() as { icons?: string[] };
const icons: string[] = data.icons ?? [];
+ const result: IconSearchResult = { icons };
+
+ // Store in cache
+ this.cache.set(normalizedQuery, {
+ result,
+ expiry: Date.now() + ENV_CONFIG.ICON_CACHE_TTL_MS,
+ });
+
+ // Evict expired entries periodically (keep cache bounded)
+ if (this.cache.size > 200) {
+ this.evictExpired();
+ }
- return { icons };
+ return result;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
- console.warn(`⚠️ Icon search timed out for: ${query}`);
+ console.warn(`⚠️ Icon search timed out for: "${normalizedQuery}"`);
return { icons: [] };
}
@@ -39,4 +65,13 @@ export class IconService implements IIconService {
clearTimeout(timeout);
}
}
+
+ private evictExpired(): void {
+ const now = Date.now();
+ for (const [key, entry] of this.cache) {
+ if (now >= entry.expiry) {
+ this.cache.delete(key);
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/backend/src/infrastructure/services/ai/tool-call-handler.service.ts b/backend/src/infrastructure/services/ai/tool-call-handler.service.ts
index 45cd306..1c6ab7c 100644
--- a/backend/src/infrastructure/services/ai/tool-call-handler.service.ts
+++ b/backend/src/infrastructure/services/ai/tool-call-handler.service.ts
@@ -1,6 +1,7 @@
// src/infrastructure/services/tool-call-handler.service.ts
import { IIconService } from '../../../domain/services/IIconService';
+import { ENV_CONFIG } from '../../config/env.config';
export interface FunctionToolCall {
id: string;
@@ -24,8 +25,11 @@ export class ToolCallHandlerService {
console.log(`🛠️ [tools] start — ${toolCalls.length} tool call(s)`);
const start = Date.now();
- const results = await Promise.all(
- toolCalls.map(toolCall => this.handleSingleToolCall(toolCall))
+ // Run with concurrency throttle to avoid overwhelming external APIs
+ const results = await this.runWithConcurrency(
+ toolCalls,
+ tc => this.handleSingleToolCall(tc),
+ ENV_CONFIG.MAX_CONCURRENT_TOOL_CALLS
);
console.log(`✅ [tools] done — ${toolCalls.length} tool call(s) in ${Date.now() - start}ms`);
@@ -62,4 +66,32 @@ export class ToolCallHandlerService {
content: result,
};
}
+
+ /**
+ * Runs async tasks with a concurrency limit.
+ * Avoids blasting external APIs with unbounded parallel requests.
+ */
+ private async runWithConcurrency(
+ items: T[],
+ fn: (item: T) => Promise,
+ limit: number
+ ): Promise {
+ if (items.length <= limit) {
+ return Promise.all(items.map(fn));
+ }
+
+ const results: R[] = new Array(items.length);
+ let nextIndex = 0;
+
+ async function worker() {
+ while (nextIndex < items.length) {
+ const idx = nextIndex++;
+ results[idx] = await fn(items[idx]);
+ }
+ }
+
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
+ await Promise.all(workers);
+ return results;
+ }
}
diff --git a/backend/src/infrastructure/services/storage/s3.service.ts b/backend/src/infrastructure/services/storage/s3.service.ts
index ea35bc6..451b640 100644
--- a/backend/src/infrastructure/services/storage/s3.service.ts
+++ b/backend/src/infrastructure/services/storage/s3.service.ts
@@ -2,6 +2,47 @@ import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client
import { ENV_CONFIG } from '../../config/env.config';
import { randomUUID } from 'crypto';
+const ALLOWED_MIME_TYPES: Record = {
+ 'image/png': {
+ extension: 'png',
+ magic: [Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
+ },
+ 'image/jpeg': {
+ extension: 'jpg',
+ magic: [Buffer.from([0xff, 0xd8, 0xff])],
+ },
+ 'image/webp': {
+ extension: 'webp',
+ // RIFF????WEBP — bytes 0-3 are RIFF, bytes 8-11 are WEBP
+ magic: [Buffer.from([0x52, 0x49, 0x46, 0x46])],
+ },
+};
+
+const MAX_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
+
+function validateImageBuffer(buffer: Buffer, mimeType: string): void {
+ if (buffer.length > MAX_SIZE_BYTES) {
+ throw new Error(`File size ${buffer.length} exceeds the 5 MB limit`);
+ }
+
+ const spec = ALLOWED_MIME_TYPES[mimeType];
+ if (!spec) {
+ throw new Error(`MIME type "${mimeType}" is not allowed. Allowed types: ${Object.keys(ALLOWED_MIME_TYPES).join(', ')}`);
+ }
+
+ const matchesMagic = spec.magic.some(magic => buffer.slice(0, magic.length).equals(magic));
+
+ // WebP requires an additional check on bytes 8-11
+ if (mimeType === 'image/webp') {
+ const webpMarker = buffer.slice(8, 12).toString('ascii');
+ if (!matchesMagic || webpMarker !== 'WEBP') {
+ throw new Error('File content does not match declared MIME type image/webp');
+ }
+ } else if (!matchesMagic) {
+ throw new Error(`File content does not match declared MIME type ${mimeType}`);
+ }
+}
+
export class S3Service {
private readonly client: S3Client;
private readonly bucket: string;
@@ -19,14 +60,17 @@ export class S3Service {
}
async uploadBase64Image(base64DataUrl: string, folder = 'component-previews'): Promise {
- const matches = base64DataUrl.match(/^data:(.+);base64,(.+)$/);
+ const matches = base64DataUrl.match(/^data:([a-zA-Z0-9][a-zA-Z0-9!#$&\-^_]+\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_]+);base64,([A-Za-z0-9+/]+=*)$/);
if (!matches) {
throw new Error('Invalid base64 image format. Expected: data:;base64,');
}
const mimeType = matches[1];
const buffer = Buffer.from(matches[2], 'base64');
- const extension = mimeType.split('/')[1] || 'png';
+
+ validateImageBuffer(buffer, mimeType);
+
+ const extension = ALLOWED_MIME_TYPES[mimeType].extension;
const key = `${folder}/${randomUUID()}.${extension}`;
await this.client.send(new PutObjectCommand({
diff --git a/backend/src/infrastructure/web/controllers/ui-library.controller.ts b/backend/src/infrastructure/web/controllers/ui-library.controller.ts
index 9386329..076bb73 100644
--- a/backend/src/infrastructure/web/controllers/ui-library.controller.ts
+++ b/backend/src/infrastructure/web/controllers/ui-library.controller.ts
@@ -62,7 +62,9 @@ export class UILibraryController {
message: 'Project deleted successfully',
});
} catch (error) {
- next(error);
+ const message = error instanceof Error ? error.message : 'Failed to delete project';
+ const statusCode = message.toLowerCase().includes('not found') ? 404 : 500;
+ res.status(statusCode).json({ success: false, message });
}
}
@@ -119,7 +121,9 @@ export class UILibraryController {
message: 'Component deleted successfully',
});
} catch (error) {
- next(error);
+ const message = error instanceof Error ? error.message : 'Failed to delete component';
+ const statusCode = message.toLowerCase().includes('not found') ? 404 : 500;
+ res.status(statusCode).json({ success: false, message });
}
}
diff --git a/backend/src/infrastructure/web/middleware/concurrency-limit.middleware.ts b/backend/src/infrastructure/web/middleware/concurrency-limit.middleware.ts
new file mode 100644
index 0000000..4329740
--- /dev/null
+++ b/backend/src/infrastructure/web/middleware/concurrency-limit.middleware.ts
@@ -0,0 +1,68 @@
+import { Request, Response, NextFunction } from 'express';
+
+const MAX_CONCURRENT = 2;
+// const SLOT_TTL_MS = 5 * 60 * 1000; // 5 minutes — auto-release stuck slots
+
+interface Slot {
+ count: number;
+ lastUpdated: number;
+}
+
+const activeRequests = new Map();
+
+// // Cleanup stale slots every minute
+// setInterval(() => {
+// const now = Date.now();
+// for (const [userId, slot] of activeRequests) {
+// if (now - slot.lastUpdated > SLOT_TTL_MS) {
+// activeRequests.delete(userId);
+// }
+// }
+// }, 60 * 1000);
+
+function getSlot(userId: string): Slot {
+ let slot = activeRequests.get(userId);
+ if (!slot) {
+ slot = { count: 0, lastUpdated: Date.now() };
+ activeRequests.set(userId, slot);
+ }
+ return slot;
+}
+
+function release(userId: string): void {
+ const slot = activeRequests.get(userId);
+ if (!slot) return;
+ slot.count = Math.max(0, slot.count - 1);
+ slot.lastUpdated = Date.now();
+ if (slot.count === 0) {
+ activeRequests.delete(userId);
+ }
+}
+
+export function aiConcurrencyLimiter(req: Request, res: Response, next: NextFunction): void {
+ const userId: string | undefined = (req as any).user?.id?.toString();
+
+ if (!userId) {
+ next();
+ return;
+ }
+
+ const slot = getSlot(userId);
+
+ if (slot.count >= MAX_CONCURRENT) {
+ res.status(429).json({
+ success: false,
+ message: `You already have ${MAX_CONCURRENT} AI requests in progress. Please wait for one to finish.`,
+ });
+ return;
+ }
+
+ slot.count++;
+ slot.lastUpdated = Date.now();
+
+ // Release the slot when the response finishes (success, error, or client disconnect)
+ res.on('finish', () => release(userId));
+ res.on('close', () => release(userId));
+
+ next();
+}
diff --git a/backend/src/infrastructure/web/middleware/rate-limit.middleware.ts b/backend/src/infrastructure/web/middleware/rate-limit.middleware.ts
new file mode 100644
index 0000000..4783eff
--- /dev/null
+++ b/backend/src/infrastructure/web/middleware/rate-limit.middleware.ts
@@ -0,0 +1,58 @@
+import rateLimit, { ipKeyGenerator } from 'express-rate-limit';
+import { Request } from 'express';
+
+const userOrIpKey = (req: Request): string =>
+ (req as any).user?.id?.toString() ?? ipKeyGenerator(req.ip ?? '');
+
+const rateLimitResponse = (message: string) => ({
+ success: false,
+ message,
+});
+
+// Auth endpoints: 50 req/min per IP (brute-force protection)
+export const authLimiter = rateLimit({
+ windowMs: 60 * 1000,
+ max: 50,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: rateLimitResponse('Too many auth requests. Please try again in a minute.'),
+});
+
+// AI generation: 5 req/min per user (cost protection)
+export const aiLimiterPerMinute = rateLimit({
+ windowMs: 60 * 1000,
+ max: 5,
+ keyGenerator: userOrIpKey,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: rateLimitResponse('AI generation limit reached. Max 5 requests per minute.'),
+});
+
+// File upload: 5 req/min per user
+export const uploadLimiter = rateLimit({
+ windowMs: 60 * 1000,
+ max: 5,
+ keyGenerator: userOrIpKey,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: rateLimitResponse('Upload limit reached. Max 5 uploads per minute.'),
+});
+
+// Payment/subscription checkout: 5 req/min per user
+export const paymentLimiter = rateLimit({
+ windowMs: 60 * 1000,
+ max: 5,
+ keyGenerator: userOrIpKey,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: rateLimitResponse('Payment request limit reached. Max 5 requests per minute.'),
+});
+
+// Stripe webhook: 100 req/min (generous, Stripe retries legitimately)
+export const webhookLimiter = rateLimit({
+ windowMs: 60 * 1000,
+ max: 100,
+ standardHeaders: true,
+ legacyHeaders: false,
+ message: rateLimitResponse('Webhook rate limit exceeded.'),
+});
diff --git a/backend/src/infrastructure/web/server.ts b/backend/src/infrastructure/web/server.ts
index a4ffdcd..e570d6f 100644
--- a/backend/src/infrastructure/web/server.ts
+++ b/backend/src/infrastructure/web/server.ts
@@ -25,6 +25,14 @@ import designGenerationRoutes from './routes/design-generation.routes';
import { setupDependencies } from './dependencies';
import { logger } from './middleware/logger.middleware';
+import {
+ authLimiter,
+ aiLimiterPerMinute,
+ uploadLimiter,
+ paymentLimiter,
+ webhookLimiter,
+} from './middleware/rate-limit.middleware';
+import { aiConcurrencyLimiter } from './middleware/concurrency-limit.middleware';
export class Server {
private app: Application;
@@ -75,14 +83,16 @@ export class Server {
this.container.authMiddleware.requireAuthForApi(req, res, next);
});
- this.app.use('/auth', authRoutes(this.container.authController));
- this.app.use('/api/designs', designRoutes(this.container.designController));
+ this.app.use('/auth', authLimiter, authRoutes(this.container.authController));
+ this.app.use('/api/designs', aiLimiterPerMinute, aiConcurrencyLimiter, designRoutes(this.container.designController));
this.app.use('/api/ai-models', aiModelsRoutes(this.container.aiModelsController));
this.app.use('/api/design-systems', designSystemsRoutes(this.container.designSystemsController));
this.app.use('/api/errors', clientErrorRoutes(this.container.clientErrorController));
+ this.app.use('/api/ui-library/components/upload-image', uploadLimiter);
this.app.use('/api/ui-library', uiLibraryRoutes(this.container.uiLibraryController));
- this.app.use('/api/payments', paymentRoutes(this.container.paymentController));
- this.app.use('/api/subscriptions', subscriptionRoutes(this.container.subscriptionController));
+ this.app.use('/api/payments/webhook', webhookLimiter);
+ this.app.use('/api/payments', paymentLimiter, paymentRoutes(this.container.paymentController));
+ this.app.use('/api/subscriptions', paymentLimiter, subscriptionRoutes(this.container.subscriptionController));
this.app.use('/api/design-generations', designGenerationRoutes(this.container.designGenerationController));
this.app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
}
diff --git a/backend/src/infrastructure/web/validation/index.ts b/backend/src/infrastructure/web/validation/index.ts
index b1aba77..d1281ca 100644
--- a/backend/src/infrastructure/web/validation/index.ts
+++ b/backend/src/infrastructure/web/validation/index.ts
@@ -140,6 +140,7 @@ export const uploadComponentImageValidation = [
body('image')
.notEmpty().withMessage('Image is required')
.isString().withMessage('Image must be a string')
+ .isLength({ max: 5 * 1024 * 1024 }).withMessage('Image must be less than 5MB')
.matches(/^data:image\/(png|jpeg|jpg|webp);base64,/).withMessage('Image must be a valid base64 data URL'),
];
diff --git a/figma-plugin/src/domain/entities/design-node.ts b/figma-plugin/src/domain/entities/design-node.ts
index 3ff3647..a21c5fc 100644
--- a/figma-plugin/src/domain/entities/design-node.ts
+++ b/figma-plugin/src/domain/entities/design-node.ts
@@ -315,8 +315,8 @@ export interface DesignNode {
// Children
children?: DesignNode[];
- // Image data (for embedded images)
- imageData?: string;
+ // Image data (for embedded images) (disabled)
+ // imageData?: string;
// SVG URL for icon nodes (uses figma.createNodeFromSvg instead of image fill)
svgUrl?: string;
diff --git a/figma-plugin/src/domain/entities/fill.ts b/figma-plugin/src/domain/entities/fill.ts
index b6eea85..172b122 100644
--- a/figma-plugin/src/domain/entities/fill.ts
+++ b/figma-plugin/src/domain/entities/fill.ts
@@ -63,7 +63,7 @@ export interface Fill {
// Image fill
scaleMode?: 'FILL' | 'FIT' | 'CROP' | 'TILE';
imageHash?: string;
- imageData?: string; // Base64 encoded image data for export/import
+ // imageData?: string; // Base64 encoded image data for export/import (disabled)
imageUrl?: string; // URL to fetch image from (for import)
imageTransform?: [[number, number, number], [number, number, number]];
scalingFactor?: number;
diff --git a/figma-plugin/src/infrastructure/figma/exporters/node.exporter.ts b/figma-plugin/src/infrastructure/figma/exporters/node.exporter.ts
index 7533e2b..d809248 100644
--- a/figma-plugin/src/infrastructure/figma/exporters/node.exporter.ts
+++ b/figma-plugin/src/infrastructure/figma/exporters/node.exporter.ts
@@ -961,23 +961,23 @@ export class NodeExporter {
if (imagePaint.imageHash) {
fill.imageHash = imagePaint.imageHash;
- // Try to get base64 image data
- if (!this.imageCache.has(imagePaint.imageHash)) {
- try {
- const image = figma.getImageByHash(imagePaint.imageHash);
- if (image) {
- const bytes = await image.getBytesAsync();
- const base64 = this.bytesToBase64(bytes);
- this.imageCache.set(imagePaint.imageHash, base64);
- }
- } catch (e) {
- console.warn('Failed to export image:', e);
- }
- }
-
- if (this.imageCache.has(imagePaint.imageHash)) {
- fill.imageData = this.imageCache.get(imagePaint.imageHash);
- }
+ // imageData export disabled — not included in exported JSON
+ // if (!this.imageCache.has(imagePaint.imageHash)) {
+ // try {
+ // const image = figma.getImageByHash(imagePaint.imageHash);
+ // if (image) {
+ // const bytes = await image.getBytesAsync();
+ // const base64 = this.bytesToBase64(bytes);
+ // this.imageCache.set(imagePaint.imageHash, base64);
+ // }
+ // } catch (e) {
+ // console.warn('Failed to export image:', e);
+ // }
+ // }
+
+ // if (this.imageCache.has(imagePaint.imageHash)) {
+ // fill.imageData = this.imageCache.get(imagePaint.imageHash);
+ // }
}
if (imagePaint.imageTransform) {
diff --git a/figma-plugin/src/infrastructure/mappers/fill.mapper.ts b/figma-plugin/src/infrastructure/mappers/fill.mapper.ts
index a4f94fd..e4e6758 100644
--- a/figma-plugin/src/infrastructure/mappers/fill.mapper.ts
+++ b/figma-plugin/src/infrastructure/mappers/fill.mapper.ts
@@ -189,11 +189,11 @@ export class FillMapper {
if (fill.imageUrl) {
image = await FillMapper.fetchImageFromUrl(fill.imageUrl);
}
- // Priority 2: If we have base64 image data, create the image
- else if (fill.imageData) {
- const bytes = FillMapper.base64ToBytes(fill.imageData);
- image = await figma.createImage(bytes);
- }
+ // Priority 2: imageData disabled
+ // else if (fill.imageData) {
+ // const bytes = FillMapper.base64ToBytes(fill.imageData);
+ // image = await figma.createImage(bytes);
+ // }
// Priority 3: Try to get existing image by hash
else if (fill.imageHash) {
image = figma.getImageByHash(fill.imageHash);
diff --git a/figma-plugin/src/infrastructure/services/plugin-image-optimizer.service.ts b/figma-plugin/src/infrastructure/services/plugin-image-optimizer.service.ts
index edd654d..32d8185 100644
--- a/figma-plugin/src/infrastructure/services/plugin-image-optimizer.service.ts
+++ b/figma-plugin/src/infrastructure/services/plugin-image-optimizer.service.ts
@@ -12,7 +12,7 @@
export interface ImageReference {
path: string[];
imageHash: string;
- imageData: string;
+ // imageData: string; // disabled
scaleMode?: string;
}
@@ -89,18 +89,16 @@ export class ImageOptimizerService {
if (node.fills && Array.isArray(node.fills)) {
node.fills.forEach((fill: any, fillIndex: number) => {
if (fill.type === 'IMAGE' && fill.imageData) {
- const imagePath = [...currentPath, 'fills', fillIndex];
-
- imageReferences.push({
- path: imagePath,
- imageHash: fill.imageHash || '',
- imageData: fill.imageData,
- scaleMode: fill.scaleMode
- });
-
- // Remove imageData
- delete fill.imageData;
- fill._imageStripped = true;
+ // imageData disabled — skipping strip/restore cycle
+ // const imagePath = [...currentPath, 'fills', fillIndex];
+ // imageReferences.push({
+ // path: imagePath,
+ // imageHash: fill.imageHash || '',
+ // imageData: fill.imageData,
+ // scaleMode: fill.scaleMode
+ // });
+ // delete fill.imageData;
+ // fill._imageStripped = true;
}
});
}
@@ -142,14 +140,16 @@ export class ImageOptimizerService {
// Verify this is still an IMAGE fill
if (fill.type === 'IMAGE' && fill._imageStripped === true) {
- fill.imageData = imageRef.imageData;
+ // imageData disabled
+ // fill.imageData = imageRef.imageData;
if (imageRef.scaleMode) {
fill.scaleMode = imageRef.scaleMode;
}
delete fill._imageStripped;
return true;
} else if (fill.type === 'IMAGE' && fill.imageHash === imageRef.imageHash) {
- fill.imageData = imageRef.imageData;
+ // imageData disabled
+ // fill.imageData = imageRef.imageData;
if (imageRef.scaleMode) {
fill.scaleMode = imageRef.scaleMode;
}
@@ -167,7 +167,8 @@ export class ImageOptimizerService {
const found = this.findImageFillByHash(design, imageRef.imageHash);
if (found && found.fill) {
- found.fill.imageData = imageRef.imageData;
+ // imageData disabled
+ // found.fill.imageData = imageRef.imageData;
if (imageRef.scaleMode) {
found.fill.scaleMode = imageRef.scaleMode;
}
@@ -219,11 +220,8 @@ export class ImageOptimizerService {
return null;
}
- private estimateTokenSavings(imageReferences: ImageReference[]): number {
- let totalChars = 0;
- for (const ref of imageReferences) {
- totalChars += ref.imageData.length;
- }
- return Math.floor(totalChars / 4);
+ private estimateTokenSavings(_imageReferences: ImageReference[]): number {
+ // imageData disabled — always 0 savings
+ return 0;
}
}
\ No newline at end of file
diff --git a/figma-plugin/src/presentation/handlers/plugin-message.handler.ts b/figma-plugin/src/presentation/handlers/plugin-message.handler.ts
index e60c494..45c8338 100644
--- a/figma-plugin/src/presentation/handlers/plugin-message.handler.ts
+++ b/figma-plugin/src/presentation/handlers/plugin-message.handler.ts
@@ -7,7 +7,7 @@ import {
ExportSelectedUseCase,
ExportAllUseCase,
} from '../../application/use-cases';
-import { ApiConfig, defaultModel } from '../../shared/constants/plugin-config.js';
+import { ApiConfig, defaultModel, MAX_PAYLOAD_BYTES } from '../../shared/constants/plugin-config.js';
import { NodeExporter } from '../../infrastructure/figma/exporters/node.exporter';
import { GetUserInfoUseCase } from '@application/use-cases/getUserInfoUseCase';
import { errorReporter } from '../../infrastructure/services/error-reporter.service';
@@ -596,16 +596,22 @@ export class PluginMessageHandler {
const requestKey = `edit_request_${Date.now()}`;
this.imageReferencesStore.set(requestKey, imageReferences);
+ const editBody = JSON.stringify({
+ message: userMessage,
+ history: this.conversationHistory,
+ currentDesign: cleanedDesign,
+ modelId: selectedModel,
+ designSystemId: designSystemId
+ });
+
+ if (editBody.length > MAX_PAYLOAD_BYTES) {
+ throw new Error('Selected layer is too large to edit (exceeds 5MB). Please select a smaller layer.');
+ }
+
const response = await fetch(`${ApiConfig.BASE_URL}/api/designs/edit-with-ai`, {
method: 'POST',
headers: await this.getUserInfoUseCase.execute(),
- body: JSON.stringify({
- message: userMessage,
- history: this.conversationHistory,
- currentDesign: cleanedDesign,
- modelId: selectedModel,
- designSystemId: designSystemId
- })
+ body: editBody,
});
if (!response.ok) {
@@ -701,17 +707,23 @@ export class PluginMessageHandler {
console.log(`Plugin: sending ${cleanedReferences.length} references to backend`);
+ const basedOnExistingBody = JSON.stringify({
+ message: userMessage,
+ history: conversationHistory,
+ referenceDesigns: cleanedReferences,
+ modelId: selectedModel,
+ pinnedComponentNames: pinnedComponentNames ?? [],
+ ...(imageDataUrl ? { imageDataUrl } : {}),
+ });
+
+ if (basedOnExistingBody.length > MAX_PAYLOAD_BYTES) {
+ throw new Error('References are too large to send (exceeds 5MB). Please attach fewer layers or components.');
+ }
+
const response = await fetch(`${ApiConfig.BASE_URL}/api/designs/generate-based-on-existing`, {
method: 'POST',
headers: await this.getUserInfoUseCase.execute(),
- body: JSON.stringify({
- message: userMessage,
- history: conversationHistory,
- referenceDesigns: cleanedReferences,
- modelId: selectedModel,
- pinnedComponentNames: pinnedComponentNames ?? [],
- ...(imageDataUrl ? { imageDataUrl } : {}),
- })
+ body: basedOnExistingBody,
});
if (!response.ok) {
diff --git a/figma-plugin/src/presentation/ui/components/modals/SaveModal.tsx b/figma-plugin/src/presentation/ui/components/modals/SaveModal.tsx
index e52c66c..91b1562 100644
--- a/figma-plugin/src/presentation/ui/components/modals/SaveModal.tsx
+++ b/figma-plugin/src/presentation/ui/components/modals/SaveModal.tsx
@@ -17,6 +17,7 @@ export default function SaveModal(): React.JSX.Element | null {
const [projects, setProjects] = useState([]);
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
const componentName = useMemo(() => getComponentNameFromExportData(currentExportData), [currentExportData]);
const componentNames = useMemo(() => getComponentNamesFromExportData(currentExportData), [currentExportData]);
@@ -65,6 +66,7 @@ export default function SaveModal(): React.JSX.Element | null {
try {
setIsSaving(true);
+ setSaveError(null);
let previewImageUrl: string | null = null;
try {
@@ -103,7 +105,7 @@ export default function SaveModal(): React.JSX.Element | null {
setDescription('');
setSelectedProjectId('');
} catch (error) {
- notify(`❌ ${(error as Error).message}`, 'error');
+ setSaveError((error as Error).message);
reportErrorAsync(error, {
actionType: 'saveComponent',
});
@@ -116,6 +118,7 @@ export default function SaveModal(): React.JSX.Element | null {
dispatch({ type: 'CLOSE_SAVE_MODAL' });
setDescription('');
setSelectedProjectId('');
+ setSaveError(null);
};
return (
@@ -210,6 +213,12 @@ export default function SaveModal(): React.JSX.Element | null {
disabled={isSaving}
/>
+ {saveError && (
+