2024-11-21
3 Days from Mockup to Production: Real CRM Implementation
Client request: "We need a CRM module. Contact management, deal pipeline, task tracking. How long?"
Traditional answer: "3 months with 4 developers."
PMFA answer: "3 days with 1 developer."
Client: "Impossible."
This blog post: Receipts.
The Challenge
Requirements
Module: CRM (Customer Relationship Management)
Entities:
- Contact - customers, leads, prospects
- Company - organizations contacts belong to
- Deal - sales opportunities with pipeline stages
- Task - to-do items assigned to users
- Activity - calls, meetings, emails (audit trail)
Features:
- Contact management (CRUD + search + import)
- Deal pipeline (drag-drop between stages)
- Task assignment & notifications
- Activity timeline per contact
- Dashboard (deals by stage, tasks by user)
- Email integration (send/receive)
Expected timeline (traditional):
- Backend: 6 weeks
- Frontend: 4 weeks
- Integration: 2 weeks
- Testing: 2 weeks
- Total: 3 months
PMFA timeline: 3 days. Let's see how.
Day 1 (Friday Evening): Define Metadata
Step 1: Entity Definitions (YAML)
# meta_model.yaml
Company:
fields:
- name:
type: string
required: true
- industry:
type: enum
values: [technology, finance, retail, manufacturing, other]
- website:
type: url
- employee_count:
type: integer
validation: "employee_count > 0"
relations:
- contacts:
type: has_many(Contact)
Contact:
fields:
- first_name:
type: string
required: true
- last_name:
type: string
required: true
- email:
type: email
required: true
unique: true
- phone:
type: phone
- title:
type: string # Job title
- company_id:
type: reference(Company)
- status:
type: enum
values: [lead, prospect, customer, inactive]
default: lead
relations:
- company:
type: belongs_to(Company)
- deals:
type: has_many(Deal)
- tasks:
type: has_many(Task)
- activities:
type: has_many(Activity)
ui:
list:
columns: [first_name, last_name, email, company.name, status]
filters: [status, company_id]
search: [first_name, last_name, email]
form:
layout: two_column
sections:
- title: "Contact Info"
fields: [first_name, last_name, email, phone]
- title: "Company"
fields: [company_id, title]
- title: "Status"
fields: [status]
Deal:
fields:
- title:
type: string
required: true
- contact_id:
type: reference(Contact)
required: true
- amount:
type: decimal
required: true
validation: "amount > 0"
- probability:
type: integer
validation: "probability >= 0 AND probability <= 100"
default: 50
- stage:
type: enum
values: [lead, qualified, proposal, negotiation, won, lost]
default: lead
- expected_close_date:
type: date
- assigned_to:
type: reference(User)
workflow:
states:
- lead:
transitions:
- to: qualified
action: qualify
conditions: ["probability >= 20"]
- to: lost
action: mark_lost
- qualified:
transitions:
- to: proposal
action: send_proposal
- to: lost
action: mark_lost
- proposal:
transitions:
- to: negotiation
action: start_negotiation
- to: lost
action: mark_lost
- negotiation:
transitions:
- to: won
action: close_won
conditions: ["amount > 0", "expected_close_date IS NOT NULL"]
- to: lost
action: mark_lost
- won:
final: true
on_enter:
- trigger: create_invoice
- notify: finance_team
- lost:
final: true
requires_reason: true
ui:
kanban:
group_by: stage
card_fields: [title, contact.name, amount, probability]
list:
columns: [title, contact.name, amount, stage, expected_close_date]
filters: [stage, assigned_to, date_range]
Task:
fields:
- title:
type: string
required: true
- description:
type: text
- due_date:
type: datetime
required: true
- priority:
type: enum
values: [low, medium, high, urgent]
default: medium
- status:
type: enum
values: [pending, in_progress, done, cancelled]
default: pending
- assigned_to:
type: reference(User)
required: true
- related_to:
type: polymorphic[Contact, Deal]
workflow:
states:
- pending:
transitions:
- to: in_progress
action: start
- in_progress:
transitions:
- to: done
action: complete
- to: cancelled
action: cancel
- done:
final: true
- cancelled:
final: true
notifications:
- event: due_date_approaching
trigger: "due_date - NOW() < '24 hours'"
recipients: [assigned_to]
template: task_reminder
Activity:
fields:
- type:
type: enum
values: [call, meeting, email, note]
- subject:
type: string
required: true
- description:
type: text
- contact_id:
type: reference(Contact)
required: true
- created_by:
type: reference(User)
- scheduled_at:
type: datetime
ui:
timeline:
order_by: created_at DESC
group_by: date
Time spent: 2 hours (writing YAML)
Lines: ~250 lines of metadata
Day 2 (Saturday Morning): Code Generation
Step 2: Generate Backend + Frontend
$ pmfa generate --from meta_model.yaml --output src/
Analyzing metadata...
✅ Found 5 entities: Company, Contact, Deal, Task, Activity
Generating database schema...
✅ Created 12 tables (including junction tables)
✅ Created 24 indexes
✅ Created 8 foreign keys
Generating TypeScript types...
✅ Generated 45 interfaces
✅ Generated 15 enums
✅ Generated 25 DTOs (Data Transfer Objects)
Generating REST API...
✅ Generated 60 API endpoints:
- GET /api/contacts
- POST /api/contacts
- GET /api/contacts/:id
- PUT /api/contacts/:id
- DELETE /api/contacts/:id
- POST /api/deals/:id/qualify
- POST /api/deals/:id/send-proposal
... (54 more)
Generating validation logic...
✅ Generated 32 validators
✅ Generated 18 business rules
Generating workflow engines...
✅ Generated 3 state machines (Deal, Task, Activity)
Generating UI components...
✅ Generated 15 React forms
✅ Generated 8 list views
✅ Generated 1 Kanban board (Deal pipeline)
✅ Generated 1 Timeline component (Activity)
Generating tests...
✅ Generated 127 unit tests
✅ Generated 45 integration tests
Total time: 23 seconds
What PMFA generated:
- 12 database tables (PostgreSQL)
- 45 TypeScript interfaces
- 60 REST API endpoints (Express.js)
- 15 React components (forms, lists, dashboards)
- 3 workflow engines
- 127 unit tests
Time spent: 23 seconds (automatic)
Step 3: Deploy to Staging
$ pmfa deploy --environment staging
Building...
✅ Compiled TypeScript (8 seconds)
✅ Built React frontend (12 seconds)
✅ Generated Docker image (15 seconds)
Deploying to Kubernetes...
✅ Applied database migrations (3 seconds)
✅ Deployed backend pods (8 seconds)
✅ Deployed frontend (5 seconds)
Staging URL: https://crm-staging.example.com
Total time: 51 seconds
Result: Fully functional CRM running on staging environment.
Time spent: 51 seconds (automatic)
Day 2 (Saturday Afternoon): Custom Logic
Step 4: Add Business-Specific Requirements
Client: "When a deal is won, we need to:"
- Create an invoice automatically
- Notify finance team
- Update contact status to 'customer'
PMFA custom logic:
// src/custom/deal_hooks.ts
import { PMFA } from '@pmfa/runtime';
// Hook: Deal state change
PMFA.registerHook('Deal.onStateChange', async (deal, oldStage, newStage) => {
if (newStage === 'won') {
// 1. Create invoice
const invoice = await PMFA.entities.Invoice.create({
deal_id: deal.id,
contact_id: deal.contact_id,
amount: deal.amount,
due_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
status: 'draft'
});
// 2. Notify finance team
await PMFA.notifications.send({
to: 'finance@company.com',
template: 'new_invoice_created',
data: { invoice, deal }
});
// 3. Update contact status
await PMFA.entities.Contact.update(deal.contact_id, {
status: 'customer'
});
}
});
// Validator: Amount must match contact's budget
PMFA.registerValidator('Deal.beforeCreate', async (deal) => {
const contact = await PMFA.entities.Contact.findById(deal.contact_id);
if (deal.amount > contact.approved_budget) {
throw new PMFA.ValidationError(
`Deal amount ($${deal.amount}) exceeds approved budget ($${contact.approved_budget})`
);
}
});
Time spent: 1 hour (writing custom logic)
Lines: 50 lines of TypeScript
Day 3 (Sunday): Testing & Polish
Step 5: Run Tests
$ pmfa test
Running unit tests...
✅ 127/127 passed (auto-generated)
Running custom tests...
✅ 12/12 passed
Running integration tests...
✅ 45/45 passed
Code coverage: 94%
Total time: 2 minutes 15 seconds
Time spent: 2 minutes (automatic)
Step 6: Import Sample Data
$ pmfa import --from sample_data.csv
Importing contacts...
✅ Created 150 contacts
Importing companies...
✅ Created 50 companies
Importing deals...
✅ Created 80 deals
Total time: 12 seconds
Time spent: 12 seconds (automatic)
Step 7: Deploy to Production
$ pmfa deploy --environment production
Building...
✅ Compiled TypeScript
✅ Built React frontend
✅ Optimized assets
Deploying...
✅ Database migrations applied (0 downtime)
✅ Backend deployed (rolling update)
✅ Frontend deployed (CDN cached)
Production URL: https://crm.example.com
Monitoring:
✅ Health check: OK
✅ API latency: 45ms (avg)
✅ Frontend load time: 1.2s
Total time: 3 minutes 12 seconds
Time spent: 3 minutes (automatic)
Final Timeline
| Day | Task | Time |
|---|---|---|
| Day 1 (Friday) | Define YAML metadata | 2 hours |
| Day 2 (Saturday AM) | Generate code + deploy staging | 23 sec + 51 sec |
| Day 2 (Saturday PM) | Write custom business logic | 1 hour |
| Day 3 (Sunday) | Testing + import data + deploy prod | 5 min |
| Total | 3 hours 6 minutes |
Developer-days: 0.4 (one developer, 3 hours spread over 3 days)
Comparison: PMFA vs Traditional
Traditional Framework (React + Node.js + TypeScript)
Backend development:
// Just ONE entity (Contact) requires:
// 1. Database migration (20 lines)
CREATE TABLE contacts (
id SERIAL PRIMARY KEY,
first_name VARCHAR(100),
last_name VARCHAR(100),
email VARCHAR(255) UNIQUE,
-- ... 10 more fields
);
// 2. TypeScript entity (40 lines)
interface Contact {
id: number;
firstName: string;
lastName: string;
email: string;
// ... 10 more fields
}
// 3. Repository (80 lines)
class ContactRepository {
async findAll() { /* ... */ }
async findById(id: number) { /* ... */ }
async create(data: CreateContactDto) { /* ... */ }
async update(id: number, data: UpdateContactDto) { /* ... */ }
async delete(id: number) { /* ... */ }
}
// 4. Service layer (120 lines)
class ContactService {
constructor(private repo: ContactRepository) {}
async create(data: CreateContactDto) {
// Validation
if (!data.email) throw new Error('Email required');
if (!isValidEmail(data.email)) throw new Error('Invalid email');
// Check duplicates
const existing = await this.repo.findByEmail(data.email);
if (existing) throw new Error('Email already exists');
// Create
return this.repo.create(data);
}
// ... 80 more lines for update, delete, search, etc.
}
// 5. REST API controller (60 lines)
@Controller('/api/contacts')
class ContactController {
@Get('/')
async list() { /* ... */ }
@Post('/')
async create(@Body() dto: CreateContactDto) { /* ... */ }
// ... 40 more lines
}
// 6. DTOs (30 lines)
class CreateContactDto {
@IsString() firstName: string;
@IsEmail() email: string;
// ... validation decorators
}
// 7. Unit tests (150 lines)
describe('ContactService', () => {
it('should create contact', async () => { /* ... */ });
it('should reject duplicate email', async () => { /* ... */ });
// ... 20 more tests
});
Frontend development:
// Contact form component (200 lines)
function ContactForm({ contactId }: Props) {
const [contact, setContact] = useState<Contact>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
useEffect(() => {
if (contactId) {
fetchContact(contactId);
}
}, [contactId]);
const fetchContact = async (id: number) => {
setLoading(true);
try {
const data = await api.get(`/contacts/${id}`);
setContact(data);
} catch (err) {
setErrors({ general: err.message });
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
// Client-side validation
const newErrors: Record<string, string> = {};
if (!contact.firstName) newErrors.firstName = 'Required';
if (!contact.email) newErrors.email = 'Required';
if (contact.email && !isValidEmail(contact.email)) {
newErrors.email = 'Invalid email';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// Submit
setLoading(true);
try {
if (contactId) {
await api.put(`/contacts/${contactId}`, contact);
} else {
await api.post('/contacts', contact);
}
navigate('/contacts');
} catch (err) {
setErrors({ general: err.message });
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="firstName"
value={contact.firstName || ''}
onChange={(e) => setContact({ ...contact, firstName: e.target.value })}
/>
{errors.firstName && <span className="error">{errors.firstName}</span>}
{/* ... 150 more lines for other fields */}
</form>
);
}
Total for ONE entity (Contact):
- Backend: 500 lines
- Frontend: 200 lines
- Tests: 150 lines
- Total: 850 lines
CRM has 5 entities: 850 × 5 = 4,250 lines minimum
Plus:
- Workflow engines (500 lines)
- Dashboard (300 lines)
- Integration logic (400 lines)
- Grand total: ~5,500 lines
Time estimate:
- 1 developer: 3 months
- 2 developers: 6-8 weeks
- 4 developers: 4-6 weeks
PMFA Approach
Metadata: 250 lines of YAML
Custom logic: 50 lines of TypeScript
Total: 300 lines
Generated automatically:
- 12 database tables
- 60 API endpoints
- 15 React components
- 127 unit tests
- 3 workflow engines
Time: 3 hours over 3 days
Key Insights
Why PMFA is 30x Faster
- No boilerplate code - PMFA generates standard CRUD operations
- No manual testing - Auto-generated tests cover 80% of logic
- No deployment scripts - PMFA handles database migrations + rolling updates
- No UI development - Forms/lists generated from metadata
What You Still Write
Custom business logic only:
- Unique validations (20% of rules)
- Integration with external systems
- Complex calculations
- Business-specific workflows
Result: You write 300 lines instead of 5,500 lines.
Conclusion
Traditional framework:
- ❌ 5,500 lines of code
- ❌ 3 months with 4 developers
- ❌ $150K development cost
PMFA platform:
- ✅ 300 lines (250 YAML + 50 TypeScript)
- ✅ 3 days with 1 developer
- ✅ $5K development cost
Savings: $145K + 2.5 months faster to market
The secret: PMFA treats entities, workflows, and UI as declarative metadata, not imperative code. Generate once, deploy everywhere.
Want to build ERP modules in days instead of months? Contact us at office@nonnotech.com for PMFA technical demo.