# BYOB (Bring Your Own Backend) System **Phase ID:** PHASE-A **Priority:** πŸ”΄ Critical **Estimated Duration:** 4-6 weeks **Status:** Planning **Last Updated:** 2025-12-28 ## Executive Summary BYOB transforms Noodl from a platform with prescribed backend services into a flexible system that works with any BaaS (Backend-as-a-Service) provider. Users can: 1. **Connect to any backend** - Directus, Supabase, Pocketbase, Parse, Firebase, or custom REST/GraphQL APIs 2. **Auto-discover schema** - Noodl introspects the backend and populates node property dropdowns automatically 3. **Switch backends easily** - Same visual graph can point to different backends (dev/staging/prod) 4. **Use multiple backends** - Single project can connect to multiple services simultaneously ## Problem Statement ### Current State Noodl's original cloud services were tightly coupled to a specific backend implementation. With OpenNoodl, users need flexibility to: - Use existing company infrastructure - Choose self-hosted solutions for data sovereignty - Select platforms based on specific feature needs - Avoid vendor lock-in ### Pain Points 1. **No backend flexibility** - Original cloud services are defunct 2. **Manual configuration** - Users must wire up HTTP nodes for every operation 3. **No schema awareness** - Property panel can't know what fields exist 4. **No switching** - Changing backends requires rewiring entire project 5. **Security scattered** - Auth tokens hard-coded or awkwardly passed around ## Solution Architecture ### High-Level Design ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ NOODL EDITOR β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ BACKEND CONFIGURATION HUB β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ Directus β”‚ β”‚ Supabase β”‚ β”‚ Pocketbase β”‚ [+ Add] β”‚ β”‚ β”‚ β”‚ β”‚ βœ“ Active β”‚ β”‚ Staging β”‚ β”‚ Local Dev β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ SCHEMA INTROSPECTION β”‚ β”‚ β”‚ β”‚ Backend β†’ Schema Endpoint β†’ Parse β†’ Store in Project β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ DATA NODES β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ Query Records β”‚ β”‚ Create Record β”‚ β”‚ Update Record β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Backend: [β–Ύ] β”‚ β”‚ Backend: [β–Ύ] β”‚ β”‚ Backend: [β–Ύ] β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Table: [β–Ύ] β”‚ β”‚ Table: [β–Ύ] β”‚ β”‚ Table: [β–Ύ] β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Fields: [...] β”‚ β”‚ Fields: [...] β”‚ β”‚ Fields: [...] β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BACKEND ADAPTER LAYER β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ UNIFIED BACKEND API β”‚ β”‚ β”‚ β”‚ query(table, filters, options) β†’ Promise β”‚ β”‚ β”‚ β”‚ create(table, data) β†’ Promise β”‚ β”‚ β”‚ β”‚ update(table, id, data) β†’ Promise β”‚ β”‚ β”‚ β”‚ delete(table, id) β†’ Promise β”‚ β”‚ β”‚ β”‚ introspect() β†’ Promise β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β–Ό β–Ό β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Directus β”‚ β”‚ Supabase β”‚ β”‚ Pocketbase β”‚ β”‚ β”‚ β”‚ Adapter β”‚ β”‚ Adapter β”‚ β”‚ Adapter β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Directus β”‚ β”‚ Supabase β”‚ β”‚ Pocketbase β”‚ β”‚ Server β”‚ β”‚ Project β”‚ β”‚ Server β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Detailed Specifications ### 1. Backend Configuration Data Model ```typescript /** * Configuration for a single backend connection */ interface BackendConfig { /** Unique identifier for this backend config */ id: string; /** User-friendly name (e.g., "Production Directus", "Local Dev") */ name: string; /** Backend platform type */ type: BackendType; /** Base URL of the backend */ url: string; /** Authentication configuration */ auth: AuthConfig; /** Cached schema from introspection */ schema: SchemaCache | null; /** Last successful schema sync */ lastSynced: Date | null; /** Connection status */ status: ConnectionStatus; /** Platform-specific configuration */ platformConfig?: PlatformSpecificConfig; } type BackendType = 'directus' | 'supabase' | 'pocketbase' | 'parse' | 'firebase' | 'custom-rest' | 'custom-graphql'; interface AuthConfig { /** Authentication method */ method: AuthMethod; /** Static API token (stored encrypted) */ staticToken?: string; /** OAuth/JWT configuration */ oauth?: OAuthConfig; /** API key header name (for custom backends) */ apiKeyHeader?: string; /** Whether to use runtime user auth (pass through user's token) */ useRuntimeAuth?: boolean; } type AuthMethod = 'none' | 'static-token' | 'api-key' | 'oauth' | 'runtime'; // Use currently logged-in user's token interface OAuthConfig { clientId: string; clientSecret?: string; // Encrypted authorizationUrl: string; tokenUrl: string; scopes: string[]; } type ConnectionStatus = 'connected' | 'disconnected' | 'error' | 'unknown'; /** * Platform-specific configuration options */ interface PlatformSpecificConfig { // Directus directus?: { /** Use static tokens or Directus auth flow */ authMode: 'static' | 'directus-auth'; }; // Supabase supabase?: { /** Anon key for client-side access */ anonKey: string; /** Enable realtime subscriptions */ realtimeEnabled: boolean; }; // Pocketbase pocketbase?: { /** Admin email for schema introspection */ adminEmail?: string; }; // Firebase firebase?: { projectId: string; apiKey: string; authDomain: string; }; // Custom REST customRest?: { /** Endpoint patterns */ endpoints: { list: string; // GET /api/{table} get: string; // GET /api/{table}/{id} create: string; // POST /api/{table} update: string; // PATCH /api/{table}/{id} delete: string; // DELETE /api/{table}/{id} schema?: string; // GET /api/schema (optional) }; /** Response data path (e.g., "data.items" for nested responses) */ dataPath?: string; /** Pagination style */ pagination: 'offset' | 'cursor' | 'page'; }; } ``` ### 2. Schema Introspection System ```typescript /** * Cached schema from backend introspection */ interface SchemaCache { /** Schema version/hash for cache invalidation */ version: string; /** When this schema was fetched */ fetchedAt: Date; /** Collections/tables in the backend */ collections: CollectionSchema[]; /** Global types/enums if the backend defines them */ types?: TypeDefinition[]; } /** * Schema for a single collection/table */ interface CollectionSchema { /** Internal collection name */ name: string; /** Display name (if different from name) */ displayName?: string; /** Collection description */ description?: string; /** Fields in this collection */ fields: FieldSchema[]; /** Primary key field name */ primaryKey: string; /** Timestamps configuration */ timestamps?: { createdAt?: string; updatedAt?: string; }; /** Whether this is a system collection (hidden by default) */ isSystem?: boolean; /** Relations to other collections */ relations?: RelationSchema[]; } /** * Schema for a single field */ interface FieldSchema { /** Field name */ name: string; /** Display name */ displayName?: string; /** Field type (normalized across platforms) */ type: FieldType; /** Original platform-specific type */ nativeType: string; /** Whether field is required */ required: boolean; /** Whether field is unique */ unique: boolean; /** Default value */ defaultValue?: any; /** Validation rules */ validation?: ValidationRule[]; /** For enum types, the allowed values */ enumValues?: string[]; /** For relation types, the target collection */ relationTarget?: string; /** Whether field is read-only (system-generated) */ readOnly?: boolean; /** Field description */ description?: string; } /** * Normalized field types across all platforms */ type FieldType = // Primitives | 'string' | 'text' // Long text / rich text | 'number' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'time' // Complex | 'json' | 'array' | 'enum' | 'uuid' // Files | 'file' | 'image' // Relations | 'relation-one' | 'relation-many' // Special | 'password' // Hashed, never returned | 'email' | 'url' | 'unknown'; /** * Relation between collections */ interface RelationSchema { /** Field that holds this relation */ field: string; /** Target collection */ targetCollection: string; /** Target field (usually primary key) */ targetField: string; /** Relation type */ type: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many'; /** Junction table for many-to-many */ junctionTable?: string; } /** * Validation rule */ interface ValidationRule { type: 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'custom'; value: any; message?: string; } ``` ### 3. Backend Adapter Interface ```typescript /** * Interface that all backend adapters must implement */ interface BackendAdapter { /** Backend type identifier */ readonly type: BackendType; /** Display name for UI */ readonly displayName: string; /** Icon for UI */ readonly icon: string; /** Initialize adapter with configuration */ initialize(config: BackendConfig): Promise; /** Test connection to backend */ testConnection(): Promise; /** Introspect schema from backend */ introspectSchema(): Promise; // CRUD Operations /** Query records from a collection */ query(params: QueryParams): Promise; /** Get single record by ID */ getById(collection: string, id: string): Promise; /** Create a new record */ create(collection: string, data: object): Promise; /** Update an existing record */ update(collection: string, id: string, data: object): Promise; /** Delete a record */ delete(collection: string, id: string): Promise; // Batch Operations /** Create multiple records */ createMany?(collection: string, data: object[]): Promise; /** Update multiple records */ updateMany?(collection: string, ids: string[], data: object): Promise; /** Delete multiple records */ deleteMany?(collection: string, ids: string[]): Promise; // Advanced Operations (optional) /** Execute raw query (platform-specific) */ rawQuery?(query: string, params?: object): Promise; /** Subscribe to realtime changes */ subscribe?(collection: string, callback: ChangeCallback): Unsubscribe; /** Upload file */ uploadFile?(file: File, options?: UploadOptions): Promise; /** Call server function/action */ callFunction?(name: string, params?: object): Promise; } /** * Query parameters */ interface QueryParams { /** Collection to query */ collection: string; /** Filter conditions */ filter?: FilterGroup; /** Fields to return (null = all) */ fields?: string[] | null; /** Sort order */ sort?: SortSpec[]; /** Pagination */ limit?: number; offset?: number; cursor?: string; /** Relations to expand/populate */ expand?: string[]; /** Search query (full-text search) */ search?: string; } /** * Filter condition or group */ interface FilterGroup { operator: 'and' | 'or'; conditions: (FilterCondition | FilterGroup)[]; } interface FilterCondition { field: string; operator: FilterOperator; value: any; } type FilterOperator = | 'eq' // equals | 'neq' // not equals | 'gt' // greater than | 'gte' // greater than or equal | 'lt' // less than | 'lte' // less than or equal | 'in' // in array | 'nin' // not in array | 'contains' // string contains | 'startsWith' | 'endsWith' | 'isNull' | 'isNotNull'; /** * Query result */ interface QueryResult { /** Returned records */ data: Record[]; /** Total count (if available) */ totalCount?: number; /** Pagination cursor for next page */ nextCursor?: string; /** Whether more records exist */ hasMore: boolean; } /** * Generic record type */ interface Record { /** Primary key (always present) */ id: string; /** All other fields */ [field: string]: any; } ``` ### 4. Platform-Specific Adapters #### 4.1 Directus Adapter ```typescript /** * Directus backend adapter * Directus REST API: https://docs.directus.io/reference/introduction.html */ class DirectusAdapter implements BackendAdapter { readonly type = 'directus'; readonly displayName = 'Directus'; readonly icon = 'directus-logo'; private sdk: DirectusSDK; private config: BackendConfig; async initialize(config: BackendConfig): Promise { this.config = config; this.sdk = createDirectus(config.url).with(rest()).with(staticToken(config.auth.staticToken!)); } async introspectSchema(): Promise { // Fetch collections const collections = await this.sdk.request(readCollections()); // Fetch fields for each collection const fields = await this.sdk.request(readFields()); // Fetch relations const relations = await this.sdk.request(readRelations()); return this.mapToSchemaCache(collections, fields, relations); } async query(params: QueryParams): Promise { const items = await this.sdk.request( readItems(params.collection, { filter: this.mapFilter(params.filter), fields: params.fields || ['*'], sort: params.sort?.map((s) => `${s.desc ? '-' : ''}${s.field}`), limit: params.limit, offset: params.offset, deep: params.expand ? this.buildDeep(params.expand) : undefined }) ); // Get total count if needed let totalCount: number | undefined; if (params.limit) { const countResult = await this.sdk.request( aggregate(params.collection, { aggregate: { count: '*' }, query: { filter: this.mapFilter(params.filter) } }) ); totalCount = countResult[0]?.count; } return { data: items, totalCount, hasMore: params.limit ? items.length === params.limit : false }; } private mapFilter(filter?: FilterGroup): object | undefined { if (!filter) return undefined; // Map our normalized filter to Directus filter format // Directus: { _and: [{ field: { _eq: value } }] } const conditions = filter.conditions.map((c) => { if ('operator' in c && 'conditions' in c) { // Nested group return this.mapFilter(c as FilterGroup); } const cond = c as FilterCondition; return { [cond.field]: { [`_${this.mapOperator(cond.operator)}`]: cond.value } }; }); return { [`_${filter.operator}`]: conditions }; } private mapOperator(op: FilterOperator): string { const mapping: Record = { eq: 'eq', neq: 'neq', gt: 'gt', gte: 'gte', lt: 'lt', lte: 'lte', in: 'in', nin: 'nin', contains: 'contains', startsWith: 'starts_with', endsWith: 'ends_with', isNull: 'null', isNotNull: 'nnull' }; return mapping[op]; } // ... other methods } ``` #### 4.2 Supabase Adapter ```typescript /** * Supabase backend adapter * Supabase JS: https://supabase.com/docs/reference/javascript/introduction */ class SupabaseAdapter implements BackendAdapter { readonly type = 'supabase'; readonly displayName = 'Supabase'; readonly icon = 'supabase-logo'; private client: SupabaseClient; private config: BackendConfig; async initialize(config: BackendConfig): Promise { this.config = config; const supabaseConfig = config.platformConfig?.supabase; this.client = createClient(config.url, supabaseConfig?.anonKey || config.auth.staticToken!, { auth: { persistSession: false }, realtime: { enabled: supabaseConfig?.realtimeEnabled ?? false } }); } async introspectSchema(): Promise { // Supabase provides schema via PostgREST // Use the /rest/v1/ endpoint with OpenAPI spec const response = await fetch(`${this.config.url}/rest/v1/`, { headers: { apikey: this.config.auth.staticToken!, Authorization: `Bearer ${this.config.auth.staticToken}` } }); // Parse OpenAPI spec to extract tables and columns const openApiSpec = await response.json(); return this.mapOpenApiToSchema(openApiSpec); } async query(params: QueryParams): Promise { let query = this.client.from(params.collection).select(params.fields?.join(',') || '*', { count: 'exact' }); // Apply filters if (params.filter) { query = this.applyFilters(query, params.filter); } // Apply sort if (params.sort) { for (const sort of params.sort) { query = query.order(sort.field, { ascending: !sort.desc }); } } // Apply pagination if (params.limit) { query = query.limit(params.limit); } if (params.offset) { query = query.range(params.offset, params.offset + (params.limit || 100) - 1); } const { data, count, error } = await query; if (error) throw new BackendError(error.message, error); return { data: data || [], totalCount: count ?? undefined, hasMore: params.limit ? (data?.length || 0) === params.limit : false }; } // Realtime subscription (Supabase-specific feature) subscribe(collection: string, callback: ChangeCallback): Unsubscribe { const channel = this.client .channel(`${collection}_changes`) .on('postgres_changes', { event: '*', schema: 'public', table: collection }, (payload) => { callback({ type: payload.eventType as 'INSERT' | 'UPDATE' | 'DELETE', record: payload.new as Record, oldRecord: payload.old as Record }); }) .subscribe(); return () => { this.client.removeChannel(channel); }; } // ... other methods } ``` #### 4.3 Pocketbase Adapter ```typescript /** * Pocketbase backend adapter * Pocketbase JS: https://pocketbase.io/docs/client-side-sdks/ */ class PocketbaseAdapter implements BackendAdapter { readonly type = 'pocketbase'; readonly displayName = 'Pocketbase'; readonly icon = 'pocketbase-logo'; private client: PocketBase; private config: BackendConfig; async initialize(config: BackendConfig): Promise { this.config = config; this.client = new PocketBase(config.url); // Authenticate if admin credentials provided const pbConfig = config.platformConfig?.pocketbase; if (pbConfig?.adminEmail && config.auth.staticToken) { await this.client.admins.authWithPassword(pbConfig.adminEmail, config.auth.staticToken); } } async introspectSchema(): Promise { // Pocketbase provides collection schema via admin API const collections = await this.client.collections.getFullList(); return { version: Date.now().toString(), fetchedAt: new Date(), collections: collections.map((c) => this.mapCollection(c)) }; } private mapCollection(pb: any): CollectionSchema { return { name: pb.name, displayName: pb.name, primaryKey: 'id', fields: pb.schema.map((f: any) => this.mapField(f)), timestamps: { createdAt: 'created', updatedAt: 'updated' }, isSystem: pb.system }; } private mapField(field: any): FieldSchema { const typeMap: Record = { text: 'string', editor: 'text', number: 'number', bool: 'boolean', email: 'email', url: 'url', date: 'datetime', select: 'enum', json: 'json', file: 'file', relation: field.options?.maxSelect === 1 ? 'relation-one' : 'relation-many' }; return { name: field.name, type: typeMap[field.type] || 'unknown', nativeType: field.type, required: field.required, unique: field.unique, enumValues: field.type === 'select' ? field.options?.values : undefined, relationTarget: field.options?.collectionId }; } async query(params: QueryParams): Promise { const result = await this.client .collection(params.collection) .getList(params.offset ? Math.floor(params.offset / (params.limit || 20)) + 1 : 1, params.limit || 20, { filter: params.filter ? this.buildFilter(params.filter) : undefined, sort: params.sort?.map((s) => `${s.desc ? '-' : ''}${s.field}`).join(','), expand: params.expand?.join(',') }); return { data: result.items, totalCount: result.totalItems, hasMore: result.page < result.totalPages }; } private buildFilter(filter: FilterGroup): string { // Pocketbase uses a custom filter syntax // e.g., "name = 'John' && age > 18" const parts = filter.conditions.map((c) => { if ('operator' in c && 'conditions' in c) { return `(${this.buildFilter(c as FilterGroup)})`; } const cond = c as FilterCondition; const op = this.mapOperator(cond.operator); const value = typeof cond.value === 'string' ? `'${cond.value}'` : cond.value; return `${cond.field} ${op} ${value}`; }); const joiner = filter.operator === 'and' ? ' && ' : ' || '; return parts.join(joiner); } // ... other methods } ``` ## UI Specifications ### Backend Configuration Hub This replaces the current Cloud Services UI with a flexible multi-backend configuration panel. ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Backend Configuration [Γ—] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ ACTIVE BACKEND β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [Directus Logo] Production Directus [Change β–Ύ] β”‚ β”‚ β”‚ β”‚ https://api.myapp.com β€’ βœ“ Connected β€’ Last sync: 2 min ago β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ ─────────────────────────────────────────────────────────────────────────────── β”‚ β”‚ β”‚ β”‚ ALL BACKENDS [+ Add Backend]β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [βœ“] [Directus] Production Directus β”‚ β”‚ β”‚ β”‚ https://api.myapp.com β”‚ β”‚ β”‚ β”‚ βœ“ Connected β€’ 12 collections β€’ Last sync: 2 min ago β”‚ β”‚ β”‚ β”‚ [Sync Schema] [Edit] [Delete] β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ [ ] [Directus] Staging β”‚ β”‚ β”‚ β”‚ https://staging.myapp.com β”‚ β”‚ β”‚ β”‚ βœ“ Connected β€’ 12 collections β€’ Last sync: 1 hour ago β”‚ β”‚ β”‚ β”‚ [Sync Schema] [Edit] [Delete] β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ [ ] [Supabase] Analytics DB β”‚ β”‚ β”‚ β”‚ https://xyz.supabase.co β”‚ β”‚ β”‚ β”‚ βœ“ Connected β€’ 5 collections β€’ Realtime enabled β”‚ β”‚ β”‚ β”‚ [Sync Schema] [Edit] [Delete] β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ [ ] [Pocketbase] Local Dev β”‚ β”‚ β”‚ β”‚ http://localhost:8090 β”‚ β”‚ β”‚ β”‚ ⚠ Disconnected β€’ Click to reconnect β”‚ β”‚ β”‚ β”‚ [Reconnect] [Edit] [Delete] β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ ─────────────────────────────────────────────────────────────────────────────── β”‚ β”‚ β”‚ β”‚ SCHEMA BROWSER [Expand All] [↻] β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β–Ό users 12 fields β”‚ β”‚ β”‚ β”‚ β”œβ”€ id (uuid) PRIMARY KEY β”‚ β”‚ β”‚ β”‚ β”œβ”€ email (email) REQUIRED UNIQUE β”‚ β”‚ β”‚ β”‚ β”œβ”€ name (string) β”‚ β”‚ β”‚ β”‚ β”œβ”€ avatar (image) β”‚ β”‚ β”‚ β”‚ β”œβ”€ role β†’ roles (relation-one) β”‚ β”‚ β”‚ β”‚ └─ ... β”‚ β”‚ β”‚ β”‚ β–Ά posts 8 fields β”‚ β”‚ β”‚ β”‚ β–Ά comments 6 fields β”‚ β”‚ β”‚ β”‚ β–Ά categories 4 fields β”‚ β”‚ β”‚ β”‚ β–Ά media 7 fields β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### Add Backend Dialog ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Add Backend [Γ—] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ SELECT BACKEND TYPE β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [Directus] β”‚ β”‚ [Supabase] β”‚ β”‚[Pocketbase] β”‚ β”‚ [Parse] β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Directus β”‚ β”‚ Supabase β”‚ β”‚ Pocketbase β”‚ β”‚ Parse β”‚ β”‚ β”‚ β”‚ ● Selected β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [Firebase] β”‚ β”‚ [REST API] β”‚ β”‚ [GraphQL] β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Firebase β”‚ β”‚ Custom REST β”‚ β”‚ GraphQL β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ ─────────────────────────────────────────────────────────────────────────────── β”‚ β”‚ β”‚ β”‚ DIRECTUS CONFIGURATION β”‚ β”‚ β”‚ β”‚ Name: β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Production Directus β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ URL: β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ https://api.myapp.com β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ Authentication: β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ ● Static Token β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’ [πŸ‘] [πŸ“‹] β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β—‹ Directus Auth Flow β”‚ β”‚ β”‚ β”‚ Users will authenticate with their Directus credentials β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β—‹ Pass-through (Runtime Auth) β”‚ β”‚ β”‚ β”‚ Use the logged-in user's token from your app's auth β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ [Cancel] [Test Connection] [Add Backend] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### Data Node with Backend Selection ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Query Records β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ BACKEND β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [Directus] Production Directus β–Ύ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β—‹ Use Active Backend (currently: Production Directus) β”‚ β”‚ ● Use Specific Backend β”‚ β”‚ β”‚ β”‚ COLLECTION β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ posts β–Ύ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”œβ”€ users β”‚ β”‚ β”œβ”€ posts ← β”‚ β”‚ β”œβ”€ comments β”‚ β”‚ β”œβ”€ categories β”‚ β”‚ └─ media β”‚ β”‚ β”‚ β”‚ ─────────────────────────────────────────────────────────────────────────────── β”‚ β”‚ β”‚ β”‚ β–Ό FILTER β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ status β–Ύ β”‚ β”‚ equals β–Ύ β”‚ β”‚ published β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ [+ Add Condition] [+ Add Group] β”‚ β”‚ β”‚ β”‚ β–Ό SORT β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ created_at β–Ύ β”‚ β”‚ Descendingβ–Ύ β”‚ [+ Add Sort] β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β–Ό PAGINATION β”‚ β”‚ Limit: [20 ] Offset: [0 ] β”‚ β”‚ β”‚ β”‚ β–Ά ADVANCED β”‚ β”‚ β”‚ β”‚ ─────────────────────────────────────────────────────────────────────────────── β”‚ β”‚ β”‚ β”‚ OUTPUTS β”‚ β”‚ β—‹ Records (Array) β”‚ β”‚ β—‹ First Record (Object) β”‚ β”‚ β—‹ Count (Number) β”‚ β”‚ β—‹ Loading (Boolean) β”‚ β”‚ β—‹ Error (Object) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Implementation Phases ### Phase A.1: HTTP Node Foundation (5-7 days) _Prerequisite: TASK-002 HTTP Node_ The HTTP Node provides the underlying request capability that all backend adapters will use. **Tasks:** - [ ] Implement robust HTTP node with all methods (GET, POST, PUT, PATCH, DELETE) - [ ] Add authentication header configuration - [ ] Add request/response transformation options - [ ] Add error handling with retry logic - [ ] Add request timeout configuration ### Phase A.2: Backend Configuration System (1 week) **Tasks:** - [ ] Design `BackendConfig` data model - [ ] Implement encrypted credential storage - [ ] Create Backend Configuration Hub UI - [ ] Create Add/Edit Backend dialogs - [ ] Implement connection testing - [ ] Store backend configs in project metadata **Files to Create:** ``` packages/noodl-editor/src/editor/src/ β”œβ”€β”€ models/ β”‚ └── BackendConfigModel.ts # Data model for backend configs β”œβ”€β”€ stores/ β”‚ └── BackendStore.ts # State management for backends β”œβ”€β”€ views/panels/ β”‚ └── BackendConfiguration/ β”‚ β”œβ”€β”€ BackendConfigPanel.tsx # Main panel β”‚ β”œβ”€β”€ BackendList.tsx # List of configured backends β”‚ β”œβ”€β”€ BackendCard.tsx # Single backend display β”‚ β”œβ”€β”€ AddBackendDialog.tsx # Add new backend β”‚ β”œβ”€β”€ EditBackendDialog.tsx # Edit existing β”‚ β”œβ”€β”€ SchemaViewer.tsx # Schema browser β”‚ └── ConnectionTest.tsx # Connection test UI └── utils/ └── credentials.ts # Encryption utilities ``` ### Phase A.3: Schema Introspection Engine (1 week) **Tasks:** - [ ] Define unified `SchemaCache` interface - [ ] Implement schema introspection for Directus - [ ] Implement schema introspection for Supabase - [ ] Implement schema introspection for Pocketbase - [ ] Create schema caching mechanism - [ ] Implement schema diff detection (for sync) - [ ] Add manual schema refresh **Files to Create:** ``` packages/noodl-runtime/src/backends/ β”œβ”€β”€ types.ts # Shared type definitions β”œβ”€β”€ BackendAdapter.ts # Base adapter interface β”œβ”€β”€ SchemaIntrospector.ts # Schema introspection logic β”œβ”€β”€ adapters/ β”‚ β”œβ”€β”€ DirectusAdapter.ts β”‚ β”œβ”€β”€ SupabaseAdapter.ts β”‚ β”œβ”€β”€ PocketbaseAdapter.ts β”‚ └── CustomRestAdapter.ts └── index.ts ``` ### Phase A.4: Directus Adapter (1 week) **Tasks:** - [ ] Implement full CRUD operations via Directus SDK - [ ] Map Directus filter syntax to unified format - [ ] Handle Directus-specific field types - [ ] Implement file upload to Directus - [ ] Handle Directus relations and expansions - [ ] Test with real Directus instance ### Phase A.5: Data Node Updates (1 week) **Tasks:** - [ ] Add backend selector to all data nodes - [ ] Populate collection dropdown from schema - [ ] Generate field inputs from collection schema - [ ] Update Query Records node - [ ] Update Create Record node - [ ] Update Update Record node - [ ] Update Delete Record node - [ ] Add loading and error outputs **Nodes to Modify:** - `Query Records` - Add backend selector, dynamic fields - `Create Record` - Schema-aware field inputs - `Update Record` - Schema-aware field inputs - `Delete Record` - Collection selector - `Insert Record` (alias) - Map to Create Record - `Set Record Properties` (alias) - Map to Update Record ## Testing Strategy ### Unit Tests ```typescript describe('DirectusAdapter', () => { describe('introspectSchema', () => { it('should fetch and map collections correctly', async () => { const adapter = new DirectusAdapter(); await adapter.initialize(mockConfig); const schema = await adapter.introspectSchema(); expect(schema.collections).toHaveLength(3); expect(schema.collections[0].name).toBe('users'); expect(schema.collections[0].fields).toContainEqual(expect.objectContaining({ name: 'email', type: 'email' })); }); }); describe('query', () => { it('should translate filters correctly', async () => { const spy = jest.spyOn(directusSdk, 'request'); await adapter.query({ collection: 'posts', filter: { operator: 'and', conditions: [ { field: 'status', operator: 'eq', value: 'published' }, { field: 'views', operator: 'gt', value: 100 } ] } }); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ filter: { _and: [{ status: { _eq: 'published' } }, { views: { _gt: 100 } }] } }) ); }); }); }); ``` ### Integration Tests ```typescript describe('Backend Integration', () => { // These require actual backend instances // Run with: npm test:integration describe('Directus', () => { const adapter = new DirectusAdapter(); beforeAll(async () => { await adapter.initialize({ url: process.env.DIRECTUS_URL!, auth: { method: 'static-token', staticToken: process.env.DIRECTUS_TOKEN } }); }); it('should create, read, update, delete a record', async () => { // Create const created = await adapter.create('test_items', { name: 'Test' }); expect(created.id).toBeDefined(); // Read const fetched = await adapter.getById('test_items', created.id); expect(fetched?.name).toBe('Test'); // Update const updated = await adapter.update('test_items', created.id, { name: 'Updated' }); expect(updated.name).toBe('Updated'); // Delete await adapter.delete('test_items', created.id); const deleted = await adapter.getById('test_items', created.id); expect(deleted).toBeNull(); }); }); }); ``` ## Security Considerations ### Credential Storage All sensitive credentials (API tokens, passwords) must be encrypted at rest: ```typescript class CredentialManager { private encryptionKey: Buffer; constructor() { // Derive key from machine-specific identifier this.encryptionKey = this.deriveKey(); } async encrypt(value: string): Promise { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv); const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]); const authTag = cipher.getAuthTag(); return Buffer.concat([iv, authTag, encrypted]).toString('base64'); } async decrypt(encrypted: string): Promise { const buffer = Buffer.from(encrypted, 'base64'); const iv = buffer.subarray(0, 16); const authTag = buffer.subarray(16, 32); const data = buffer.subarray(32); const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv); decipher.setAuthTag(authTag); return decipher.update(data) + decipher.final('utf8'); } } ``` ### Runtime Token Handling When using runtime authentication (passing through user's token): ```typescript interface RuntimeAuthContext { /** Get current user's auth token */ getToken(): Promise; /** Refresh token if expired */ refreshToken(): Promise; /** Clear auth state (logout) */ clearAuth(): void; } // In backend adapter async query(params: QueryParams, authContext?: RuntimeAuthContext): Promise { const headers: Record = {}; if (this.config.auth.useRuntimeAuth && authContext) { const token = await authContext.getToken(); if (token) { headers['Authorization'] = `Bearer ${token}`; } } else if (this.config.auth.staticToken) { headers['Authorization'] = `Bearer ${this.config.auth.staticToken}`; } // ... make request with headers } ``` ## Future Enhancements (Post-MVP) ### Realtime Subscriptions For backends that support it (Supabase, Pocketbase): ```typescript interface RealtimeSubscription { /** Subscribe to collection changes */ subscribe(collection: string, options?: SubscribeOptions): Observable; /** Subscribe to specific record changes */ subscribeToRecord(collection: string, id: string): Observable; } // New node: "Subscribe to Changes" // Outputs: onInsert, onUpdate, onDelete, record, oldRecord ``` ### GraphQL Support For backends with GraphQL APIs: ```typescript class GraphQLAdapter implements BackendAdapter { async introspectSchema(): Promise { // Use GraphQL introspection query const introspectionResult = await this.client.query({ query: getIntrospectionQuery() }); return this.mapGraphQLSchemaToCache(introspectionResult); } async query(params: QueryParams): Promise { // Generate GraphQL query from params const query = this.buildQuery(params); return this.client.query({ query, variables: params.filter }); } } ``` ### Schema Migrations Detect and suggest schema changes: ```typescript interface SchemaDiff { added: { collections: CollectionSchema[]; fields: FieldChange[] }; removed: { collections: string[]; fields: FieldChange[] }; modified: FieldChange[]; } interface FieldChange { collection: string; field: string; before?: FieldSchema; after?: FieldSchema; } // UI notification when schema changes detected // "Your backend schema has changed. 2 new fields detected in 'users' collection." ``` ## Success Metrics | Metric | Target | Measurement | | -------------------------- | ------- | --------------------------------------------- | | Backend connection time | < 2s | Time from "Add Backend" to "Connected" | | Schema introspection time | < 5s | Time to fetch and parse full schema | | Query execution overhead | < 50ms | Time added by adapter vs raw HTTP | | Node property panel render | < 100ms | Time to render schema-aware dropdowns | | User satisfaction | > 4/5 | Survey of users migrating from cloud services | ## Appendix: Backend Comparison Matrix | Feature | Directus | Supabase | Pocketbase | Parse | Firebase | | ------------------------ | ------------ | ----------- | ---------------- | ------------ | ----------- | | **Schema Introspection** | βœ… REST API | βœ… OpenAPI | βœ… Admin API | βœ… REST | ⚠️ Manual | | **CRUD Operations** | βœ… Full | βœ… Full | βœ… Full | βœ… Full | βœ… Full | | **Filtering** | βœ… Rich | βœ… Rich | βœ… Good | βœ… Rich | ⚠️ Limited | | **Relations** | βœ… Full | βœ… Full | βœ… Full | βœ… Full | ⚠️ Manual | | **File Upload** | βœ… Built-in | βœ… Storage | βœ… Built-in | βœ… Files | βœ… Storage | | **Realtime** | ⚠️ Extension | βœ… Built-in | βœ… SSE | βœ… LiveQuery | βœ… Built-in | | **Auth Integration** | βœ… Full | βœ… Full | βœ… Full | βœ… Full | βœ… Full | | **Self-Hostable** | βœ… Docker | ⚠️ Complex | βœ… Single binary | βœ… Docker | ❌ No | | **Open Source** | βœ… Yes | βœ… Yes | βœ… Yes | βœ… Yes | ❌ No | ## Task Index | Task | Name | Status | Priority | Description | | ---------------------------------------------- | ---------------------- | ----------- | ----------- | -------------------------------------------------------------- | | [TASK-001](./TASK-001-backend-services-panel/) | Backend Services Panel | βœ… Complete | πŸ”΄ Critical | Sidebar panel for configuring backends | | [TASK-002](./TASK-002-data-nodes/) | Data Nodes Integration | Not Started | πŸ”΄ Critical | Query, Create, Update, Delete nodes with Visual Filter Builder | | [TASK-003](./TASK-003-schema-viewer/) | Schema Viewer | Not Started | 🟑 Medium | Interactive tree view of backend schema | | [TASK-004](./TASK-004-edit-backend-dialog/) | Edit Backend Dialog | Not Started | 🟑 Medium | Edit existing backend configurations | | [TASK-005](./TASK-005-local-docker-wizard/) | Local Docker Wizard | Not Started | 🟒 Low | Spin up local backends via Docker | ## Recommended Implementation Order ``` TASK-001 βœ… β†’ TASK-002 (Critical) β†’ TASK-003 β†’ TASK-004 β†’ TASK-005 ↑ Hero Feature: Visual Filter Builder ``` ## Document Index - [README.md](./README.md) - This overview document - [SCHEMA-SPEC.md](./SCHEMA-SPEC.md) - Detailed schema specification (planned) - [ADAPTER-GUIDE.md](./ADAPTER-GUIDE.md) - Guide for implementing new adapters (planned)