11 — Tech Stack & Rationale

Project: crmoffice Last updated: 2026-05-22

Every choice rationalized. Trade-offs documented. No vendor lock-in beyond essential.

Note: Stack was upgraded mid-Phase-1 from the initially planned Laravel 11 + Filament 3 to the current Laravel 13.7 + Filament 5. Reason: both received major releases during planning phase, and starting on the newest LTS avoided a near-term migration. Older docs still mention Laravel 11; this table is the source of truth.


1. Backend

Language & Framework

Choice Why
PHP 8.3+ Long-tested for CRM/billing; broad hosting compat; PHP 8.3 has typed class constants, JIT, perf wins
Laravel 13.7 Latest LTS at install time (2026-05); upgraded from initial Laravel 11 plan to ride newest queue/Pennant/scheduler improvements

Alternatives considered:

  • ❌ Symfony — more verbose, smaller talent pool for SMB
  • ❌ Hyperf / Swoole — async perf gains not needed at MVP scale; complicates Filament integration
  • ❌ Django / Rails / NestJS — global stack lock per user (CLAUDE.md)

Database

Choice Why
MySQL 8.0+ Stack lock; mature, well-supported; JSON columns + generated indexes cover custom-field flex; replication well-understood; broad managed-DB support

Alternatives considered:

  • ❌ PostgreSQL — stack lock; for a CRM, MySQL features (incl. JSON, full-text via Meilisearch) suffice. No need for advanced types.
  • ❌ MariaDB — fork divergence risks; stick with upstream MySQL
  • ❌ SQLite — single-server only, no concurrent writes at scale

Cache / Queue / Session / Lock

Choice Why
Redis 7+ Single dependency covers 4 needs; Horizon requires it; pub/sub for future real-time

Queue Manager

Choice Why
Laravel Horizon Native; per-queue worker pool config; dashboard; metrics; standard for Laravel
Choice Why
Meilisearch Self-hostable; sub-100ms; typo-tolerant; faceted filters; small footprint; Laravel Scout driver mature

Alternatives considered:

  • ❌ Algolia — paid, vendor lock-in
  • ❌ Typesense — comparable, slightly less PHP ecosystem support
  • ❌ MySQL FULLTEXT — adequate basic; misses typo tolerance, faceting; harder to scale
  • ❌ Elasticsearch — overkill, ops-heavy for SMB CRM

PDF Rendering

Choice Why
dompdf (default) Pure PHP, no system deps; fast enough for invoices/proposals
Browsershot (Puppeteer) Optional opt-in via setting; needed for complex layouts (Phase 3+)

Authentication

Choice Why
Laravel Sanctum Lightweight; supports both SPA cookies and personal access tokens (one stack for Inertia portal + future Flutter API)
pragmarx/google2fa-laravel TOTP standard; well-maintained

Alternatives considered:

  • ❌ Passport (OAuth2) — overkill; we don't need full OAuth2 for first-party clients
  • ❌ Breeze — too minimal; we need API + Web both

RBAC

Choice Why
spatie/laravel-permission De-facto standard; cache-friendly; role + direct permission both supported

File Handling

Choice Why
Custom files table + S3CompatibleAdapter Lightweight; polymorphic by design; integrates with Sanctum signed URLs
(NOT spatie/medialibrary for primary file storage) Too opinionated for our polymorphic single-table approach; might add later as opt-in for image conversions

Reconsider: actually use spatie/laravel-medialibrary if image conversions (avatar thumbnails, etc.) are needed. For Phase 1 default: custom files table. Add Spatie MediaLibrary if image variants become a need.

Other Composer Packages

Package Purpose
laravel/sanctum API auth
laravel/horizon Queue manager
laravel/scout + meilisearch/meilisearch-php Search
spatie/laravel-permission RBAC
pragmarx/google2fa-laravel TOTP 2FA
barryvdh/laravel-dompdf Invoice/proposal/contract PDF
spatie/laravel-html Email HTML helpers
intervention/image Image manipulation (avatars)
league/flysystem-aws-s3-v3 S3 driver
phpoffice/phpspreadsheet CSV/XLSX import-export
phpmailer/phpmailer Already in Laravel — for advanced SMTP edge cases
webklex/php-imap IMAP poll for inbound email
spatie/laravel-query-builder API filter/sort/include conventions
dedoc/scramble (or darkaonline/l5-swagger) OpenAPI doc generation

2. Admin UI

Choice: Filament 5

Why:

  • Built on Livewire + Alpine + Tailwind 4 — Laravel-native
  • Resource pattern is exactly what CRM admin needs (CRUD-heavy)
  • Filament 5's new Schemas + Tables architecture (app/Filament/.../Schemas/*.php, Tables/*.php) keeps Resources clean
  • Custom Pages support Kanban, Gantt, Reports without fighting the framework
  • Theming via viteTheme(), branding, permissions integration mature
  • Active development, large community
  • Accessibility baseline good

Trade-offs accepted:

  • Livewire roundtrips for some interactions; mitigated by Alpine for instant UX
  • Less custom design freedom than a pure SPA — acceptable for admin

Alternatives considered:

  • ❌ Backpack — solid but smaller community, less momentum
  • ❌ Nova — paid, less community
  • ❌ Custom-built admin in Inertia — months of build, no value-add for CRM admin

3. Customer Portal & Marketing

Customer Portal — Inertia + Vue 3 + Tailwind

Per stack-lock skill recommendation (balanced, modern, interactive UX). Reasons:

  • Customer portal benefits from snappy interactivity (invoice browsing, ticket conversation thread)
  • Single SPA-like feel without separate API ceremony
  • Tailwind matches Filament for visual consistency
  • Vue 3 Composition API → reusable composables (useInvoices, useTickets)

Marketing Site / pSEO — Blade + Tailwind (no Inertia)

Decision: Override the original "Inertia for everything public" plan. pSEO pages have to be SEO-perfect — best served as server-rendered Blade with minimal JS.

Why:

  • Sub-200ms TTFB matters for SEO
  • No hydration cost, content fully visible to crawlers without JS parsing
  • Alpine.js for tiny interactivity (accordion, tabs, FAQ toggle)
  • Simpler caching (cache full HTML in Redis)

Trade-off: Two frontend mental models in same repo (Vue for portal, Blade for marketing). Acceptable because they're cleanly separated by route group.

Vue ecosystem packages

Package Purpose
@inertiajs/vue3 Inertia client
tailwindcss + @tailwindcss/forms, @tailwindcss/typography Styling
@headlessui/vue Accessible primitives (modal, listbox)
@heroicons/vue Icons
vue-i18n i18n
axios HTTP (Inertia-bundled)
dayjs Date utils
chart.js (or apexcharts-vue) Customer portal charts

4. Asset Pipeline

Choice Why
Vite Laravel-native; fast HMR; native ESM
Tailwind via PostCSS Standard
Production: minified + gzip/brotli, content-hashed Standard

5. Infrastructure / Deployment

Server stack

Choice Why
Ubuntu 22.04 LTS or 24.04 LTS Stable, supported, broad guides
Nginx Standard reverse proxy + static
PHP-FPM 8.3 Standard
MySQL 8 Managed (RDS, DigitalOcean, Aiven) preferred for prod
Redis 7 Managed preferred (Upstash, ElastiCache, DigitalOcean)
Meilisearch Self-host (small VM); or Meilisearch Cloud opt-in
Supervisor Horizon workers
Cron Laravel scheduler (* * * * * php artisan schedule:run)

Deploy Targets (Supported, Documented)

Target Notes
Laravel Forge Recommended; turnkey; docs in docs/deploy/forge.md (Phase 8)
Ploi Alternative to Forge; same pattern
Docker Compose Self-hosted; docker-compose.yml at repo root (Phase 8)
Coolify Self-hosted PaaS; documented
Kubernetes Not officially supported MVP; community

Object Storage

Choice Why
S3-compatible via single adapter Per "no hardcoded providers" rule. Cloudflare R2 / Wasabi / AWS S3 / DO Spaces / Backblaze B2 all work
Local disk fallback For single-server installs

Email (Transactional)

Choice Why
Provider-agnostic via SmtpAdapter or RestApiAdapter Per "no hardcoded providers" rule. Owner picks Mailgun / Postmark / SES / SendGrid / Resend / self-hosted Postfix

CDN

Choice Why
Cloudflare (recommended, not required) Free tier sufficient; cache pSEO HTML at edge

TLS

Choice Why
Let's Encrypt via certbot (or Forge auto) Free, automated renewal

6. Observability

Concern Tool
Errors Sentry (or compat: GlitchTip, Bugsnag) — env-var configurable, no lock-in
Logs Laravel daily channel; optional ship to Loki / CloudWatch / Papertrail via syslog driver
Metrics Horizon dashboard (queues); custom Filament dashboard (KPIs); optional Prometheus via exporter (Phase 9)
Uptime UptimeRobot / BetterStack — owner sets up
APM Telescope local; Sentry Performance prod (optional)

7. Development Tooling

Tool Purpose
Pest 3 Tests (preferred over PHPUnit for syntax)
PHPStan / Larastan Static analysis level 6 minimum
Pint Code formatting (Laravel-native)
Rector Automated upgrades (Phase 8+)
Laravel Pail Live log tail
Laravel Boost Debug helpers (dev only)
TablePlus / Sequel Ace DB GUI (developer choice)
Postman / Bruno / Hoppscotch API testing
Lighthouse CI Frontend perf gates

Testing

Layer Approach
Unit Pest, focused on Domain services & Actions
Feature HTTP tests for controllers, Filament resources
Browser Pest 3 Browser tests or Dusk for critical flows (login, invoice → pay)
API contract Schema validation against OpenAPI spec
Performance k6 / Artillery against staging (Phase 8)

8. Build / CI

Stage Tool / Action
Lint Pint check (auto-fix in pre-commit)
Static analysis PHPStan level 6
Tests Pest with parallel; coverage report
Browser tests Pest 3 browser (in CI on Phase 5+)
Frontend build npm run build
Deploy Forge auto-deploy on main push (or manual approve for prod)

Pipeline file: .github/workflows/ci.yml.


9. Security Posture

Concern Implementation
OWASP Top 10 Laravel defaults handle most (CSRF, SQLi via Eloquent, XSS via Blade auto-escape, etc.)
Password hashing Bcrypt cost 12, or Argon2id if user opts
Session timeout Configurable; 1 day default for admin; 30 days for portal
Rate limiting Per API design §1.8
Provider credentials Encrypted at rest (Laravel encrypted cast)
Webhook signature HMAC-SHA256 per outbound; verified per inbound per provider format
2FA TOTP optional/mandatory per role
Audit log Append-only, mandatory triggers
SSRF Provider URL validator
File upload MIME validation, size limits, optional ClamAV scan
CSRF Laravel default for web; not for API (token-based)

10. Mobile (Phase 2 / Deferred)

Choice Why
Flutter Stack lock per global rule (cross-platform iOS+Android one codebase)
Folder D:\project flutter\crmoffice\ (separate repo or monorepo subfolder — TBD with user)
State Riverpod (modern, simple, testable)
HTTP Dio + Retrofit-like generator
Offline cache Drift (SQLite) or Isar
Auth Sanctum personal access token via secure storage
Push FCM (Firebase Cloud Messaging) via provider-agnostic adapter where possible

11. Stack Decision Summary Table

Layer Choice Locked by
Lang PHP 8.3+ Stack-lock
Framework Laravel 11 Stack-lock
DB MySQL 8 Stack-lock
Admin UI Filament 3 Recommended in stack-lock
Customer Portal Inertia + Vue 3 + Tailwind User chose in initial scoping
Marketing/pSEO Blade + Tailwind + Alpine Architecture override for SEO perf
Search Meilisearch Stack-lock recommended
Queue Horizon Stack-lock recommended
Cache Redis Stack-lock recommended
Auth Sanctum Standard
RBAC spatie/laravel-permission Standard
PDF dompdf default, Browsershot opt-in Pragmatic
Mobile Flutter (Phase 2) Stack-lock
Object storage S3-compatible (dynamic) Global rule
Email SMTP / REST (dynamic) Global rule
Payment Format-based adapter (dynamic) Global rule
LLM Format-based adapter (dynamic) Global rule

12. What We're NOT Using (Explicit)

Tech Reason
Next.js / Nuxt as standalone SPA Stack-lock; portal nempel ke Laravel via Inertia
React Native Stack-lock — Flutter only
GraphQL Adds complexity; REST + Inertia + Scout suffice
WebSocket real-time (Laravel Reverb) at MVP Polling notifications adequate; opt-in Phase 7
Event sourcing Audit log suffices
CQRS Over-engineering for CRM scale
Microservices Modular monolith is correct shape
Kubernetes Forge/Ploi cover deploy; K8s for >Phase 9 enterprise
Vendor SDKs for Payment/Mail/SMS Replaced by format-based HTTP adapters
ionCube / encrypted source Open / readable source, no obfuscation

13. Versions Locked

Component Min Version Pin Reason
PHP 8.3 Typed class constants, JIT improvements
Laravel 11.x Latest LTS-spirit release
MySQL 8.0 JSON indexing via generated columns
Redis 7 Stream support for future
Node 20 LTS Vite & build chain
Filament 3.x Per latest stable
Vue 3.4+ Composition API stable
Tailwind 3.x (4 when stable adoption)
Meilisearch 1.6+ Pagination total field