The problem: operational chaos beyond 10+ projects
On the surface, a web agency is a simple business model: proposal, contract, build, handover, payment. In reality, every active project is its own workflow, every client is its own communication thread, and every deadline is a legal obligation.
Beyond 10–15 active projects, traditional tools break down. Not because they're bad — but because they weren't built for this.
That's exactly what we experienced at Compass. The big picture was fine, but the details were getting harder and harder to manage:
⏱
Asset collection deadlines
Who has sent their materials, who hasn't, and how many days late are they? Someone had to check this by hand every morning.
💳
Payment statuses
Who has paid the deposit, who has an open final invoice, who is overdue? Pieced together from emails and spreadsheets.
⚖️
Legal escalations
The project freeze deadline is written into the contract. But who is watching for day 60?
📬
Client communication
A legally defensible communication trail can't be reconstructed from an inbox.
🔄
Revision rounds
Free or paid? Which round are we on? Who signed off on the change list?
All of this is manageable by hand for 5 projects. Beyond 15, it's 2–3 hours of admin every day — and that's just the tracking. Communication, payments, and legal protection come on top of it.
Why isn't a Kanban board enough?
Trello, Notion, ClickUp, Asana — all excellent task management tools. But task management and workflow orchestration solve different problems.
TASK MANAGEMENT
- Who's doing what?
- When will it be done?
- What's left?
WORKFLOW ORCHESTRATION
- What's the legal state?
- What's the next automated step?
- What's the contractual obligation?
- What can be proven in an audit?
- When should an email go out automatically?
In an agency, where every project is backed by a contract, deadlines, and legal obligations, a task management tool isn't enough. You need a workflow orchestration system.
Architecture decisions
We designed the system around three principles: auditability, automation, and scalability.
⚡
Event-Driven Architecture
Every business action writes an event to the project_events table. Status changes, sent emails, file uploads, recorded payments — all logged as audit events with actor, source, visibility, and metadata fields.
Why? The event log is the single source of truth. If a legal question ever comes up, we can replay exactly what happened, when, and who triggered it.
🎛
4 Independent Status Dimensions
project_phase (technical state), payment_status (payment state), legal_status (legal state), operational_status (operational state) — each one fully INDEPENDENT of the others.
Why? A project can be technically finished, legally frozen, and overdue on payment all at once. A single status field can't represent that.
🧩
Service Separation
Every domain lives in its own service: projectEventService, projectPaymentService, projectRevisionService, projectMaterialService, projectHandoverService, projectFileService.
Why? Page components hold no business logic. Each service has a single responsibility — so it stays testable, replaceable, and extensible.
🔄
Status Machine
Legal and operational statuses aren't free-form strings. Validated state transitions, terminal-state handling, and consistency checks run as part of the system.
Why? Invalid state combinations (e.g. a closed project with an open payment) are detected and fixed before they turn into a legal problem.
The 4 status dimensions and the automation engine
The automation engine runs once a day (6:00 AM, Vercel Cron) and evaluates every active project against 5 rules.
4 INDEPENDENT STATUS DIMENSIONS
Project Phase
project_phaseasset_request_sentawaiting_client_assetsin_developmentinternal_reviewfirst_handoverclient_testingrevision_roundmarked_completehanded_overclosed
Legal Status
legal_statusokclient_delayedpayment_overduework_suspendedfrozenin_collections
Payment Status
payment_statusdeposit_pendingdeposit_paidinstallment_duefinal_payment_pendingpaid_in_full
Operational Status
operational_statusactivepausedclosedarchived
5 AUTOMATION RULES
1
ruleMaterialDeadlineTrigger:Asset request overdue by 3+ days
Action:legal_status → client_delayed + reminder email
Frequency:Every 3 days, up to day 30
2
rulePaymentOverdueTrigger:Final invoice past due
Action:legal_status → payment_overdue + payment notice email
Frequency:On days 1, 3, 7, and 14
3
rulePriceIncreaseWarningTrigger:client_delayed + 30/45/55 days without progress
Action:Project freeze warning email
Frequency:On days 30, 45, and 55
4
ruleAutoFreezeTrigger:client_delayed + 60+ days without progress
Action:legal_status → frozen + operational_status → paused
Frequency:Once, on day 60
5
ruleAutoApprovalTrigger:client_testing phase + 3+ days with no response
Action:project_phase → marked_complete (contractual auto-acceptance)
Frequency:Once, on day 3
Engineering deep-dive
📋
Event Sourcing
- Every project_events record contains: event_type, title, actor, source, visibility, old_value, new_value, metadata
- The Communication tab only shows client-facing events (COMM_EVENT_TYPES whitelist)
- The Audit log tab shows every event — a complete legal trace
- Replayable state: the event log can reconstruct what happened and when, at any time
⏰
Deadline Engine
- buildDefaultDeadlines() — the canonical source: 3/30/60-day rules
- calcOverdue() — calculates how many days each deadline is overdue
- syncDeadlineOverdueStatus() — daily sync that updates is_overdue and overdue_days
- Every deadline type has an auto_action_type field — the next automated step
🔒
Cron & Automation Deduplication
- wasActionDoneToday() — the same action runs at most once a day (dedup)
- cron_executions table — every run is logged: lock mechanism, duration, error count
- Duplicate run protection: 409 Conflict if a run is already active
- Stale lock cleanup: after a 10-minute timeout, a stale lock is marked timed_out
📧
Email Orchestration
- Email isn't a notification. It's a legal communication layer.
- Every email is logged against a project_id and lands in the audit trail
- email_events table: template, recipient, status, retry_count, resend_id
- sendAutomationEmail() error handling: an email_send_failed project_event on failure
Technical hardening: production-grade stabilization
A workflow orchestration system isn't production-grade because of its core features — it's production-grade because of how it behaves in edge cases, under failure, and over the long run.
Error Handling Layer
- Every automation rule runs inside try/catch — failures write an automation_failed project_event
- Email send failure: an email_send_failed project_event + incremented retry_count
- logAutomationFailedEvent() and logEmailSendFailedEvent() — typed wrappers
Reconciliation Engine
- 6 impossible-state rules that detect consistency errors
- Auto-fix: frozen + active → automatically set to paused
- Runs on the daily cron; every issue writes a state_reconciliation_executed event
Monitoring & Observability
- Ops Monitoring Dashboard: heartbeat, health scores, failed automations, stuck projects
- cron_reliability_7d %, email_delivery_rate % — system health metrics
- Dead-letter queue: emails that still didn't go out after 3 retries
Data Lifecycle
- cron_executions: 90-day retention — older records are deleted
- Dismissed notifications: 30-day retention
- system_errors: 60-day retention
- project_events and email_events are NEVER deleted — for audit purposes
Key learnings
„The website isn't the product. The operating system is the product."An agency's value isn't in the deliverable. It's in the system it uses to produce it.
„Email isn't a notification layer. It's a legal communication layer."Every reminder we send is a provable legal step. If it isn't recorded, it didn't happen.
„No audit trail, no contractual evidence."In a client dispute, the agency that wins is the one that can prove what it sent, when, and who received it.
„Deadline management isn't task management. It's a legal obligation."Contractual deadlines are legal obligations — not to-do items. The system needs to know that too.
„Traditional tools track tasks. We needed to orchestrate workflows."The system is built around the business's complexity — the business doesn't bend to the limits of a tool.
ARCHITECTURE SUMMARY
Event-Driven ArchitectureWorkflow OrchestrationContract AutomationStatus MachineAudit TrailDeadline Engine