# Banatie API v1 - Technical Changes and Refactoring ## Context Project is in active development with no existing clients. All changes can be made without backward compatibility concerns. **Priority: high-quality and correct API implementation.** --- ## 1. Parameter Naming Cleanup ✅ ### 1.1 POST /api/v1/generations **Current parameters:** - `assignAlias` → rename to `alias` - `assignFlowAlias` → rename to `flowAlias` **Rationale:** Shorter, clearer, no need for "assign" prefix when assignment is obvious from endpoint context. **Affected areas:** - Request type definitions - Route handlers - Service methods - API documentation --- ## 2. Enhanced Prompt Support - Logic Redesign ### 2.1 Database Schema Changes **Required schema modifications:** 1. **Rename field:** `enhancedPrompt` → `originalPrompt` 2. **Change field semantics:** - `prompt` - ALWAYS contains the prompt that was used for generation (enhanced or original) - `originalPrompt` - contains user's original input ONLY if autoEnhance was used (nullable) **Field population logic:** ``` Case 1: autoEnhance = false prompt = user input originalPrompt = NULL Case 2: autoEnhance = true prompt = enhanced prompt (used for generation) originalPrompt = user input (preserved) ``` ### 2.2 API Response Format **Response structure:** ```json { "prompt": "detailed enhanced prompt...", // Always the prompt used for generation "originalPrompt": "sunset" // Only present if enhancement was used } ``` **Affected endpoints:** - `POST /api/v1/generations` response - `GET /api/v1/generations/:id` response - `GET /api/v1/generations` list response --- ## 3. Regeneration Endpoint Refactoring ✅ ### 3.1 Endpoint Rename **Change:** - ❌ OLD: `POST /api/v1/generations/:id/retry` - ✅ NEW: `POST /api/v1/generations/:id/regenerate` ### 3.2 Remove Status Checks - Remove `if (original.status === 'success') throw error` check - Remove `GENERATION_ALREADY_SUCCEEDED` error constant - Allow regeneration for any status (pending, processing, success, failed) ### 3.3 Remove Retry Logic - Remove `retryCount >= MAX_RETRY_COUNT` check - Remove retryCount increment - Remove `MAX_RETRY_COUNT` constant ### 3.4 Remove Override Parameters - Remove `prompt` and `aspectRatio` parameters from request body - Always regenerate with exact same parameters as original ### 3.5 Image Update Behavior **Update existing image instead of creating new:** **Preserve:** - `imageId` (UUID remains the same) - `storageKey` (MinIO path) - `storageUrl` - `alias` (if assigned) - `createdAt` (original creation timestamp) **Update:** - Physical file in MinIO (overwrite) - `fileSize` (if changed) - `updatedAt` timestamp **Generation record:** - Update `status` → processing → success/failed - Update `processingTimeMs` - Keep `outputImageId` (same value) - Keep `flowId` (if present) ### 3.6 Additional Endpoint **Add for Flow:** - `POST /api/v1/flows/:id/regenerate` - Regenerates the most recent generation in flow - Returns `FLOW_HAS_NO_GENERATIONS` error if flow is empty - Uses parameters from the last generation in flow --- ## 4. Flow Auto-Creation (Lazy Flow Pattern) ### 4.1 Lazy Flow Creation Strategy **Concept:** 1. **First request without flowId** → return generated `flowId` in response, but **DO NOT create in DB** 2. **Any request with valid flowId** → create flow in DB if doesn't exist, add this request to flow 3. **If flowAlias specified in request** → create flow immediately (eager creation) ### 4.2 Implementation Details **Flow ID Generation:** - When generation/upload has no flowId, generate UUID for potential flow - Return this flowId in response - Save `flowId` in generation/image record, but DO NOT create flow record **Flow Creation in DB:** **Trigger:** ANY request with valid flowId value **Logic:** 1. Check if flow record exists in DB 2. Check if there are existing generations/images with this flowId 3. If flow doesn't exist: - Create flow record with provided flowId - Include all existing records with this flowId - Maintain chronological order based on createdAt timestamps 4. If flow exists: - Add new record to existing flow **Eager creation:** - If request includes `flowAlias` → create flow immediately - Set alias in `flow.aliases` object **Database Schema:** - `generations` table already has `flowId` field (foreign key to flows.id) - `images` table already has `flowId` field (foreign key to flows.id) - No schema changes needed **Orphan flowId handling:** - If `flowId` exists in generation/image record but not in `flows` table - this is normal - Such records are called "orphans" and simply not shown in `GET /api/v1/flows` list - No cleanup job needed - Do NOT delete such records automatically - System works correctly with orphan flowIds until flow record is created ### 4.3 Endpoint Changes **Remove:** - ❌ `POST /api/v1/flows` endpoint (no longer needed) **Modify responses:** - `POST /api/v1/generations` → always return `flowId` in response (see section 10.1) - `POST /api/v1/images/upload` → always return `flowId` in response (see section 10.1) --- ## 5. Upload Image Enhancements ### 5.1 Add Parameters **POST /api/v1/images/upload:** **Parameters:** - `alias` (optional, string) - project-scoped alias - `flowAlias` (optional, string) - flow-scoped alias for uploaded image - `flowId` (optional, string) - flow association **Behavior:** - If `flowAlias` and `flowId` specified: - Ensure flow exists (or create via lazy pattern) - Add alias to `flow.aliases` object - If `flowAlias` WITHOUT `flowId`: - Apply lazy flow creation with eager pattern - Create flow immediately, set flowAlias - If only `alias` specified: - Set project-scoped alias on image ### 5.2 Alias Conflict Resolution **Validation rules:** 1. **Technical aliases are forbidden:** - Cannot use: `@last`, `@first`, `@upload` or any reserved technical alias - Return validation error if attempted 2. **Alias override behavior:** - If alias already exists → new request has higher priority - Alias points to new image - Previous image loses its alias but is NOT deleted - Same logic applies to both project aliases and flow aliases 3. **Applies to both:** - Image upload with alias - Generation with alias/flowAlias **Example:** ``` State: Image A has alias "@hero" Request: Upload Image B with alias "@hero" Result: - Image B now has alias "@hero" - Image A loses alias (alias = NULL) - Image A is NOT deleted ``` --- ## 6. Image Alias Management Refactoring ### 6.1 Endpoint Consolidation **Remove alias handling from:** - ❌ `PUT /api/v1/images/:id` (body: { alias, focalPoint, meta }) - Remove `alias` parameter - Keep only `focalPoint` and `meta` **Single method for project-scoped alias management:** - ✅ `PUT /api/v1/images/:id/alias` (body: { alias }) - Set new alias - Change existing alias - Remove alias (pass `alias: null`) **Rationale:** Explicit intent, dedicated endpoint for alias operations, simpler validation. ### 6.2 Alias as Image Identifier **Support alias in path parameters:** **Syntax:** - UUID: `GET /api/v1/images/550e8400-e29b-41d4-a716-446655440000` - Alias: `GET /api/v1/images/@hero` - `@` symbol distinguishes alias from UUID (UUIDs never contain `@`) **UUID validation:** UUIDs can NEVER contain `@` symbol - this guarantees no conflicts **Flow-scoped resolution:** - `GET /api/v1/images/@hero?flowId=uuid-123` - Searches for alias `@hero` in context of flow `uuid-123` - Uses 3-tier precedence (technical → flow → project) **Endpoints with alias support:** - `GET /api/v1/images/:id_or_alias` - `PUT /api/v1/images/:id_or_alias` (for focalPoint, meta) - `PUT /api/v1/images/:id_or_alias/alias` - `DELETE /api/v1/images/:id_or_alias` **Implementation:** - Check first character of path parameter - If starts with `@` → resolve via AliasService - If doesn't start with `@` → treat as UUID - After resolution, work with imageId as usual ### 6.3 CDN-style Image URLs with Alias Support **Current URL format must be changed.** **New standardized URL patterns:** **For all generated and uploaded images:** ``` GET /cdn/:orgSlug/:projectSlug/img/:filenameOrAlias ``` **For live URLs:** ``` GET /cdn/:orgSlug/:projectSlug/live/:scope?prompt=...&aspectRatio=... ``` **All image URLs returned by API must follow this pattern.** **Resolution Logic:** 1. Check if `:filenameOrAlias` starts with `@` 2. If yes → resolve alias via AliasService 3. If no → search by filename/storageKey 4. Return image bytes with proper content-type headers **Response Headers:** - Content-Type: image/jpeg (or appropriate MIME type) - Cache-Control: public, max-age=31536000 - ETag: based on imageId or fileHash **URL Encoding for prompts:** - Spaces can be replaced with underscores `_` for convenience - Both `prompt=beautiful%20sunset` and `prompt=beautiful_sunset` are valid - System should handle both formats **Examples:** ``` GET /cdn/acme/website/img/@hero → resolve @hero alias GET /cdn/acme/website/img/logo.png → find by filename GET /cdn/acme/website/img/@product-1 → resolve @product-1 alias ``` **Error Handling:** - Alias not found → 404 - Filename not found → 404 - Multiple matches → alias takes priority over filename --- ## 7. Deletion Strategy Overhaul ### 7.1 Image Deletion (Hard Delete) **DELETE /api/v1/images/:id** **Operations:** 1. Delete physical file from MinIO storage 2. Delete record from `images` table (hard delete) 3. Cascade: set `outputImageId = NULL` in related generations 4. Cascade: **completely remove alias entries** from all `flow.aliases` where imageId is referenced - Remove entire key-value pairs, not just values 5. Cascade: remove imageId from `generation.referencedImages` JSON arrays **Example cascade for flow.aliases:** ``` Before: flow.aliases = { "@hero": "img-123", "@product": "img-456" } Delete img-123 After: flow.aliases = { "@product": "img-456" } ``` **Rationale:** User wants to delete - remove completely, free storage. Alias entries are also completely removed. ### 7.2 Generation Deletion (Conditional) **DELETE /api/v1/generations/:id** **Behavior depends on output image alias:** **Case 1: Output image WITHOUT project alias** 1. Delete output image completely (hard delete with MinIO cleanup) 2. Delete generation record (hard delete) **Case 2: Output image WITH project alias** 1. Keep output image (do not delete) 2. Delete only generation record (hard delete) 3. Set `generationId = NULL` in image record **Decision Logic:** - If `outputImage.alias !== null` → keep image, delete only generation - If `outputImage.alias === null` → delete both image and generation **Rationale:** - Image with project alias is used as standalone asset, preserve it - Image without alias was created only for this generation, delete together **No regeneration of deleted generations** - deleted generations cannot be regenerated ### 7.3 Flow Deletion (Cascade with Alias Protection) **DELETE /api/v1/flows/:id** **Operations:** 1. Delete flow record from DB 2. Cascade: delete all generations associated with this flowId 3. Cascade: delete all images associated with this flowId **EXCEPT** images with project alias **Detailed Cascade Logic:** **For Generations:** - Delete each generation (follows conditional delete from 7.2) - If output image has no alias → delete image - If output image has alias → keep image, set generationId = NULL, set flowId = NULL **For Images (uploaded):** - If image has no alias → delete (with MinIO cleanup) - If image has alias → keep, set flowId = NULL **Summary:** - Flow record → DELETE - All generations → DELETE - Images without alias → DELETE (with MinIO cleanup) - Images with project alias → KEEP (unlink: flowId = NULL) **Rationale:** Flow deletion removes all content except images with project aliases (used globally in project). ### 7.4 Transactional Delete Pattern **All delete operations must be transactional:** 1. Delete from MinIO storage first 2. Delete from database (with cascades) 3. If MinIO delete fails → rollback DB transaction 4. If DB delete fails → cleanup MinIO file (or rollback if possible) 5. Log all delete operations for audit trail **Principle:** System must be designed so orphaned files in MinIO NEVER occur. **Database Constraints:** - ON DELETE CASCADE for appropriate foreign keys - ON DELETE SET NULL where related records must be preserved - Proper referential integrity **No background cleanup jobs needed** - system is self-sufficient and always consistent. --- ## 8. Live URL System ### 8.1 Core Concept **Purpose:** Permanent URLs that can be immediately inserted into HTML and work forever. **Use Case:** ```html ``` **Key Points:** - URL is constructed immediately and used permanently - No preliminary generation through API needed - No signed URLs or tokens in query params - First request → generation, subsequent → cache ### 8.2 URL Format & Structure **URL Pattern:** ``` /cdn/:orgSlug/:projectSlug/live/:scope?prompt=...&aspectRatio=... ``` **URL Components:** ``` /cdn/acme/website/live/hero-section?prompt=beautiful_sunset&aspectRatio=16:9 │ │ │ │ │ │ │ │ │ └─ Generation params (query string) │ │ │ └─ Scope identifier │ │ └─ "live" prefix │ └─ Project slug └─ Organization slug ``` **Scope Parameter:** - Name: `scope` (confirmed) - Purpose: logical separation of live URLs within project - Format: alphanumeric + hyphens + underscores - Any user can specify any scope (no validation/signature required) ### 8.3 First Request Flow **Cache MISS (first request):** 1. Parse orgSlug, projectSlug, scope from URL 2. Compute cache key: hash(projectId + scope + prompt + params) 3. Check if image exists in cache 4. If NOT found: - Check scope settings (allowNewGenerations, limit) - Trigger image generation - Create database records (generation, image, cache entry) - Wait for generation to complete - Return image bytes **Response:** - Content-Type: image/jpeg - Cache-Control: public, max-age=31536000 - X-Cache-Status: MISS - X-Scope: hero-section - X-Image-Id: uuid **Cache HIT (subsequent requests):** 1. Same cache key lookup 2. Found existing image 3. Return cached image bytes immediately **Response:** - Content-Type: image/jpeg - Cache-Control: public, max-age=31536000 - X-Cache-Status: HIT - X-Image-Id: uuid **Generation in Progress:** - If image is not in cache but generation is already running: - System must have internal status to track this - Wait for generation to complete - Return image bytes immediately when ready - This ensures consistent behavior for concurrent requests ### 8.4 Scope Management **Database Table: `live_scopes`** Create dedicated table with fields: - `id` (UUID, primary key) - `project_id` (UUID, foreign key to projects) - `slug` (TEXT, unique within project) - used in URL - `allowNewGenerations` (BOOLEAN, default: true) - controls if new generations can be triggered - `newGenerationsLimit` (INTEGER, default: 30) - max number of generations in this scope - `created_at` (TIMESTAMP) - `updated_at` (TIMESTAMP) **Scope Behavior:** **allowNewGenerations:** - Controls whether new generations can be triggered in this scope - Already generated images are ALWAYS served publicly regardless of this setting - Default: true **newGenerationsLimit:** - Limit on number of generations in this scope - Only affects NEW generations, does not affect regeneration - Default: 30 **Scope Creation:** - Manual: via dedicated endpoint (see below) - Automatic: when new scope is used in live URL (if project allows) **Project-level Settings:** Add to projects table or settings: - `allowNewLiveScopes` (BOOLEAN, default: true) - allows creating new scopes via live URLs - If false: new scopes cannot be created via live URL - If false: scopes can still be created via API endpoint - `newLiveScopesGenerationLimit` (INTEGER, default: 30) - generation limit for auto-created scopes - This value is set as `newGenerationsLimit` for newly created scopes ### 8.5 Scope Management API **Create scope (manual):** ``` POST /api/v1/live/scopes Headers: X-API-Key: bnt_project_key Body: { "slug": "hero-section", "allowNewGenerations": true, "newGenerationsLimit": 50 } ``` **List scopes:** ``` GET /api/v1/live/scopes Headers: X-API-Key: bnt_project_key Response: { "scopes": [ { "id": "uuid", "slug": "hero-section", "allowNewGenerations": true, "newGenerationsLimit": 50, "currentGenerations": 23, "lastGeneratedAt": "2024-01-15T10:30:00Z" } ] } ``` **Get scope details:** ``` GET /api/v1/live/scopes/:slug Headers: X-API-Key: bnt_project_key Response: { "id": "uuid", "slug": "hero-section", "allowNewGenerations": true, "newGenerationsLimit": 50, "currentGenerations": 23, "images": [...] } ``` **Update scope:** ``` PUT /api/v1/live/scopes/:slug Headers: X-API-Key: bnt_project_key Body: { "allowNewGenerations": false, "newGenerationsLimit": 100 } ``` **Regenerate scope images:** ``` POST /api/v1/live/scopes/:slug/regenerate Headers: X-API-Key: bnt_project_key Body: { "imageId": "uuid" } // Optional: regenerate specific image Response: { "regenerated": 1, "images": [...] } ``` **Delete scope:** ``` DELETE /api/v1/live/scopes/:slug Headers: X-API-Key: bnt_project_key ``` **Deletion behavior:** Deletes all images in this scope (follows standard image deletion with alias protection). ### 8.6 Security & Rate Limiting **Rate Limiting by IP:** - Aggressive limits for live URLs (e.g., 10 new generations per hour per IP) - Separate from API key limits - Cache hits do NOT count toward limit - Only new generations count **Scope Quotas:** - Maximum N unique prompts per scope (newGenerationsLimit) - After limit reached → return existing images, do not generate new - Regeneration does not count toward limit ### 8.7 Caching Strategy **Cache Key:** ``` cacheKey = hash(projectId + scope + prompt + aspectRatio + otherParams) ``` **Cache Invalidation:** - Manual: via API endpoint regenerate - Automatic: never (images cached forever unless explicitly regenerated) **Scope Naming:** `scope` (confirmed) **URL Encoding:** - Prompt in query string: URL-encoded or underscores for spaces - Both formats supported: `prompt=beautiful%20sunset` and `prompt=beautiful_sunset` - Scope in path: alphanumeric + hyphens + underscores ### 8.8 Error Handling **Detailed errors for live URLs:** - Invalid scope format → 400 "Invalid scope format. Use alphanumeric characters, hyphens, and underscores" - New scope creation disabled → 403 "Creating new live scopes is disabled for this project" - Generation limit exceeded → 429 "Scope generation limit exceeded. Maximum N generations per scope" - Generation fails → 500 with retry logic - Rate limit by IP exceeded → 429 "Rate limit exceeded. Try again in X seconds" with Retry-After header --- ## 9. Generation Modification ### 9.1 Update Generation Endpoint **New endpoint:** ``` PUT /api/v1/generations/:id ``` **Modifiable Fields:** - `prompt` - change prompt - `aspectRatio` - change aspect ratio - `flowId` - change/remove/add flow association - `meta` - update metadata **Behavior:** **Case 1: Non-generative parameters (flowId, meta)** - Simply update fields in DB - Do NOT regenerate image **Case 2: Generative parameters (prompt, aspectRatio)** - Update fields in DB - Automatically trigger regeneration - Update existing image (same imageId, path, URL) ### 9.2 FlowId Management **FlowId handling:** - `flowId: null` → detach from flow - `flowId: "new-uuid"` → attach to different flow - If flow doesn't exist → create new flow eagerly (with this flowId) - If flow exists → add generation to existing flow - `flowId: undefined` → do not change current value **Use Case - "Detach from Flow":** - Set `flowId: null` to detach generation from flow - Output image is preserved (if has alias) - Useful before deleting flow to protect important generations ### 9.3 Validation Rules **Use existing validation logic from generation creation:** - Prompt validation (existing rules) - AspectRatio validation (existing rules) - FlowId validation: - If provided (not null): must be valid UUID format - Flow does NOT need to exist (will be created eagerly if missing) - Allow null explicitly (for detachment) ### 9.4 Response Format ```json { "success": true, "data": { "id": "gen-uuid", "prompt": "updated prompt", "aspectRatio": "16:9", "flowId": null, "status": "processing", // If regeneration triggered "regenerated": true, // Flag indicating regeneration started "outputImage": { ... } // Current image (updates when regeneration completes) } } ``` --- ## 10. Response Format Consistency ### 10.1 FlowId in Responses **Rule for flowId in generation and upload responses:** **If request has `flowId: undefined` (not provided):** - Generate new flowId - Return in response: `"flowId": "new-uuid"` **If request has `flowId: null` (explicitly null):** - Do NOT generate flowId - Flow is definitely not needed - Return in response: `"flowId": null` **If request has `flowId: "uuid"` (specific value):** - Use provided flowId - Return in response: `"flowId": "uuid"` **Examples:** ```json // Request without flowId POST /api/v1/generations Body: { "prompt": "sunset" } Response: { "flowId": "generated-uuid", ... } // Request with explicit null POST /api/v1/generations Body: { "prompt": "sunset", "flowId": null } Response: { "flowId": null, ... } // Request with specific flowId POST /api/v1/generations Body: { "prompt": "sunset", "flowId": "my-flow-uuid" } Response: { "flowId": "my-flow-uuid", ... } ``` --- ## 11. Error Messages Updates **Remove constants:** - `GENERATION_ALREADY_SUCCEEDED` (no longer needed) - `MAX_RETRY_COUNT_EXCEEDED` (no longer needed) **Add constants:** - `SCOPE_INVALID_FORMAT` - "Invalid scope format. Use alphanumeric characters, hyphens, and underscores" - `SCOPE_CREATION_DISABLED` - "Creating new live scopes is disabled for this project" - `SCOPE_GENERATION_LIMIT_EXCEEDED` - "Scope generation limit exceeded. Maximum {limit} generations per scope" - `STORAGE_DELETE_FAILED` - "Failed to delete file from storage" **Update constants:** - `GENERATION_FAILED` - include details about network/storage errors - `IMAGE_NOT_FOUND` - distinguish between deleted and never existed --- ## Summary of Changes ### Database Changes 1. Rename `enhancedPrompt` → `originalPrompt` in generations table 2. Create `live_scopes` table with fields: id, project_id, slug, allowNewGenerations, newGenerationsLimit 3. Add project settings: allowNewLiveScopes, newLiveScopesGenerationLimit 4. Add `scope` and `isLiveUrl` fields to images table (optional, can use meta) ### API Changes 1. Rename parameters: assignAlias → alias, assignFlowAlias → flowAlias 2. Rename endpoint: POST /generations/:id/retry → /generations/:id/regenerate 3. Remove endpoint: POST /api/v1/flows (no longer needed) 4. Add endpoint: POST /api/v1/flows/:id/regenerate 5. Add endpoint: PUT /api/v1/generations/:id (modification) 6. Add CDN endpoints: - GET /cdn/:org/:project/img/:filenameOrAlias (all images) - GET /cdn/:org/:project/live/:scope (live URLs) 7. Add scope management endpoints (CRUD for live_scopes) 8. Update all image URLs in API responses to use CDN format ### Behavior Changes 1. Lazy flow creation (create on second request or when flowAlias present) 2. Alias conflict resolution (new overwrites old) 3. Regenerate updates existing image (same ID, path, URL) 4. Hard delete for images (with MinIO cleanup) 5. Conditional delete for generations (based on alias) 6. Cascade delete for flows (with alias protection) 7. Live URL caching and scope management 8. FlowId in responses (generate if undefined, keep if null) ### Validation Changes 1. @ symbol distinguishes aliases from UUIDs 2. Technical aliases forbidden in user input 3. Flow creation on-the-fly for non-existent flowIds 4. Scope format validation for live URLs