The Tools We Tried (And Why They All Failed the Same Way)
Before FlowBoard existed, Harbor ran on a patchwork of tools that would look familiar to any agency. Asana for project tracking. Harvest for time tracking. Google Sheets for capacity planning. Slack for status updates. Notion for documentation. Each tool was decent in isolation. Together, they created a fragmented operating system where no one had a complete picture of what was happening across the agency.
The breaking point came during a quarter where we were running 14 active client projects simultaneously. A developer was double-booked across three projects without anyone realizing it until two deadlines slipped in the same week. The capacity data lived in a spreadsheet that was updated weekly (optimistically). The project timeline lived in Asana. The time tracking lived in Harvest. No single view connected these three dimensions — and by the time someone manually reconciled the data, the problem had already caused damage.
That incident wasn’t a one-off. It was the most visible symptom of a systemic problem: our operational data was scattered across six tools that didn’t share a data model. Each tool had its own notion of a “project,” its own notion of a “team member,” and its own way of tracking time and progress. Integrations existed (Asana-to-Harvest, Harvest-to-Sheets) but they were fragile, laggy, and lost information in translation.
We evaluated every agency management platform on the market: Teamwork, Monday.com, ClickUp, Productive.io, Scoro, Kantata, and several others. They all shared the same fundamental problem — they were built for generic project management and bolted on agency-specific features as an afterthought. Resource allocation was always a secondary feature, never a first-class concern. Time tracking integration was always a two-way sync that drifted. Financial visibility was always a reporting add-on that ran on stale data, not a real-time feed.
So we built FlowBoard. Not because we wanted to build a project management tool — the world has enough of those — but because we needed an agency operating system that treats resource allocation, project delivery, and financial health as three facets of the same reality, updated from the same data, in real time.
The Three Pillars Architecture
FlowBoard is built around three interconnected pillars: Delivery, Capacity, and Financials. Every feature belongs to one pillar but references the others. This isn’t a philosophical framework — it’s literally the data model. A task belongs to the Delivery pillar but its estimated hours affect Capacity, and its actual hours affect Financials. Changing any one datum ripples through all three views.
Pillar 1: Delivery
The delivery pillar handles what you’d expect from project management: tasks, milestones, timelines, and status tracking. We deliberately didn’t reinvent this. Tasks have statuses, assignees, due dates, and dependencies. Projects have phases and milestones. Status workflows are customizable per project. Standard stuff that works.
What’s different is how delivery data feeds the other two pillars. Every task has an estimated effort (in hours) and an actual effort (aggregated from time entries). The difference between these two numbers — the burn efficiency — is the most important metric in an agency, and it’s shockingly hard to get from existing tools without manual reconciliation.
interface Task {
id: string;
projectId: string;
title: string;
status: 'backlog' | 'todo' | 'in_progress' | 'review' | 'done';
assigneeId: string | null;
estimatedHours: number;
actualHours: number; // Aggregated from time entries in real time
billableRate: number; // Inherited from project rate card, overridable
dueDate: Date | null;
phase: string;
dependencies: string[]; // Task IDs this task depends on
tags: string[]; // For filtering and reporting
}
// The metric that drives project health
const burnEfficiency = (task: Task): number => {
if (task.actualHours === 0) return 1;
return task.estimatedHours / task.actualHours;
// > 1.0 = under budget (delivered in fewer hours than estimated)
// = 1.0 = on budget (exactly as estimated)
// < 1.0 = over budget (took more hours than estimated)
};
We also track estimation accuracy over time. If a developer consistently estimates 8 hours for tasks that take 12, FlowBoard surfaces this pattern (visible only to the PM and the developer, not publicly). This isn't about blame — it's about calibration. Accurate estimates are the foundation of everything else: capacity planning, financial projections, deadline commitments. Improving estimation accuracy by even 15% has cascading positive effects across the entire system.
Pillar 2: Capacity
This is where FlowBoard diverges from every tool we evaluated. Capacity isn't a report you generate at the end of the week. It isn't a tab buried under three levels of navigation. It's a live, interactive allocation board that serves as the homepage of FlowBoard — the first thing every PM sees when they open the application.
Every team member has a weekly capacity (default 40 hours, adjustable for part-time staff, PTO, holidays, training days, etc.). Every project has allocated hours per person per week. The allocation board shows a grid: team members on the Y-axis, weeks on the X-axis, colored cells showing allocation percentage.
The color coding is intentionally simple:
- Green (0-80%): Healthy. Room for unexpected work or scope changes.
- Yellow (80-100%): Full but manageable. No buffer for surprises.
- Red (over 100%): Overallocated. Something will slip unless allocation changes.
- Gray: PTO, holiday, or otherwise unavailable.
const calculateWeeklyAllocation = (
memberId: string,
weekStart: Date
): AllocationSummary => {
const capacity = getMemberCapacity(memberId, weekStart); // Accounts for PTO
const allocations = getProjectAllocations(memberId, weekStart);
const totalAllocated = allocations.reduce((sum, a) => sum + a.hours, 0);
const percentage = capacity.availableHours > 0
? (totalAllocated / capacity.availableHours) * 100
: 0;
return {
memberId,
weekStart,
availableHours: capacity.availableHours,
allocatedHours: totalAllocated,
remainingHours: Math.max(0, capacity.availableHours - totalAllocated),
percentage,
status: percentage > 100 ? 'over' : percentage > 80 ? 'full' : 'available',
projects: allocations.map(a => ({
projectId: a.projectId,
projectName: a.projectName,
hours: a.hours,
percentage: (a.hours / capacity.availableHours) * 100,
})),
};
};
The allocation board updates in real time as tasks are assigned, estimates change, and time is logged. This is the single view that prevented the double-booking problem that motivated FlowBoard in the first place. When a PM tries to assign a developer who's already at 95% allocation for the week, FlowBoard shows a warning immediately — not three days later when the spreadsheet gets updated.
We also added forward-looking capacity planning. Project managers can draft future allocations for projects in the pipeline that haven't started yet. This answers the perpetual agency question: "If we win this deal next month, do we have the people to deliver it?" You can toggle draft allocations on and off to model different scenarios without affecting the live board. Win the deal? Convert the draft to a real allocation with one click. Lose the deal? Delete the draft and the board reverts automatically.
One feature we added after three months of use was skill-based filtering. Not all developers are interchangeable. When planning capacity for a React Native project, you need to see availability of developers with React Native experience, not just any developer. Team members have skill tags, and the allocation board can be filtered by skill to show relevant availability. This was a direct response to a situation where we had "available capacity" on paper but the available developers didn't have the right skills for the project.
Pillar 3: Financials
Most agencies track financials after the fact — invoicing at end of month, profitability analysis at end of quarter. By the time you discover a project is over budget, it's too late to course-correct. FlowBoard provides real-time financial health per project, updated with every time entry.
Every project has a budget model: fixed-price, time-and-materials, or retainer. Each model has different health metrics:
interface ProjectFinancials {
projectId: string;
budgetModel: 'fixed' | 'time_and_materials' | 'retainer';
// Fixed price
contractValue?: number;
budgetedHours?: number;
consumedHours: number;
burnRate: number; // consumedHours / budgetedHours
deliveryProgress: number; // Weighted: completed task hours / total estimated hours
healthScore: number; // deliveryProgress / burnRate
projectedOverrun: number; // Estimated hours remaining - budget remaining
// Time & Materials
billableHours: number;
nonBillableHours: number;
billablePercentage: number; // Target: > 80%
revenue: number; // billableHours * effective blended rate
effectiveRate: number; // Actual revenue / total hours (incl. non-billable)
// Retainer
monthlyHoursContracted?: number;
monthlyHoursUsed: number;
utilizationPercentage: number;
rolloverHours: number; // Unused hours from previous months
rolloverPolicy: 'accumulate' | 'forfeit' | 'cap';
}
The healthScore metric for fixed-price projects is the most actionable number in FlowBoard. It divides delivery progress by budget consumption. If a project is 50% delivered but has consumed 70% of its budget, the health score is 0.71 — a clear, unambiguous signal that something needs to change. Either scope gets reduced, the team adjusts approach, or the PM has a conversation with the client about a change order. The key is that this signal appears in real time, not in a monthly finance meeting when the damage is done.
For T&M projects, the critical metric is billable percentage. Every non-billable hour is revenue left on the table. FlowBoard tracks this per-person per-project, so PMs can identify patterns — if a developer is spending 30% of their project hours on non-billable internal communication, that's a process problem worth fixing.
For retainers, the game is different: under-utilization means the client is paying for hours they're not using (which eventually leads to churn), and over-utilization means we're doing free work. FlowBoard shows utilization trending through the month so PMs can proactively reach out to under-utilizing clients or flag over-utilization before it accumulates.
The Technical Stack
FlowBoard is a Next.js application with a PostgreSQL database. We chose this stack for speed of development and because our team knows it deeply. The backend uses Next.js API routes with Prisma as the ORM. Authentication is handled by NextAuth with team-based access control — PMs see all projects, developers see their assigned projects, executives see aggregate dashboards.
Real-time updates use Server-Sent Events (SSE) rather than WebSockets. SSE is simpler to implement, works through most corporate proxies without configuration, reconnects automatically on network interruption, and is sufficient for our use case. The allocation board and financial dashboards subscribe to SSE streams that push updates when underlying data changes. We don't need bidirectional real-time communication — clients push data via standard HTTP requests, and the server pushes view updates via SSE.
Time tracking is built in — no external integration, no sync. The timer is a simple start/stop interface that records time entries against tasks. We considered integrating with Toggl or Harvest instead of building our own, but the tight integration with the capacity and financial pillars made a native implementation worthwhile. When you log time against a task, the capacity board, burn rate, and financial projections update within seconds. No sync delay, no data mapping, no reconciliation errors, no "the numbers in Harvest don't match Asana" conversations.
The Hardest Problem: Data Integrity Across Pillars
The three-pillar model creates interdependencies that are easy to design on a whiteboard and hard to maintain in production. When a task estimate changes, it affects capacity allocations and financial projections. When a team member goes on PTO, it affects capacity, which affects delivery timelines, which affects financial burn rates. When a project's billing model changes mid-engagement, the financial history needs recalculation.
We handle this with an event-driven internal architecture. Every mutation emits domain events that trigger downstream recalculations:
// Domain event types
type DomainEvent =
| { type: 'TASK_ESTIMATE_CHANGED'; taskId: string; projectId: string; assigneeId: string;
oldEstimate: number; newEstimate: number }
| { type: 'TIME_ENTRY_CREATED'; taskId: string; projectId: string; memberId: string;
hours: number; billable: boolean }
| { type: 'MEMBER_PTO_UPDATED'; memberId: string; dates: DateRange[] }
| { type: 'ALLOCATION_CHANGED'; memberId: string; projectId: string; weekStart: Date;
oldHours: number; newHours: number }
| { type: 'PROJECT_BUDGET_CHANGED'; projectId: string; field: string; value: any };
// Event handler: recalculate downstream when a task estimate changes
const handleEstimateChange = async (event: Extract) => {
// Recalculate capacity allocation for the assignee's current week
await recalculateAllocation(event.assigneeId, event.projectId);
// Recalculate project financial projections
await recalculateProjectFinancials(event.projectId);
// Check if allocation change pushes anyone over 100%
const allocation = await calculateWeeklyAllocation(event.assigneeId, currentWeekStart());
if (allocation.status === 'over') {
await createAlert({
type: 'overallocation',
memberId: event.assigneeId,
percentage: allocation.percentage,
});
}
// Push SSE updates to connected clients viewing affected data
await pushUpdate('allocation', { memberId: event.assigneeId });
await pushUpdate('financials', { projectId: event.projectId });
};
The cascade is deterministic: estimate change triggers allocation recalc triggers financial recalc triggers UI update. Each step is idempotent, so if something fails mid-cascade, we can safely retry from any point. We log every domain event, which gives us a complete audit trail of how the system state evolved over time — invaluable for answering questions like "when did this project go over budget and what caused it?"
What an Agency OS Gets You
Six months after FlowBoard replaced our tool patchwork, the operational improvements were measurable and significant:
- Zero double-bookings. The capacity board catches conflicts before they happen. PMs see overallocation warnings in real time instead of discovering conflicts after deadlines slip.
- 30% fewer over-budget projects. Real-time health scores surface problems weeks earlier than monthly financial reviews. When a project's health score drops below 0.85, the PM knows immediately and can intervene while course correction is still possible.
- 50% less time on status reporting. FlowBoard generates weekly project summaries automatically from delivery data and time entries. No more Monday morning standups that are really just reading from a spreadsheet. The standups now focus on decisions, not status.
- Faster sales decisions. When a new deal comes in, we can model capacity impact in 5 minutes instead of a day of spreadsheet wrestling. Draft allocations make it trivial to answer "what happens if we sign this client?"
- Better estimation over time. The estimation accuracy tracking has improved our median estimate accuracy from roughly 65% to 82% over six months. Better estimates mean better capacity planning, which means better financial projections, which means fewer surprises.
The cultural impact was equally significant. When everyone can see the capacity board, allocation decisions become transparent. "Why did Project X get two developers while Project Y only got one?" is answerable by looking at the board. This eliminated a surprising amount of internal friction that we didn't even realize we had.
Lessons for Building Internal Tools
FlowBoard reinforced principles we now apply to every internal tool we build, and to client projects that involve internal tooling:
- Start with the decision it needs to support. FlowBoard exists to answer "do we have capacity for this project?" and "is this project financially healthy?" Every feature either helps answer those questions or doesn't ship. This focus prevented scope creep into CRM features, invoicing, and other capabilities that would have diluted the core value.
- Build for the daily user, not the quarterly reviewer. PMs use FlowBoard every day. Executives look at it monthly. The UI is optimized for the daily workflow — quick time entry, at-a-glance allocation status, one-click drill-down. Reports and dashboards for leadership are secondary views generated from the same underlying data.
- Integrate or build — never sync. Two-way syncs between tools are maintenance nightmares that drift silently. FlowBoard has native time tracking specifically to avoid syncing with Harvest. If we'd built a sync, we'd still be debugging reconciliation issues six months later.
- Make the important data unavoidable. The capacity board is the homepage. The project health score is on every project card. The burn rate is next to every task list. If data is hidden behind navigation, it gets ignored. If it's in your face every day, it changes behavior.
- Events over computed views. Every state change flows through domain events. This makes the system auditable, debuggable, and extensible. Adding a new downstream effect (like sending a Slack notification when a project goes over budget) is trivial — just add a new event handler.
FlowBoard isn't a product we sell — not yet, anyway. It's an internal tool that makes Harbor run better. But the architecture — three interconnected pillars with event-driven consistency, real-time visibility, and decision-focused UI — is a pattern we've applied to client projects in logistics, healthcare, and financial services. If your organization runs on a patchwork of tools that don't talk to each other, the answer isn't a better integration layer. The answer is a purpose-built system that treats your core operating dimensions as a unified data model. That's what FlowBoard taught us, and it's the advice we give to every client who asks.