03 — System Architecture
Project: crmoffice Last updated: 2026-05-30
1. Architecture Style
Modular monolith dengan layered architecture dan domain-oriented namespacing. Bukan microservices (over-engineering untuk MVP CRM). Bukan plain MVC tipis (terlalu thin untuk domain selevel ini).
Technology Stack: Laravel 13.7 + Filament 5 + Tailwind 4 + Vue 3 + Inertia 2
Prinsip:
- Controllers tipis — hanya HTTP plumbing
- Domain logic di Service / Action classes
- Persistence via Eloquent (tidak perlu repository abstraction sampai ada motivasi spesifik)
- Domain events untuk cross-module communication (Filament panel ↔ Customer portal ↔ API)
- Background work via queued Jobs (Horizon)
- Public boundary (controllers, console, jobs, events) di top, infra (cache, queue, DB, storage) di bottom
2. High-Level System Diagram
flowchart TB
subgraph CLIENT [Clients]
BR[Browser - Admin]
BR2[Browser - Customer Portal]
BR3[Browser - Public Marketing]
FL[Flutter App - Phase 2]
WH[3rd-party Webhooks / Email pipe]
end
subgraph EDGE [Edge]
CDN[CDN / Cloudflare]
REV[Nginx]
end
subgraph APP [Application]
FIL[Filament Admin /admin]
POR[Blade Customer Portal /portal]
PUB[Public Controllers / pSEO /]
API[Sanctum API /api]
CON[Console / Schedule]
HOR[Queue Workers / Horizon]
end
subgraph DOMAIN [Domain Services]
BIL[Billing Service]
SCH[Scheduling Service]
NOT[Notification Service]
PSO[pSEO Generator]
ADP[Provider Adapters]
PDF[PDF Renderer]
IMP[Email Importer]
end
subgraph DATA [Data Layer]
MYSQL[(MySQL 8)]
RED[(Redis - cache & queue & session)]
MEI[(Meilisearch)]
S3[(S3-compat storage)]
end
subgraph EXT [External]
PAY[Payment Gateway]
MAIL[Mail Provider]
SMS[SMS Provider]
LLM[LLM Provider]
end
BR --> CDN
BR2 --> CDN
BR3 --> CDN
FL --> CDN
CDN --> REV
WH --> REV
REV --> FIL
REV --> POR
REV --> PUB
REV --> API
CON --> HOR
FIL --> DOMAIN
POR --> DOMAIN
PUB --> DOMAIN
API --> DOMAIN
HOR --> DOMAIN
DOMAIN --> MYSQL
DOMAIN --> RED
DOMAIN --> MEI
DOMAIN --> S3
ADP --> EXT
3. Layered Architecture
┌──────────────────────────────────────────────────────────┐
│ Presentation │
│ Filament Resources · Inertia Controllers · API │
│ Controllers · Blade views · Vue pages · Form Requests │
├──────────────────────────────────────────────────────────┤
│ Application │
│ Actions (single-purpose use cases) · DTOs · Policies │
│ Domain Events · Listeners · Notifications │
├──────────────────────────────────────────────────────────┤
│ Domain │
│ Models (Eloquent) · Value Objects · Domain Services │
│ Domain Events │
├──────────────────────────────────────────────────────────┤
│ Infrastructure │
│ Provider Adapters (Payment / Mail / SMS / Storage / │
│ LLM) · Search index sync · PDF renderer · Email pipe │
│ parser · Webhook dispatcher · Job queue │
└──────────────────────────────────────────────────────────┘
Rules:
- Presentation can call Application & Domain (read-only models OK), never Infrastructure directly
- Application orchestrates Domain + Infrastructure
- Domain knows nothing of HTTP, queue, or external services
- Infrastructure implements ports declared by Application
4. Request Lifecycle (Example: Mark Invoice Paid via Gateway Callback)
sequenceDiagram
participant GW as Payment Gateway
participant NX as Nginx
participant CTRL as Webhook Controller
participant ACT as ProcessPaymentCallback Action
participant ADP as Payment Adapter
participant INV as Invoice Model
participant EVT as Event Dispatcher
participant JOB as Notify Job
GW->>NX: POST /webhooks/payment/{provider}
NX->>CTRL: route
CTRL->>CTRL: validate signature
CTRL->>ACT: handle(payload)
ACT->>ADP: parse(payload)
ADP-->>ACT: ParsedPayment DTO
ACT->>INV: applyPayment()
INV->>EVT: InvoicePaid event
EVT->>JOB: queue SendInvoicePaidNotification
ACT-->>CTRL: 200 OK
CTRL-->>NX: response
NX-->>GW: 200
JOB->>JOB: email + portal notif (async)
5. Folder Structure (Laravel)
crmoffice/
├── app/
│ ├── Console/
│ │ ├── Commands/ # Artisan commands (import, recurring, dunning)
│ │ └── Kernel.php # Schedule
│ ├── Filament/
│ │ ├── Resources/ # 35 resources in 9 navigation groups (Master Data, Penjualan, Finance, Operasional, Security, Laporan, Marketing, Integrasi, Sistem)
│ │ ├── Pages/ # Custom pages: Dashboard, 6 Report pages (Sales, Leads, Project Profitability, Time Tracking, Tickets, Expense), Task Gantt Chart
│ │ ├── Widgets/ # 7 dashboard widgets with per-role visibility (StatsOverview, RevenueChartWidget, RecentLeadsTable, PendingInvoicesTable, MyTasksTable, SupportQueueTable, AccountWidget)
│ │ └── Clusters/ # Grouped resources (Sales cluster, etc.)
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Portal/ # Blade controllers — customer portal (invoices, projects, tickets)
│ │ │ ├── Public/ # Marketing + pSEO + public proposal/invoice
│ │ │ ├── Api/V1/ # Sanctum API
│ │ │ └── Webhook/ # Inbound webhooks (payment, email)
│ │ ├── Middleware/
│ │ ├── Requests/ # FormRequests for validation
│ │ └── Resources/ # API resources (transformers)
│ ├── Models/ # 56 Eloquent models
│ ├── Domain/
│ │ ├── Crm/ # Clients, Leads, Contacts services
│ │ ├── Sales/ # Estimate, Invoice, Payment services
│ │ ├── Projects/ # Project, Task, Time services
│ │ ├── Support/ # Ticket, KB services
│ │ ├── Platform/ # Custom fields, audit, notification
│ │ └── Pseo/ # Programmatic SEO generators
│ ├── Actions/ # Single-purpose use case classes
│ │ ├── Crm/ConvertLeadToClient.php
│ │ ├── Sales/ConvertEstimateToInvoice.php
│ │ ├── Sales/GenerateRecurringInvoices.php
│ │ ├── Sales/ApplyPaymentToInvoice.php
│ │ ├── Projects/InvoiceTimeEntries.php
│ │ ├── Support/AssignTicketViaSlaPolicy.php
│ │ └── Support/PipeEmailToTicket.php
│ ├── Adapters/ # Infrastructure adapters
│ │ ├── Payment/
│ │ │ ├── PaymentAdapterContract.php
│ │ │ ├── RedirectFlowAdapter.php
│ │ │ ├── EmbedFlowAdapter.php
│ │ │ └── QrFlowAdapter.php
│ │ ├── Mail/
│ │ │ ├── SmtpAdapter.php
│ │ │ └── RestApiAdapter.php
│ │ ├── Sms/
│ │ │ ├── RestSmsAdapter.php
│ │ │ └── SmppSmsAdapter.php
│ │ ├── Storage/
│ │ │ └── S3CompatibleAdapter.php
│ │ └── Llm/
│ │ ├── OpenAICompatibleAdapter.php
│ │ ├── AnthropicFormatAdapter.php
│ │ └── GeminiFormatAdapter.php
│ ├── Events/
│ ├── Listeners/
│ ├── Jobs/
│ ├── Notifications/
│ ├── Policies/
│ ├── Providers/
│ └── Support/ # Helpers, traits, value objects
├── bootstrap/
├── config/
│ ├── crmoffice.php # App-specific config
│ ├── pseo.php # pSEO route catalog
│ └── ...
├── database/
│ ├── factories/
│ ├── migrations/ # 63 migration files
│ └── seeders/
├── lang/
│ ├── en/
│ └── id/
├── resources/
│ ├── css/
│ │ └── filament/
│ │ └── admin/
│ │ └── theme.css # Custom premium Filament theme (responsive, dark mode, glass topbar, gradient primary)
│ ├── js/
│ │ ├── Pages/ # Vue 3 Inertia pages (public marketing + pSEO)
│ │ │ └── Public/ # Marketing + pSEO pages
│ │ ├── Components/ # Shared Vue components
│ │ ├── Layouts/
│ │ └── app.js
│ └── views/
│ ├── emails/ # Mailables
│ ├── pdf/ # PDF templates (invoice, proposal, contract)
│ ├── portal/ # Customer portal Blade views
│ │ ├── invoices/
│ │ ├── projects/
│ │ └── tickets/
│ └── filament/ # Filament view overrides
├── routes/
│ ├── web.php # Public + redirect to /admin or /portal
│ ├── portal.php # Customer portal (Blade)
│ ├── public.php # Marketing + pSEO + public links
│ ├── api.php # Sanctum API
│ ├── webhooks.php # Inbound webhooks
│ └── console.php
├── storage/
│ └── app/
│ ├── public/
│ ├── invoices/ # Generated PDFs
│ ├── proposals/
│ └── provider-presets/ # JSON templates (autofill convenience)
├── tests/
│ ├── Feature/
│ ├── Unit/
│ └── Browser/ # Pest/Dusk
└── composer.json
6. Service Boundaries
| Layer | Calls Into | Cannot Call |
|---|---|---|
| Filament Resource | Action, Service, Model | Adapter directly, Job::dispatch (use Action) |
| Inertia Controller | Action, Service, Model | Adapter directly |
| API Controller | Action, Service, Model (read-only via Resource transformer) | Other controllers, Filament |
| Action | Service, Model, Event dispatcher | HTTP request, Adapter directly (via Service) |
| Service | Model, Adapter (via interface), Repository (if any) | HTTP request, Console |
| Adapter | External HTTP, Config | Model (decouple from domain) |
7. Background Jobs
| Job | Queue | Trigger | Frequency |
|---|---|---|---|
GenerateRecurringInvoices |
default |
Schedule | daily 00:30 |
SendInvoiceDueReminder |
notifications |
Schedule | daily 08:00 |
SendInvoiceOverdueNotice |
notifications |
Schedule | daily 09:00 |
RenderInvoicePdf |
pdf |
Event InvoiceCreated / InvoiceUpdated |
async |
SendInvoiceEmail |
email |
Action SendInvoice |
async |
PollEmailInboxForTicketReplies |
support |
Schedule | every 2 min |
DispatchWebhookDelivery |
webhooks |
Event subscriber | async + retry (5 attempts exponential) |
RebuildSearchIndex |
search |
Model event observer | async batched |
RunSlaCheck |
support |
Schedule | every 1 min |
CleanupOrphanFiles |
default |
Schedule | weekly Sunday 03:00 |
RebuildPseoSitemap |
default |
Schedule | hourly |
SendDailyDigestEmail |
notifications |
Schedule | daily 07:00 |
Worker pool config (config/horizon.php):
production:
supervisor-default: { queues: [default], processes: 3 }
supervisor-email: { queues: [email], processes: 5 }
supervisor-pdf: { queues: [pdf], processes: 2 }
supervisor-search: { queues: [search], processes: 2 }
supervisor-support: { queues: [support], processes: 2 }
supervisor-notif: { queues: [notifications], processes: 3 }
supervisor-webhook: { queues: [webhooks], processes: 2 }
8. Caching Strategy
| Cached | Driver | TTL | Invalidation |
|---|---|---|---|
| Settings (KV table) | Redis | forever | on settings save |
| Currency rates | Redis | 6h | scheduled refresh |
| Tax rates | Redis | forever | on edit |
| User permissions | Redis | 1h | on role/permission change |
| Filament navigation badges | Redis | 60s | natural expiry |
| pSEO page HTML (full-page cache) | Redis | 24h | on data change → cache tag invalidate |
| Sitemap.xml | file | 1h | scheduled rebuild |
| Public KB pages | Redis | 1h | on article publish |
9. Search Strategy
Meilisearch indexes (per index name):
| Index | Searchable Attributes | Filterable | Sortable |
|---|---|---|---|
clients |
company_name, contacts.email | account_manager_id, status | created_at |
leads |
name, company, email, phone | lead_status_id, assigned_to, lead_source_id | last_activity_at |
invoices |
number, client_name | status, due_date | invoice_date |
projects |
name, description | status, client_id, project_manager_id | deadline |
tasks |
title, description | status, priority, assignee_id, project_id | due_date |
tickets |
number, subject, body | status_id, department_id, assigned_to | created_at |
kb_articles |
title, content | category_id, is_published | published_at |
Models implement Scout Searchable trait. Index sync via queued job.
10. File Storage
- Default disk:
local(storage/app/private) untuk single-server install - Recommended production: S3-compatible (config-driven; no provider name in code)
- All uploads via
App\Adapters\Storage\S3CompatibleAdapter(which falls through to LaravelStoragefacade) - File metadata di table
files(single table polymorphic:attachable_type/id) - Signed temp URLs untuk private files (24h default)
- ClamAV scan opsional via queued job (config flag)
11. Session & Auth
- Session driver: Redis
- Web auth: standard Laravel auth (Filament + Inertia share guard
web) - Customer portal: separate guard
portal(tablecontacts, password hash field) - API: Sanctum personal access token untuk Flutter, Sanctum SPA mode untuk Inertia portal AJAX
- 2FA: TOTP via
pragmarx/google2fa-laravel— optional staff, mandatory owner
12. Multi-Database / Read Replica
config/database.phphasmysql(write) andmysql_read(read)- Heavy read queries (reports, dashboards) explicitly use
DB::connection('mysql_read') - Default connection writes through master
- Replica lag tolerance: 1s typical
13. Multi-Tenancy (Deferred, Phase 3)
Tidak di-build di MVP. Untuk Phase 3, opsi:
stancl/tenancydengan database-per-tenant (strong isolation, more ops cost)- Row-level tenant_id + global scope (cheaper ops, weaker isolation)
Decision deferred. MVP single-tenant, tapi semua FK pattern tidak menghalangi future multi-tenant.
14. Observability
- Logs: Laravel
dailychannel; production opt-in untuk JSON output + ship ke Loki/CloudWatch - Errors: Sentry (or compatible) — user pasang sendiri via
SENTRY_DSN - Performance: Laravel Telescope di local + staging; off di prod (or restricted)
- Health endpoint:
GET /healthz— checks DB, Redis, Meilisearch, queue lag - Metrics: Horizon dashboard untuk queue health; Filament widget untuk app KPIs
15. Deployment Targets (Documented, not built)
| Target | Notes |
|---|---|
| Laravel Forge | PHP 8.3, Nginx, MySQL 8, Redis, Horizon supervisord, scheduler cron |
| Ploi | Sama dengan Forge |
| Self-hosted Ubuntu 22.04 | docs/deploy/ubuntu.md (write in Phase 5) |
| Docker Compose | docker-compose.yml di repo root (Phase 5) — app + mysql + redis + meilisearch |
16. Anti-Patterns (Explicitly Avoided)
- ❌ Fat controllers — semua >10 LOC business logic ke Action/Service
- ❌ Hardcoded provider names di code (
MidtransAdapter,StripeAdapter) - ❌ Hardcoded credentials atau URL → semua via Provider model
- ❌ N+1 queries di Filament tables — selalu eager-load
- ❌ Sync external API calls dari HTTP request — semua via queue
- ❌ Plain text password storage anywhere
- ❌ Comments yang menjelaskan WHAT (cuma yang non-obvious WHY)
- ❌ Future-proofing yang tidak dibutuhkan MVP