Skip to content

Latest commit

 

History

History
805 lines (641 loc) · 23.9 KB

File metadata and controls

805 lines (641 loc) · 23.9 KB
title Module API Documentation
description This document defines the formal API contracts for all FlowWink modules.
category reference

Module API Documentation

Version: 1.1.0
Last Updated: 2025-01-20

This document defines the formal API contracts for all FlowWink modules. Each module exposes a well-defined interface that enables loose coupling, extensibility, and third-party module development.


Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                          MODULE REGISTRY                                │
│                       (Central Coordinator)                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│   │   Blog   │  │Newsletter│  │   CRM    │  │  Pages   │  │    KB    │ │
│   │  Module  │  │  Module  │  │  Module  │  │  Module  │  │  Module  │ │
│   │  v1.0.0  │  │  v1.0.0  │  │  v1.0.0  │  │  v1.0.0  │  │  v1.0.0  │ │
│   └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘ │
│        │             │             │             │             │        │
│        └─────────────┴─────────────┴─────────────┴─────────────┘        │
│                                   │                                     │
│                            Supabase Database                            │
└─────────────────────────────────────────────────────────────────────────┘

Core Principles

  1. Documented Contracts - Every module has explicit input/output schemas
  2. Validation First - All data is validated at module boundaries using Zod
  3. Loose Coupling - Modules communicate through the Registry, not directly
  4. Traceability - All cross-module data includes source metadata
  5. MCP-Ready by Default - Modules with db: skills are automatically operable via MCP, FlowPilot chat, and automations through the generic CRUD engine

MCP Exposure

Any module that registers skills with handler: "db:tablename" and a standard action enum (list, get, create, update, delete) is automatically accessible through:

  • MCP — External AI clients (Cursor, Claude Desktop, OpenClaw) via mcp-server
  • FlowPilot Chat — Admin and visitor conversations via chat-completion
  • Automations — Cron and event-triggered via agent-execute

To expose a module's skills via MCP, set mcp_exposed = true on the skill in agent_skills. The generic CRUD engine in agent-execute handles execution — no per-module edge function required. The table must be added to the GENERIC_CRUD_TABLES whitelist in agent-execute/index.ts.


Module Capabilities

Each module declares its capabilities:

Capability Description
content:receive Can receive content from other modules
content:produce Produces content that can be consumed by others
webhook:trigger Triggers outbound webhooks on events
webhook:receive Can receive inbound webhooks
data:read Reads data from the database
data:write Writes data to the database

Registered Modules

Module ID Version Capabilities
Blog blog 1.0.0 content:receive, data:write, webhook:trigger
Newsletter newsletter 1.0.0 content:receive, data:write
CRM crm 1.0.0 content:receive, data:write, webhook:trigger
Pages pages 1.0.0 content:receive, data:write, webhook:trigger
Knowledge Base kb 1.0.0 content:receive, data:write
Products products 1.0.0 content:receive, data:write, webhook:trigger
Booking booking 1.0.0 content:receive, data:write, webhook:trigger
Global Blocks global-blocks 1.0.0 content:receive, data:write
Media Library media 1.0.0 data:read, data:write
Deals deals 1.0.0 content:receive, data:write, webhook:trigger
Companies companies 1.0.0 content:receive, data:write
Forms forms 1.0.0 content:receive, data:write, webhook:trigger
Orders orders 1.0.0 content:receive, data:write, webhook:trigger
Webinars webinars 1.0.0 content:receive, data:write

Module Dependencies

Some modules have dependencies on other modules. When a parent module is disabled, dependent modules are automatically disabled as well. When enabling a dependent module, its parent is automatically enabled.

Dependent Module Requires
Orders Products
Deals Leads
Live Support Chat

Block-to-Module Mapping

Certain content blocks require their associated module to be enabled. When the required module is disabled, the block remains visible in the Block Selector but shows a warning indicator. Users can still add the block, but it may not function correctly.

Block Type Required Module
article-grid Blog
chat AI Chat
newsletter Newsletter
booking Bookings
products, cart Products
kb-featured, kb-hub, kb-search, kb-accordion Knowledge Base
webinar Webinars

This "hybrid guard" approach provides visual feedback without blocking the user, keeping the system flexible while preventing silent errors.


Module Definitions

Blog Module

ID: blog
Capabilities: content:receive, data:write, webhook:trigger

Input Schema

interface BlogModuleInput {
  // Required
  title: string;                    // Post title (max 200 chars)
  content: TiptapDocument | string; // Rich text content
  
  // Optional
  excerpt?: string;                 // Summary (max 500 chars)
  featured_image?: string;          // Image URL
  featured_image_alt?: string;      // Image alt text
  
  // Metadata
  meta?: {
    keywords?: string[];            // SEO keywords
    description?: string;           // Meta description
    source_module?: string;         // Originating module ID
    source_id?: string;             // Original content ID
  };
  
  // Publishing options
  options?: {
    status: 'draft' | 'published';  // Default: 'draft'
    schedule_at?: string;           // ISO 8601 datetime
    author_id?: string;             // Author profile ID
    category_ids?: string[];        // Category UUIDs
    tag_ids?: string[];             // Tag UUIDs
  };
}

Output Schema

interface BlogModuleOutput {
  success: boolean;
  id: string;           // Created post UUID
  slug: string;         // URL slug
  url: string;          // Full URL path
  status: string;       // Final status
  published_at?: string; // If published
  error?: string;       // If success is false
}

Example Usage

import { moduleRegistry } from '@/lib/module-registry';

const result = await moduleRegistry.publish('blog', {
  title: 'How to Build Modular Systems',
  content: tiptapDocument,
  excerpt: 'A guide to building maintainable software...',
  meta: {
    source_module: 'content-campaign',
    source_id: 'proposal-123'
  },
  options: {
    status: 'published'
  }
});

console.log(result);
// { success: true, id: 'uuid', slug: 'how-to-build-modular-systems', url: '/blog/how-to-build-modular-systems' }

Newsletter Module

ID: newsletter
Capabilities: content:receive, data:write

Input Schema

interface NewsletterModuleInput {
  // Required
  subject: string;                  // Email subject (max 150 chars)
  
  // Content (one of these)
  content_html?: string;            // Pre-rendered HTML
  content_json?: NewsletterBlock[]; // Structured blocks
  content_tiptap?: TiptapDocument;  // Rich text document
  
  // Optional
  preview_text?: string;            // Email preview text
  
  // Metadata
  meta?: {
    source_module?: string;
    source_id?: string;
  };
  
  // Options
  options?: {
    status: 'draft' | 'scheduled';  // Default: 'draft'
    send_at?: string;               // ISO 8601 datetime (for scheduled)
  };
}

Output Schema

interface NewsletterModuleOutput {
  success: boolean;
  id: string;           // Newsletter UUID
  status: string;       // Final status
  subscriber_count?: number; // If sending
  error?: string;
}

Webhook Module

ID: webhook
Capabilities: webhook:trigger, webhook:receive

Input Schema (Outbound)

interface WebhookModuleInput {
  // Required
  event: WebhookEventType;          // Event type to trigger
  payload: Record<string, unknown>; // Event-specific data
  
  // Optional
  channel?: string;                 // Specific webhook name filter
  
  // Metadata
  meta?: {
    source_module: string;
    trace_id?: string;              // For request tracing
  };
}

type WebhookEventType = 
  | 'page.published'
  | 'page.deleted'
  | 'blog_post.published'
  | 'blog_post.updated'
  | 'blog_post.deleted'
  | 'form.submitted'
  | 'newsletter.subscribed'
  | 'newsletter.unsubscribed'
  | 'order.created'
  | 'order.paid'
  | 'order.shipped'
  | 'order.cancelled'
  | 'booking.created'
  | 'booking.confirmed'
  | 'booking.cancelled'
  | 'lead.created'
  | 'lead.qualified'
  | 'content.published';  // Generic content event

Output Schema

interface WebhookModuleOutput {
  success: boolean;
  triggered_count: number;  // Number of webhooks triggered
  results: Array<{
    webhook_id: string;
    webhook_name: string;
    success: boolean;
    status_code?: number;
    error?: string;
  }>;
}

CRM Module

ID: crm
Capabilities: content:receive, data:write, webhook:trigger

Lead Input Schema

interface CRMLeadInput {
  // Required
  email: string;
  
  // Optional
  name?: string;
  phone?: string;
  source: string;         // Where lead came from
  source_id?: string;     // Reference to source record
  
  // Scoring
  initial_score?: number;
  
  // Metadata
  meta?: {
    source_module?: string;
    form_data?: Record<string, unknown>;
  };
}

Output Schema

interface CRMLeadOutput {
  success: boolean;
  lead_id: string;
  is_new: boolean;        // True if created, false if existing
  score: number;
  status: LeadStatus;
  error?: string;
}

Pages Module

ID: pages
Capabilities: content:receive, data:write, webhook:trigger

Input Schema

interface PageModuleInput {
  // Required
  title: string;                              // Page title (max 200 chars)
  content: ContentBlock[] | TiptapDocument | string; // Page content
  
  // Optional
  slug?: string;                              // Auto-generated if not provided
  
  // Metadata
  meta?: {
    source_module?: string;
    source_id?: string;
    seo_title?: string;                       // SEO title (max 60 chars)
    seo_description?: string;                 // Meta description (max 160 chars)
  };
  
  // Options
  options?: {
    status: 'draft' | 'published';            // Default: 'draft'
    show_in_menu: boolean;                    // Default: false
    menu_order?: number;
    schedule_at?: string;                     // ISO 8601 datetime
  };
}

Output Schema

interface PageModuleOutput {
  success: boolean;
  id: string;           // Created page UUID
  slug: string;         // URL slug
  url: string;          // Full URL path (e.g., "/about-us")
  status: string;       // Final status
  error?: string;
}

Example Usage

const result = await moduleRegistry.publish('pages', {
  title: 'About Our Company',
  content: [
    { id: 'uuid', type: 'hero', data: { title: 'About Us', subtitle: 'Our story' } },
    { id: 'uuid', type: 'text', data: { content: tiptapDoc } }
  ],
  meta: {
    seo_title: 'About Us - Company Name',
    seo_description: 'Learn about our company history and values.'
  },
  options: {
    status: 'published',
    show_in_menu: true,
    menu_order: 2
  }
});

Knowledge Base Module

ID: kb
Capabilities: content:receive, data:write

Input Schema

interface KBArticleModuleInput {
  // Required
  title: string;                    // Article title (max 200 chars)
  question: string;                 // FAQ question (max 500 chars)
  category_id: string;              // KB category UUID
  answer: TiptapDocument | string;  // Answer content
  
  // Optional
  slug?: string;                    // Auto-generated if not provided
  
  // Metadata
  meta?: {
    source_module?: string;
    source_id?: string;
    seo_title?: string;
    seo_description?: string;
  };
  
  // Options
  options?: {
    is_published: boolean;          // Default: true
    is_featured: boolean;           // Default: false
  };
}

Output Schema

interface KBArticleModuleOutput {
  success: boolean;
  id: string;           // Created article UUID
  slug: string;         // URL slug
  url: string;          // Full URL path (e.g., "/kb/how-to-reset-password")
  error?: string;
}

Example Usage

const result = await moduleRegistry.publish('kb', {
  title: 'Password Reset Guide',
  question: 'How do I reset my password?',
  category_id: 'category-uuid',
  answer: {
    type: 'doc',
    content: [
      { type: 'paragraph', content: [{ type: 'text', text: 'Follow these steps...' }] }
    ]
  },
  options: {
    is_published: true,
  }
});

Content Campaign → Module Flow

The Content Campaign module orchestrates publishing to multiple channels:

┌────────────────────┐
│  Content Campaign  │
│                    │
│  Proposal with:    │
│  - Pillar content  │
│  - Channel variants│
└─────────┬──────────┘
          │
          │ User clicks "Publish to Blog"
          ▼
┌────────────────────┐
│  Module Registry   │
│                    │
│  1. Get 'blog'     │
│  2. Validate input │
│  3. Execute        │
└─────────┬──────────┘
          │
          │ BlogModuleInput {
          │   title: variant.title,
          │   content: variant.content,
          │   meta: { source_module: 'content-campaign', source_id: proposal.id }
          │ }
          ▼
┌────────────────────┐
│   Blog Module      │
│                    │
│  1. Transform      │
│  2. Insert to DB   │
│  3. Trigger hooks  │
└─────────┬──────────┘
          │
          │ BlogModuleOutput {
          │   success: true,
          │   id: 'new-post-id',
          │   slug: 'generated-slug',
          │   url: '/blog/generated-slug'
          │ }
          ▼
┌────────────────────┐
│  Content Campaign  │
│                    │
│  Update proposal:  │
│  - published_channels │
│  - status          │
└────────────────────┘

Data Layer Contracts

Beyond the Module Registry, FlowWink enforces data ownership boundaries at the database level. Shared tables (like leads and lead_activities) have a single owner module that exposes utility functions. Other modules must use these functions — never write directly.

Table Ownership

Table Owner Contract File
leads CRM src/lib/lead-utils.ts
lead_activities CRM src/lib/lead-utils.ts
companies CRM src/lib/lead-utils.ts
blog_posts Blog src/lib/module-registry.ts (blog module)
pages Pages src/lib/module-registry.ts (pages module)

Lead Utils Contract (src/lib/lead-utils.ts)

All modules that interact with leads must use these functions.

Public surfaces write through SECURITY DEFINER RPCs, never the table. leads carries no WITH CHECK (true) policy since 20260821070000 — anonymous visitors reach the CRM only through ingest_form_lead / ingest_webinar_lead, and signed-in writes follow the role/module matrix (can_access_module('leads')). A new public lead source gets its own sister RPC with fail-closed validation; it does not get a table policy.

Function Purpose Used By
createLeadFromForm() Create/update lead from form submission FormBlock (via ingest_form_lead RPC)
createLeadFromBooking() Create/update lead from booking Staff only — no caller; booking leads are created server-side in comms-send/booking_confirmation.ts
createLeadFromWebinar() Create/update lead from webinar registration WebinarBlock (via ingest_webinar_lead RPC)
addLeadActivity() Log activity on a lead All modules via createLeadFrom*, Deals
updateLeadStatus() Change lead status Deals module
trackNewsletterActivity() Track email opens/clicks Newsletter
qualifyLead() Trigger AI qualification Called internally by createLeadFrom*

Each createLeadFrom* function handles:

  1. Find or create lead by email
  2. Auto-match company by email domain
  3. Trigger company enrichment for new companies
  4. Log activity with correct points
  5. Trigger AI qualification

Activity Points

Defined in ACTIVITY_POINTS within lead-utils.ts:

Activity Points
webinar_register 15
form_submit 10
booking 10
newsletter_subscribe 8
link_click 5
call 5
email_open 3
page_visit 2

Rules

  1. Never write directly to leads or lead_activities from hooks or components outside CRM
  2. Never import supabase and query leads table from a non-CRM module — use the contract functions
  3. Reading shared tables for analytics/display is OK (e.g., useAnalytics.ts)
  4. When adding a new lead source, create a createLeadFrom*() function in lead-utils.ts
  5. When a module needs to change lead status, use updateLeadStatus() — not a direct update

Anti-Patterns

// ❌ BAD — Direct write from webinar module
await supabase.from('leads').insert({ email, source: 'webinar' });
await supabase.from('lead_activities').insert({ lead_id, type: 'webinar_register' });

// ✅ GOOD — Via contract
import { createLeadFromWebinar } from '@/lib/lead-utils';
const { leadId } = await createLeadFromWebinar({ email, name, webinarId, webinarTitle });

// ❌ BAD — Direct status update from deals module
await supabase.from('leads').update({ status: 'customer' }).eq('id', leadId);

// ✅ GOOD — Via contract
import { updateLeadStatus } from '@/lib/lead-utils';
await updateLeadStatus(leadId, 'customer', { convertedAt: true });

Allowed Direct Access

These files may access leads/lead_activities directly because they are CRM-owned:

  • src/hooks/useLeads.ts — CRM CRUD hooks
  • src/hooks/useActivities.ts — CRM activity hooks
  • src/components/admin/CreateLeadDialog.tsx — Admin manual creation (uses addLeadActivity)
  • src/hooks/useCsvImportExport.ts — Bulk import
  • src/components/admin/ResetSiteDialog.tsx — System reset (delete all)
  • src/lib/module-registry.ts (CRM module only) — CRM module's publish()

Creating a New Module

Step 1: Define Types

Add your module's input/output types to src/types/module-contracts.ts:

// 1. Define input schema
export const myModuleInputSchema = z.object({
  required_field: z.string(),
  optional_field: z.string().optional(),
  meta: moduleMetaSchema.optional(),
});

export type MyModuleInput = z.infer<typeof myModuleInputSchema>;

// 2. Define output schema
export const myModuleOutputSchema = z.object({
  success: z.boolean(),
  id: z.string(),
  error: z.string().optional(),
});

export type MyModuleOutput = z.infer<typeof myModuleOutputSchema>;

Step 2: Implement Module

Create your module implementation:

import { ModuleDefinition } from '@/lib/module-registry';
import { MyModuleInput, MyModuleOutput, myModuleInputSchema, myModuleOutputSchema } from '@/types/module-contracts';

export const myModule: ModuleDefinition<MyModuleInput, MyModuleOutput> = {
  id: 'my-module',
  name: 'My Module',
  version: '1.0.0',
  capabilities: ['content:receive', 'data:write'],
  inputSchema: myModuleInputSchema,
  outputSchema: myModuleOutputSchema,
  
  async publish(input: MyModuleInput): Promise<MyModuleOutput> {
    // Your implementation here
    return {
      success: true,
      id: 'created-id',
    };
  }
};

Step 3: Register Module

Add to the registry in src/lib/module-registry.ts:

import { myModule } from './modules/my-module';

moduleRegistry.register(myModule);

Webhook Integration for External Modules

External systems can act as modules via webhooks:

Inbound Webhook (External → FlowWink)

The inbound endpoint is signal-ingest (there is no dedicated module-webhook function). It accepts signals from any external operator (webhooks, browser extension, integrations), stores them in agent_memory/agent_activity, and fires automation signals:

POST /functions/v1/signal-ingest
Content-Type: application/json
Authorization: Bearer <site_settings.signal_ingest_token>

{
  "note": "signal: lead.created from external-crm",
  "payload": {
    "email": "user@example.com",
    "name": "John Doe"
  }
}

Outbound Webhook (FlowWink → External)

Configure a webhook with event content.published:

{
  "event": "content.published",
  "timestamp": "2025-01-19T12:00:00Z",
  "data": {
    "module": "blog",
    "id": "post-uuid",
    "title": "New Post Title",
    "url": "/blog/new-post"
  },
  "meta": {
    "source_module": "content-campaign",
    "source_id": "proposal-uuid"
  }
}

Error Handling

All modules return consistent error structures:

interface ModuleError {
  success: false;
  error: string;           // Human-readable message
  error_code?: string;     // Machine-readable code
  validation_errors?: Array<{
    field: string;
    message: string;
  }>;
}

Error Codes

Code Description
VALIDATION_ERROR Input failed schema validation
NOT_FOUND Referenced resource doesn't exist
PERMISSION_DENIED User lacks required permissions
DUPLICATE Resource already exists
EXTERNAL_ERROR Third-party service failed

Migration Guide

From Direct Database Access

Before:

const { data } = await supabase
  .from('blog_posts')
  .insert({ title, content: contentJson });

After:

const result = await moduleRegistry.publish('blog', {
  title,
  content: contentJson,
  meta: { source_module: 'my-feature' }
});

Benefits

  1. Validation - Input is validated before database write
  2. Consistency - All blog posts created same way
  3. Hooks - Webhooks triggered automatically
  4. Traceability - Origin tracked in metadata