Reference Module Building Guide

Complete step-by-step guide to building a reference module from 0 to 100

Reference Module Building Guide

This guide walks through building a Contact Form Module from scratch using the Module Workbench, demonstrating the complete 0→100 platform generation workflow.


Overview

Goal: Create a fully functional Contact Form module that:

Route: /modularity/workbench

Time Estimate: 30-45 minutes


Step 1: Models (Define Data Structure)

Route: /modularity/workbench/models

1.1 Create Contact Entity

Click "Add Entity" and configure:

{
  name: 'Contact',
  description: 'Contact form submission',
  fields: [
    {
      name: 'email',
      type: 'string',
      required: true,
      label: 'Email Address',
      validation: {
        pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
        message: 'Invalid email format'
      }
    },
    {
      name: 'name',
      type: 'string',
      required: true,
      label: 'Full Name',
      validation: {
        minLength: 2,
        maxLength: 100
      }
    },
    {
      name: 'message',
      type: 'string',
      required: true,
      label: 'Message',
      validation: {
        minLength: 10,
        maxLength: 1000
      }
    },
    {
      name: 'phone',
      type: 'string',
      required: false,
      label: 'Phone Number'
    },
    {
      name: 'createdAt',
      type: 'date',
      required: true,
      defaultValue: 'now()',
      label: 'Submission Date'
    },
    {
      name: 'status',
      type: 'string',
      required: true,
      defaultValue: 'new',
      label: 'Status',
      enum: ['new', 'read', 'replied', 'archived']
    }
  ]
}

1.2 Preview Generated Model

The workbench will show the generated TypeScript interface:

/**
 * Contact Model
 * Contact form submission
 */
export interface Contact {
  /** Email Address */
  email: string;
  /** Full Name */
  name: string;
  /** Message */
  message: string;
  /** Phone Number */
  phone?: string;
  /** Submission Date */
  createdAt: Date;
  /** Status */
  status: 'new' | 'read' | 'replied' | 'archived';
}

✅ Checkpoint: Entity defined and validated


Step 2: State (Configure Storage)

Route: /modularity/workbench/state

2.1 Configure Collection

{
  collectionName: 'contacts',
  storageType: 'flash-persistence',
  backendSync: true,
  cacheStrategy: 'write-through',
  ttl: 90 * 24 * 60 * 60 * 1000, // 90 days
  indexes: [
    { field: 'email', unique: false },
    { field: 'createdAt', unique: false },
    { field: 'status', unique: false }
  ]
}

2.2 Define State Interface

export interface ContactModuleState {
  contacts: Contact[];
  totalCount: number;
  filters: {
    status?: string;
    dateRange?: { start: Date; end: Date };
  };
  isLoading: boolean;
}

2.3 Configure Flash Persistence

const contactsState = autoFlashState<Contact[]>('contact-module:contacts', [], {
  database: 'contact-module',
  collection: 'contacts',
  priority: 'high',
  ttl: 90 * 24 * 60 * 60 * 1000,
  debounceMs: 300
});

✅ Checkpoint: Storage configured with flash persistence


Step 3: Actions (Define Operations)

Route: /modularity/workbench/actions

3.1 Create "Submit Contact" Action

Click "Add Action" and configure:

{
  name: 'SubmitContact',
  displayName: 'Submit Contact Form',
  description: 'Submit a new contact form entry',
  category: 'forms',
  permissions: ['contacts:create'],
  
  inputs: [
    {
      name: 'email',
      type: 'string',
      required: true,
      label: 'Email Address',
      validation: { pattern: 'email' }
    },
    {
      name: 'name',
      type: 'string',
      required: true,
      label: 'Full Name'
    },
    {
      name: 'message',
      type: 'string',
      required: true,
      label: 'Message'
    },
    {
      name: 'phone',
      type: 'string',
      required: false,
      label: 'Phone Number'
    }
  ],
  
  output: {
    type: 'object',
    properties: {
      contactId: 'string',
      success: 'boolean',
      message: 'string'
    }
  }
}

3.2 Build Action Flow

Click "Build Flow" to open the Singularity flow builder:

Flow Steps:

  1. Trigger (Manual)

    • Type: trigger_manual
    • Outputs: email, name, message, phone
  2. Validate Input (Condition)

    • Type: logic_condition
    • Condition: email.includes('@') && name.length >= 2 && message.length >= 10
    • Outputs: valid (boolean)
  3. Create Contact (Data Operation)

    • Type: data_create
    • Collection: contacts
    • Data: { email, name, message, phone, createdAt: Date.now(), status: 'new' }
    • Outputs: contactId
  4. Emit Event (Event)

    • Type: event_emit
    • Event: contact:submitted
    • Payload: { contactId, email, name }
  5. Return Success (Return)

    • Type: output_return
    • Value: { contactId, success: true, message: 'Contact submitted successfully' }

Connections:

✅ Checkpoint: Action flow built and tested

3.3 Create "List Contacts" Action

{
  name: 'ListContacts',
  displayName: 'List Contacts',
  description: 'Retrieve list of contact submissions',
  category: 'data',
  permissions: ['contacts:read'],
  
  inputs: [
    {
      name: 'status',
      type: 'string',
      required: false,
      label: 'Filter by Status',
      enum: ['new', 'read', 'replied', 'archived']
    },
    {
      name: 'limit',
      type: 'number',
      required: false,
      label: 'Limit',
      defaultValue: 50
    }
  ],
  
  output: {
    type: 'array',
    items: 'Contact'
  }
}

✅ Checkpoint: 2 actions defined


Step 4: UI (Create Embeddables)

Route: /modularity/workbench/ui

4.1 Create Contact Form Widget

Click "Add Embeddable" and configure:

{
  id: 'contact:form-widget',
  displayName: 'Contact Form',
  description: 'Embeddable contact form widget',
  category: 'widget',
  permissions: ['public'],
  
  inputs: [
    {
      name: 'submitLabel',
      type: 'string',
      defaultValue: 'Send Message',
      label: 'Submit Button Label'
    },
    {
      name: 'showPhone',
      type: 'boolean',
      defaultValue: false,
      label: 'Show Phone Field'
    },
    {
      name: 'successMessage',
      type: 'string',
      defaultValue: 'Thank you! We will get back to you soon.',
      label: 'Success Message'
    }
  ],
  
  outputs: [
    {
      name: 'submitted',
      type: 'event',
      payload: { contactId: 'string', email: 'string' }
    }
  ]
}

4.2 Design Component Template

<div class="contact-form-widget">
  <form (ngSubmit)="handleSubmit()">
    <div class="form-field">
      <label for="name">Name *</label>
      <input 
        id="name" 
        type="text" 
        [(ngModel)]="formData.name" 
        required 
        placeholder="Your full name"
      />
    </div>

    <div class="form-field">
      <label for="email">Email *</label>
      <input 
        id="email" 
        type="email" 
        [(ngModel)]="formData.email" 
        required 
        placeholder="[email protected]"
      />
    </div>

    @if (showPhone()) {
      <div class="form-field">
        <label for="phone">Phone</label>
        <input 
          id="phone" 
          type="tel" 
          [(ngModel)]="formData.phone" 
          placeholder="(123) 456-7890"
        />
      </div>
    }

    <div class="form-field">
      <label for="message">Message *</label>
      <textarea 
        id="message" 
        [(ngModel)]="formData.message" 
        required 
        rows="5"
        placeholder="Your message..."
      ></textarea>
    </div>

    @if (errorMessage()) {
      <div class="error-message">{{ errorMessage() }}</div>
    }

    @if (successMessage() && submitted()) {
      <div class="success-message">{{ successMessage() }}</div>
    }

    <button 
      type="submit" 
      [disabled]="isSubmitting()"
      class="submit-button"
    >
      {{ isSubmitting() ? 'Sending...' : submitLabel() }}
    </button>
  </form>
</div>

4.3 Wire to Action

In the component logic:

async handleSubmit() {
  this.isSubmitting.set(true);
  this.errorMessage.set('');

  try {
    const result = await firstValueFrom(
      this.actionRegistry.execute('contact:submit-contact', {
        email: this.formData.email,
        name: this.formData.name,
        message: this.formData.message,
        phone: this.formData.phone
      })
    );

    if (result.success) {
      this.submitted.set(true);
      this.submitted.emit({ contactId: result.contactId, email: this.formData.email });
      this.resetForm();
    } else {
      this.errorMessage.set(result.error || 'Submission failed');
    }
  } catch (error) {
    this.errorMessage.set('An error occurred. Please try again.');
  } finally {
    this.isSubmitting.set(false);
  }
}

✅ Checkpoint: Embeddable widget created and wired


Step 5: Test (Validate Module)

Route: /modularity/workbench/test

5.1 Run Unit Tests

The workbench auto-generates tests:

describe('ContactFormModule', () => {
  it('should submit contact successfully', async () => {
    const result = await actionRegistry.execute('contact:submit-contact', {
      email: '[email protected]',
      name: 'Test User',
      message: 'This is a test message'
    });

    expect(result.success).toBe(true);
    expect(result.contactId).toBeDefined();
  });

  it('should reject invalid email', async () => {
    const result = await actionRegistry.execute('contact:submit-contact', {
      email: 'invalid-email',
      name: 'Test User',
      message: 'Test message'
    });

    expect(result.success).toBe(false);
    expect(result.error).toContain('email');
  });
});

5.2 Test in Preview Mode

Click "Preview" to test the widget in isolation:

✅ Checkpoint: All tests passing


Step 6: Deploy (Export Module)

Route: /modularity/workbench/deploy

6.1 Review Module Summary

The workbench shows:

6.2 Select Export Target

Choose "NX Library"

6.3 Configure Export

{
  libraryName: 'contact-form',
  scope: 'modules',
  path: 'modules/contact-form',
  includeTests: true,
  includeComments: true,
  generateDocs: true
}

6.4 Export

Click "Export Module"

Generated Files:

modules/contact-form/
├── src/
│   ├── lib/
│   │   ├── models/
│   │   │   └── contact.model.ts
│   │   ├── actions/
│   │   │   ├── submit-contact.action.ts
│   │   │   └── list-contacts.action.ts
│   │   ├── components/
│   │   │   └── contact-form-widget/
│   │   │       ├── contact-form-widget.component.ts
│   │   │       ├── contact-form-widget.component.html
│   │   │       └── contact-form-widget.component.scss
│   │   ├── services/
│   │   │   └── contact.service.ts
│   │   ├── contact-form.module.ts
│   │   └── contact-form.routes.ts
│   └── index.ts
├── tests/
│   └── contact-form.spec.ts
├── project.json
├── tsconfig.lib.json
├── tsconfig.spec.json
└── README.md

✅ Checkpoint: Module exported as NX library


Step 7: Register in Backend

7.1 Register Module

Endpoint: POST /api/spaces/:spaceId/modules

{
  "name": "Contact Form Module",
  "type": "module",
  "scope": "shared",
  "manifest": {
    "id": "contact-form",
    "version": "1.0.0",
    "name": "Contact Form",
    "description": "Contact form with database storage",
    "category": "forms",
    "icon": "fa-envelope",
    "embeddableComponents": [
      {
        "id": "contact:form-widget",
        "componentType": "ContactFormWidgetComponent",
        "displayName": "Contact Form",
        "description": "Embeddable contact form widget",
        "category": "widget",
        "permissions": ["public"],
        "inputs": [/* ... */],
        "outputs": [/* ... */]
      }
    ],
    "publicActions": [
      "SubmitContactAction",
      "ListContactsAction"
    ]
  },
  "isActive": true
}

✅ Checkpoint: Module registered in backend


Step 8: Create Widget Wrapper (Optional)

If you need editor-specific features (variants, property groups):

Endpoint: POST /api/spaces/:spaceId/widgets

{
  "name": "Contact Form Widget",
  "type": "contact-form",
  "category": "forms",
  "description": "Contact form with customizable styling",
  "icon": "fa-envelope",
  "scope": "private",
  "defaultConfig": {
    "submitLabel": "Send Message",
    "showPhone": false
  },
  "propertyConfig": {
    "groups": [
      {
        "id": "appearance",
        "title": "Appearance",
        "properties": [
          { "key": "backgroundColor", "label": "Background", "type": "color", "defaultValue": "#ffffff" },
          { "key": "buttonColor", "label": "Button Color", "type": "color", "defaultValue": "#3b82f6" },
          { "key": "borderRadius", "label": "Border Radius", "type": "number", "defaultValue": 8 }
        ]
      }
    ]
  },
  "variants": [
    {
      "name": "Default",
      "properties": {
        "backgroundColor": "#ffffff",
        "buttonColor": "#3b82f6",
        "borderRadius": 8
      }
    },
    {
      "name": "Dark Mode",
      "properties": {
        "backgroundColor": "#1f2937",
        "buttonColor": "#8b5cf6",
        "borderRadius": 12
      }
    }
  ]
}

✅ Checkpoint: Widget wrapper created with variants


Step 9: Use in Page Composer

Route: /editor/page/contact-us

9.1 Drag Widget to Canvas

  1. Open Widget Library
  2. Find "Contact Form Widget"
  3. Drag to page canvas

9.2 Configure Properties

In the property panel:

9.3 Bind to Action

In the behavior panel:

9.4 Save Page

Click "Save Page"

✅ Checkpoint: Page composed with contact form


Step 10: Deploy Universe

Route: /modular-universe/universe/my-universe

10.1 Add Module to Canvas

  1. Open Module Library
  2. Find "Contact Form Module"
  3. Place on canvas

10.2 Configure Deployment

Click "Deploy" and configure:

{
  "subdomain": "my-awesome-site",
  "customDomain": "contact.example.com",
  "environment": "production",
  "railwayProjectId": "project-abc123"
}

10.3 Publish

Click "Publish Universe"

Deployment Process:

  1. ✅ Validating universe data...
  2. ✅ Building production bundle...
  3. ✅ Optimizing assets...
  4. ✅ Uploading to hosting provider...
  5. ✅ Configuring domain and SSL...
  6. ✅ Verifying deployment...
  7. ✅ Deployment complete!

Result: https://my-awesome-site.gograb.space

✅ Checkpoint: Universe deployed and accessible


Success Criteria


Next Steps

  1. Add Analytics: Track form submissions
  2. Email Notifications: Send emails on submission
  3. Admin Dashboard: View and manage contacts
  4. Export Contacts: CSV/Excel export
  5. Spam Protection: Add reCAPTCHA

Last Updated: December 9, 2025 - Session 10.2