Building CreateMore: A Full-Stack ERP System for Creative Agencies

27 August 2025 (2d ago)

CreateMore Dashboard

Table of Contents

  1. Introduction
  2. The Challenge
  3. Architecture Overview
  4. Core Features Deep Dive
  5. Database Design
  6. Technical Challenges & Solutions
  7. Development Workflow
  8. Performance Optimisations
  9. Deployment & Infrastructure
  10. Lessons Learned
  11. Multi-Tenant Architecture
  12. Impact & Results
  13. Future Enhancements
  14. Conclusion

Introduction

CreateMore is a comprehensive ERP (Enterprise Resource Planning) system I developed solo for creative agencies and design studios. The project began in September 2024 when weareink.co.uk needed a massive update to replace their archaic existing system. What started as a targeted solution has evolved into a full-featured business management platform that I continue to maintain and improve to this day.

The Challenge

Creative agencies face unique operational challenges:

Existing solutions had way too many features we didn't need - we required a much lighter, targeted approach with only the specific functionality our workflows demanded. This led me to build CreateMore as a tailored solution.

Architecture Overview

CreateMore follows a modern full-stack architecture with clear separation of concerns:

Backend Architecture

Frontend Architecture

Core Features Deep Dive

1. Project Management & Gantt Charts

The project management system is built around a sophisticated Gantt chart implementation:

// Gantt configuration with custom validation
const projectConfig = useMemo(() => ({
    manuallySchedule: true,
    autoSetConstraints: false,
    milestoneLayoutMode: 'manual',
    transport: {
        load: {
            url: `${process.env.NEXT_PUBLIC_API_URL}/api/gantt/data/${activeProject?.id}`,
            transform: {
                responseSuccess: ({ response }) => {
                    // Transform resource images to include full URL path
                    if (response.resources?.rows) {
                        response.resources.rows = response.resources.rows.map(resource => {
                            if (resource.image) {
                                return {
                                    ...resource,
                                    imageUrl: `${process.env.NEXT_PUBLIC_API_URL}/uploads/${resource.image}`
                                };
                            }
                            return resource;
                        });
                    }
                    return response;
                }
            }
        },
        sync: {
            url: `${process.env.NEXT_PUBLIC_API_URL}/api/gantt/sync/${activeProject?.id}`
        }
    }
}), [activeProject?.id]);

Key features include:

2. Resource Scheduling System

The resource scheduler provides a calendar-based view for team allocation with a sophisticated Bryntum Scheduler implementation that allows managers to visualise and plan resource allocation across multiple projects and time periods.

Features include:

3. Timesheet Management

The timesheet system handles complex time tracking requirements:

// Weekly timesheet data processing
const processWeekData = (allData, dateRange) => {
    const selectedWeek = getISOWeek(dateRange.startDate);
    const selectedYear = getYear(dateRange.startDate);
 
    const filteredData = allData.map((member) => {
        const timesheetByDay = {};
        weekDays.forEach((day) => {
            timesheetByDay[day.dayKey] = 0;
        });
 
        const memberTimesheets = Array.isArray(member.data) ? member.data : [];
        
        memberTimesheets.forEach((entry) => {
            const entryWeek = parseInt(entry.week);
            const entryYear = parseInt(entry.year);
 
            if (entryWeek === selectedWeek && entryYear === selectedYear) {
                weekDays.forEach((day) => {
                    const dayKey = day.dayKey;
                    const hours = parseFloat(entry[dayKey]) || 0;
                    timesheetByDay[dayKey] += hours;
                });
            }
        });
 
        const total = Object.values(timesheetByDay).reduce((a, b) => a + b, 0);
        return {
            name: member.memberName,
            ...timesheetByDay,
            total,
        };
    });
 
    setProcessedData(filteredData);
};

Features include:

4. Invoice Generation & PDF Creation

Dynamic invoice generation uses Puppeteer for pixel-perfect PDFs:

export const downloadExpensePDF = async (req) => {
    const { expenseSheetId, selectedPersonName, pdfName } = req.body;
    
    // Fetch expense data with relationships
    const expenses = await ExpenseEntry.findAll({
        where: { expense_sheet_id: expenseSheetId },
        include: [
            { model: Project, as: 'project' },
            { model: ServiceProject, as: 'serviceProject' },
            { model: ExpenseSheet, as: 'expenseSheet' }
        ],
        order: [['date', 'DESC']]
    });
 
    // Generate paginated PDF content
    const browser = await puppeteer.launch({
        headless: 'new',
        args: ['--no-sandbox']
    });
    
    const page = await browser.newPage();
    await page.setContent(pageContent);
    await page.evaluate(() => document.fonts.ready);
    
    await page.pdf({
        path: tempPdfPath,
        format: 'A4',
        printBackground: true,
        landscape: true,
        scale: 0.5
    });
};

Features include:

5. Automated Email & Notification System

CreateMore includes a comprehensive automated notification system:

// Scheduled email jobs with queue management
const scheduleEmailJobs = () => {
    // Run every Monday and Wednesday at 9:30 AM to send timesheet reminders
    cron.schedule('30 9 * * 1,3', async () => {
        console.log('Running timesheet reminder emails...');
        await sendTimesheetReminders();
    });
 
    // Notify managers about incomplete timesheets at 10:00 AM
    cron.schedule('0 10 * * 1,3', async () => {
        console.log('Running holiday manager notifications...');
        await notifyHolidayManagersAboutIncompleteTimesheets();
    });
 
    // Send invoice reminders at 11:00 AM
    cron.schedule('0 11 * * 1,3', async () => {
        console.log('Running invoice reminder emails...');
        await sendInvoiceReminders();
    });
};

Features include:

Specialised system for handling CAF binary archive files (named after the cathy software author's choice of extension):

// CAF file indexing with Python integration
const indexCafFile = async (cafFilePath) => {
    const scriptPath = path.join(backendDir, 'cathy.py');
    const filename = path.basename(cafFilePath);
    const outputPath = path.join(indexDir, `${filename}.json`);
 
    // Use Python script to extract metadata and index content
    const cmd = `python3 "${scriptPath}" search-json "" "${cafFilePath}" true false > "${outputPath}"`;
    
    await execPromise(cmd, { timeout: 60000 });
    return { success: true, indexPath: outputPath };
};

Features include:

7. Comprehensive Expense Management

Full expense tracking and approval workflow:

// Expense field updates with validation
const handleExpenseFieldChange = useCallback(
    async (expenseEntryId, field, newValue) => {
        try {
            const updates = { [field]: newValue };
            const response = await fetch(
                `${process.env.NEXT_PUBLIC_API_URL}/api/expenses/entry/${expenseEntryId}/update-expense`,
                {
                    method: "PUT",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify(updates),
                    credentials: "include",
                }
            );
            
            if (!response.ok) throw new Error("Failed to update expense entry");
            updateRowLocal(expenseEntryId, { [field]: newValue });
        } catch (error) {
            console.error(`Error updating expense entry ${field}:`, error);
            toast.error("Failed to update expense");
        }
    },
    [updateRowLocal]
);

Features include:

8. Skills Survey & Analytics

Skills Survey

Built-in competency tracking and reporting:

// Skills analytics with visualisation
export default function SkillsSurveyResultsPage() {
    const [scope, setScope] = useState('company');
    const [category, setCategory] = useState('all');
    
    // Fetch aggregated skills data
    const fetchSkillsMatrix = async () => {
        const response = await fetch(
            `${process.env.NEXT_PUBLIC_API_URL}/api/skills-survey/heatmap?scope=${scope}&category=${category}`,
            { credentials: 'include' }
        );
        return response.json();
    };
 
    return (
        <ResponsiveContainer width="100%" height={400}>
            <RadarChart data={skillsData}>
                <PolarGrid />
                <PolarAngleAxis dataKey="skill" />
                <PolarRadiusAxis angle={90} domain={[0, 5]} />
                <Radar name="Team Average" dataKey="average" stroke="#8884d8" fill="#8884d8" fillOpacity={0.6} />
            </RadarChart>
        </ResponsiveContainer>
    );
}

Features include:

9. Advanced Holiday Management

Sophisticated leave management with approvals:

// Holiday calculation with partial days
const holidayData = useMemo(() => {
    const standardHoursPerDay = (process.env.NEXT_PUBLIC_ACTIVE_TENANT === 'artray' ? 8 : 7.5);
    
    const spentDays = data.reduce((sum, holiday) => {
        if (holiday.type !== "Annual Holiday") return sum;
        
        const hoursPerDay = holiday.hours_per_day || standardHoursPerDay;
        const daysProportion = hoursPerDay === standardHoursPerDay
            ? holiday.days
            : holiday.days * (hoursPerDay / standardHoursPerDay);
            
        return sum + daysProportion;
    }, 0);
 
    return {
        total: holidayAllowance,
        spent: Number(spentDays.toFixed(2)),
        remaining: Number((totalDays - spentDays).toFixed(2))
    };
}, [data, holidayAllowance]);

Features include:

10. Authentication & Authorization

Sophisticated auth system with LDAP integration:

// LDAP authentication middleware
const ldapAuthMiddleware = async (req, res) => {
    const { email, password } = req.body;
    
    try {
        // Authenticate against LDAP
        const ldapResult = await authDN(email, password);
        
        if (ldapResult.success) {
            // Fetch user from database with permissions
            const user = await User.findOne({
                where: { email },
                include: [
                    {
                        model: TeamMember,
                        as: 'teamMember',
                        include: [
                            { model: TeamMemberAccess, as: 'access' },
                            { model: TeamHierarchy, as: 'hierarchy' }
                        ]
                    }
                ]
            });
            
            // Generate JWT token
            const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { 
                expiresIn: '7d' 
            });
            
            res.cookie('token', token, {
                httpOnly: true,
                secure: process.env.NODE_ENV === 'production',
                sameSite: 'strict',
                maxAge: 7 * 24 * 60 * 60 * 1000
            });
        }
    } catch (error) {
        console.error('LDAP authentication failed:', error);
    }
};

Features include:

Database Design

The database schema supports complex business relationships:

Core Entities

Key Relationships

-- Project hierarchy
Projects -> ProjectTasks -> ProjectItems
Projects -> TeamMembers (assignments)
Projects -> Invoices (billing)
 
-- Time tracking
TeamMembers -> Timesheets -> ProjectTasks
TeamMembers -> Events (scheduling)
 
-- Financial tracking  
Clients -> Projects -> Invoices
ExpenseSheets -> ExpenseEntries -> Projects
Key Relationships

Technical Challenges & Solutions

1. Complex Scheduling Logic

Challenge: Managing resource conflicts and availability Solution: Custom validation layer with real-time conflict detection

2. Multi-Currency Support

Challenge: Handling exchange rates and currency conversions Solution: Dedicated currency exchange table with historical rates

3. PDF Generation Performance

Challenge: Large documents causing memory issues Solution: Pagination and streaming with temporary file cleanup

4. Real-Time Updates

Challenge: Keeping UI synchronised across user sessions Solution: Optimistic updates with automatic refresh triggers

5. File Management

Challenge: Organising and archiving large file collections Solution: Structured directory system with database indexing

Development Workflow

Database Management

Testing Strategy

Example E2E Test Coverage

// Holiday booking flow with partial days and timesheet validation
test('Complex holiday booking with partial days', async ({ page }) => {
    // Create partial-day holiday (uncheck All Day, set hours to 4)
    await page.getByRole('checkbox', { name: 'All Day' }).uncheck();
    await page.getByLabel('Hours').fill('4');
    
    // Select tomorrow as start date and set status to Approved
    await page.getByLabel('Start Date').fill(tomorrow);
    await page.getByLabel('Status').selectOption('Approved');
    
    // Save and verify event creation
    await page.getByRole('button', { name: 'Save' }).click();
    
    // Navigate to Timesheets and verify exactly 4h on the correct weekday
    await page.goto('/timesheets');
    const timesheetCell = page.locator(`[data-day="${tomorrowWeekday}"]`);
    await expect(timesheetCell).toContainText('4.0');
});

Performance Optimisations

Frontend

Performance Graph

Backend

Deployment & Infrastructure

Production Setup

Security Measures

Lessons Learned

What Worked Well

  1. Modular Architecture: Clear separation enabled parallel development
  2. Component Libraries: Consistent UI with Radix UI and shadcn foundation
  3. Database Design: Flexible schema supported feature evolution
  4. Custom Solutions: Building targeted features instead of using bloated third-party tools

What I'd Do Differently

  1. State Management: Consider Redux Toolkit for complex state
  2. Testing Coverage: Implement TDD from the beginning
  3. Documentation: Comprehensive API documentation from the beginning
  4. Monitoring: Earlier implementation of observability tools

Multi-Tenant Architecture

CreateMore supports multiple client configurations through environment-based tenancy (elegant and pragmatic for small scale):

// Tenant-specific configurations
const standardHoursPerDay = (process.env.NEXT_PUBLIC_ACTIVE_TENANT === 'artray' ? 8 : 7.5);
 
// Different business rules per tenant
const tenantConfig = {
    artray: {
        workingHours: 8,
        currency: 'BGN',
        emailDomain: '@artray.bg'
    },
    ink: {
        workingHours: 7.5, 
        currency: 'GBP',
        emailDomain: '@weareink.co.uk'
    }
};

This allows the same codebase to serve multiple organisations with different:

Impact & Results

CreateMore has transformed operations across multiple design studios:

Operational Improvements

Technical Achievements

Future Enhancements

Planned Features

Areas for Improvement

Conclusion

Building CreateMore was a significant undertaking that required balancing feature complexity with development speed. The modular architecture and modern tech stack enabled rapid iteration while maintaining code quality.

The project demonstrates how domain-specific business requirements can drive technical decisions, resulting in a system that truly fits the unique needs of creative agencies. While there's always room for improvement, CreateMore successfully solved the core operational challenges we set out to address.

For other developers considering similar projects, my key advice is:

  1. Start with the data model - get relationships right early
  2. Build custom solutions - if you have time, build from scratch rather than relying on complex third-party tools
  3. Plan for evolution - requirements will change
  4. Focus on the user - technical elegance means nothing if users struggle

The architecture showcases modern web development practices while solving real business problems. It's a testament to what's possible when you align technical capabilities with business needs.


CreateMore continues to evolve, powering the daily operations of 2 design studios while serving as a playground for new technologies and approaches. The journey from concept to production system has been both challenging and rewarding, proving that sometimes the best tools are the ones you build yourself.