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:
- Stores contact submissions in the database
- Exposes a public action for form submission
- Provides an embeddable widget for page composition
- Integrates with Singularity for flow execution
- Deploys as an NX library
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:
Trigger (Manual)
- Type:
trigger_manual - Outputs:
email,name,message,phone
- Type:
Validate Input (Condition)
- Type:
logic_condition - Condition:
email.includes('@') && name.length >= 2 && message.length >= 10 - Outputs:
valid(boolean)
- Type:
Create Contact (Data Operation)
- Type:
data_create - Collection:
contacts - Data:
{ email, name, message, phone, createdAt: Date.now(), status: 'new' } - Outputs:
contactId
- Type:
Emit Event (Event)
- Type:
event_emit - Event:
contact:submitted - Payload:
{ contactId, email, name }
- Type:
Return Success (Return)
- Type:
output_return - Value:
{ contactId, success: true, message: 'Contact submitted successfully' }
- Type:
Connections:
- Trigger → Validate Input
- Validate Input (true) → Create Contact
- Validate Input (false) → Return Error
- Create Contact → Emit Event
- Emit Event → Return Success
✅ 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:
- Fill out form
- Submit
- Verify success message
- Check data in storage
✅ Checkpoint: All tests passing
Step 6: Deploy (Export Module)
Route: /modularity/workbench/deploy
6.1 Review Module Summary
The workbench shows:
- ✅ 1 Entity (Contact)
- ✅ 2 Actions (SubmitContact, ListContacts)
- ✅ 1 Embeddable (ContactFormWidget)
- ✅ All tests passing
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
- Open Widget Library
- Find "Contact Form Widget"
- Drag to page canvas
9.2 Configure Properties
In the property panel:
- Set
submitLabel: "Get in Touch" - Enable
showPhone: true - Select variant: "Default"
9.3 Bind to Action
In the behavior panel:
- Event:
submitted - Action:
contact:submit-contact - Parameters: Auto-mapped from event payload
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
- Open Module Library
- Find "Contact Form Module"
- 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:
- ✅ Validating universe data...
- ✅ Building production bundle...
- ✅ Optimizing assets...
- ✅ Uploading to hosting provider...
- ✅ Configuring domain and SSL...
- ✅ Verifying deployment...
- ✅ Deployment complete!
Result: https://my-awesome-site.gograb.space
✅ Checkpoint: Universe deployed and accessible
Success Criteria
- Contact entity defined
- Storage configured with flash persistence
- Submit and List actions created
- Action flows built in Singularity
- Contact form widget created
- Widget wired to actions
- Module exported as NX library
- Module registered in backend
- Widget wrapper created (optional)
- Page composed with widget
- Universe deployed to production
- Live URL accessible
Next Steps
- Add Analytics: Track form submissions
- Email Notifications: Send emails on submission
- Admin Dashboard: View and manage contacts
- Export Contacts: CSV/Excel export
- Spam Protection: Add reCAPTCHA
Last Updated: December 9, 2025 - Session 10.2