Files
L-Ami-Fiduciaire/_bmad-output/planning-artifacts/architecture.md
Saad Ibn-Ezzoubayr 35545c2a8f feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12,
Vue 3, Inertia.js 2, and Tailwind CSS 4.

Story 0.1 (rename folders to declarations in database) is implemented and
code-reviewed: migration, rollback, and 6 Pest tests all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:33:10 +00:00

69 KiB

stepsCompleted, lastStep, status, completedAt, inputDocuments, workflowType, project_name, user_name, date
stepsCompleted lastStep status completedAt inputDocuments workflowType project_name user_name date
1
2
3
4
5
6
7
8
8 complete 2026-03-11
_bmad-output/planning-artifacts/product-brief-l-ami-fiduciaire-2026-03-10.md
_bmad-output/planning-artifacts/prd.md
_bmad-output/planning-artifacts/prd-validation-report.md
_bmad-output/planning-artifacts/ux-design-specification.md
_bmad-output/planning-artifacts/research/market-fiduciary-saas-morocco-research-2026-03-10.md
_bmad-output/planning-artifacts/research/domain-moroccan-fiduciary-operations-research-2026-03-10.md
_bmad-output/planning-artifacts/research/ecosystem-partners-morocco-fiduciary-research-2026-03-10.md
_bmad-output/planning-artifacts/research/cloud-adoption-saas-trends-future-outlook-research-2026-03-11.md
_bmad-output/planning-artifacts/research/domain-moroccan-tax-regulation-digital-compliance-research-2026-03-10.md
_bmad-output/project-context.md
docs/index.md
docs/project-overview.md
docs/architecture.md
docs/development-guide.md
docs/source-tree-analysis.md
architecture l'ami fiduciaire Saad 2026-03-11

Architecture Decision Document

This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together.

Project Context Analysis

Requirements Overview

Functional Requirements (58 FRs across 10 groups):

The 58 FRs decompose into three architectural tiers:

Tier FR Groups Architectural Impact
Foundation Workspace & Onboarding (FR1-FR6), Team & Role Management (FR7-FR11), Client Management (FR12-FR15) Multi-tenant data model, RBAC engine, session-based workspace resolution
Core Domain Declaration Lifecycle (FR16-FR23), Dashboard & Visibility (FR24-FR28), Collaboration & Notifications (FR29-FR33), Client Portal & Document Exchange (FR34-FR40) Declaration state machine, role-scoped queries, notification service, token-based portal
Power Features Search/Filtering/Navigation (FR41-FR44), Archive System (FR45-FR51), Activity & Audit (FR52-FR55), Platform Administration (FR56-FR58) Full-text search, archive storage strategy, audit trail, admin dashboard

The Foundation tier is partially built (multi-tenant workspaces, client CRUD, basic folder system). Core Domain and Power Features are the MVP build scope.

Non-Functional Requirements (28 NFRs across 4 categories):

Category Key NFRs Architectural Driver
Performance (NFR1-6) 2s page loads, 10s bulk operations (50 items), 1s search, 3s dashboard render for 200 clients Server-side rendering optimization, query performance, pagination strategy
Security (NFR7-15) TLS 1.2+, AES-256 at rest, tenant isolation at every query, token security, 2FA, CNDP compliance, EU hosting Encryption layer, middleware architecture, hosting infrastructure
Scalability (NFR16-20) 200 concurrent workspaces, 1,000 users, 500 clients/workspace, 5,000 declarations/workspace, 1TB storage, 2-3x peak handling Database indexing, query optimization, storage architecture, caching
Reliability (NFR21-28) 99.5% uptime, 1hr RPO, 1hr RTO, >99% email delivery, zero data loss tolerance Backup strategy, email service selection, monitoring, queue reliability

UX-Driven Architectural Requirements:

  • Role-driven sidebar and dashboard content (4 internal roles + client portal = 5 distinct view contexts)
  • Inline table actions without page navigation (requires component-level interaction patterns)
  • Filter persistence across views (session/URL state management)
  • Bulk operations with real-time feedback (progress indication for 50+ item operations)
  • Summary cards that filter tables below (client-side or server-side filter linkage)
  • Server-side pagination as Inertia.js constraint (no infinite scroll)
  • Mobile-first client portal alongside desktop-first internal app

Scale & Complexity

  • Primary domain: Full-stack web (Laravel 12 + Vue 3 monolith via Inertia.js 2)
  • Complexity level: High
  • Estimated architectural components: ~15-20 major components (auth, RBAC, tenant isolation, declaration engine, notification service, document storage, client portal, dashboard aggregation, archive system, search/filter, audit logging, admin panel, email delivery, file preview, bulk operations)

Technical Constraints & Dependencies

Brownfield Constraints (non-negotiable):

Constraint Impact
Laravel 12 + PHP 8.2+ Backend framework, ORM, routing, middleware patterns locked in
Vue 3 + TypeScript strict Frontend framework, component patterns, type system
Inertia.js 2.0 No REST API layer -- controllers render pages directly. Server-driven SPA. All URLs from PHP controllers.
shadcn-vue (reka-ui) + Tailwind CSS 4 UI component library, styling approach. Cannot modify components/ui/*
MySQL 8.4 Database engine. Session-based tenant resolution via current_workspace_id
Spatie Media Library File upload/download infrastructure
Spatie Activity Log Audit trail infrastructure
Laravel Fortify Authentication + 2FA
bensampo/laravel-enum Enum pattern for business domain values
Laravel Wayfinder Type-safe frontend route generation
Pest 4 Testing framework (PHP-only, no frontend JS tests)
Docker/Sail + GitHub Actions Dev environment + CI/CD

Regulatory Constraints:

Regulation Requirement
CNDP (Law 09-08) EU-hosted infrastructure, data retention policies, user consent mechanisms
AML (Law 43-05) Audit trails supporting firms' compliance obligations
Tax compliance context Declaration types aligned with Moroccan fiscal calendar
10-year retention Archived data must be preserved for legal minimum

Infrastructure Constraints:

Constraint Implication
Moroccan internet (ADSL/4G) Page loads must work on slower connections -- server-side rendering, optimized asset delivery
Fiscal deadline peaks (15th-20th monthly, Jan-Mar annually) System must handle 2-3x normal load without degradation
Email delivery during peaks Mission-critical -- failed email = missed filing = financial penalty for client

Cross-Cutting Concerns Identified

  1. Tenant Isolation -- Every database query, every file access, every API response must be scoped to the current workspace. This is not just a middleware concern -- it's a per-model, per-controller, per-query concern. Existing pattern: session-based current_workspace_id with abort(404) for violations.

  2. Role-Based Authorization -- Three-tier access (Owner sees all, Manager sees all with configurable actions, Worker sees only assigned items). Affects every controller method, every Eloquent query scope, every Vue component's visible content. Current pattern: custom authorizeXxx() methods (no Gates/Policies).

  3. Audit Trail -- Every data modification logged with actor, timestamp, and change details (FR52-NFR12). Already implemented via Spatie Activity Log trait. Must extend to all new models.

  4. Email Notification Reliability -- 5 existing email types, more to come (nudge notifications, bulk scheduling). >99% delivery rate required. Queue-based sending with retry logic needed.

  5. File Security -- Client financial documents (CIN numbers, bank statements, tax documents) require encryption at rest. Access limited to authorized workspace members and specific client via token link.

  6. Search & Filtering -- Consistent filter bar across Clients, Declarations, Archive views with session persistence. Quick search across views. Affects frontend component architecture and backend query building.

  7. Bulk Operations -- Creating/notifying/updating 50+ items at once. Must be performant (10s target), provide feedback, and handle partial failures gracefully. Queue-based processing likely needed.

Starter Template Evaluation

Primary Technology Domain

Full-stack web monolith (Laravel 12 + Vue 3 via Inertia.js 2.0) -- brownfield project with established stack.

Starter Assessment: Brownfield Context

This project does not require a new starter template. The foundation is an active codebase with:

  • Backend: Laravel 12, PHP 8.2+ (8.4 runtime), Eloquent ORM, 7 models, 16 migrations
  • Frontend: Vue 3.5, TypeScript 5.2 strict, 31 page components, shadcn-vue design system (20+ component groups)
  • Bridge: Inertia.js 2.0 (server-driven SPA -- no REST API layer)
  • Styling: Tailwind CSS 4.1 with CVA, cn() helper
  • Auth: Laravel Fortify (login, registration, password reset, email verification, 2FA/TOTP)
  • Files: Spatie Media Library 11 (upload/download)
  • Logging: Spatie Activity Log 4 (audit trail)
  • Enums: bensampo/laravel-enum 6 (9 business enums)
  • Routes: Laravel Wayfinder 0.1.9 (type-safe frontend routes)
  • Icons: Lucide (lucide-vue-next)
  • Testing: Pest 4 (PHP-only), 13 feature test files
  • Code Quality: Laravel Pint, ESLint 9, Prettier 3 (with Tailwind plugin)
  • Build: Vite 7, SSR enabled
  • Dev Environment: Docker/Sail (MySQL 8.4, Mailpit, Soketi)
  • CI/CD: GitHub Actions (lint + test workflows)

Architectural Decisions Already Established

Language & Runtime:

  • PHP 8.2+ with strict typing conventions, explicit $fillable, Form Request classes, PHPDoc generics on relationships
  • TypeScript 5.2 strict mode, isolatedModules: true, import type for type-only imports, @/ path alias

Styling Solution:

  • Tailwind CSS 4.1 via @tailwindcss/vite plugin, shadcn-vue components (headless reka-ui primitives), CVA for variants, cn() utility

Build Tooling:

  • Vite 7 with laravel-vite-plugin 2.0, SSR support enabled, concurrent dev server (PHP + Queue + Logs + Vite)

Testing Framework:

  • Pest 4 with RefreshDatabase auto-applied, test() closures, expect() chaining, route() helper in tests

Code Organization:

  • Pages: resources/js/pages/{domain}/{Action}.vue
  • Components: resources/js/components/ (app) + components/ui/ (shadcn-vue, never modify)
  • Types: resources/js/types/ with barrel index.ts
  • Composables: resources/js/composables/
  • Controllers: app/Http/Controllers/
  • Requests: app/Http/Requests/
  • Models: app/Models/
  • Enums: app/Enums/
  • Mail: app/Mail/

Development Experience:

  • composer dev runs all services concurrently (server + queue + logs + Vite HMR)
  • composer test runs Pint lint check then Pest tests
  • npm run lint + npm run format for JS/Vue code quality
  • Docker/Sail for consistent local environment

Stack Gaps for MVP (Requiring Architectural Decisions)

Gap Options to Evaluate Decision Step
Permission toggle system JSON column on pivot vs. permissions table Step 4
Search implementation MySQL FULLTEXT vs. Laravel Scout + Meilisearch Step 4
In-app notifications DatabaseNotification vs. custom notifications table Step 4
Bulk operation processing Synchronous vs. queued jobs with progress broadcasting Step 4
Archive strategy Soft state on declarations vs. separate archive table Step 4
Filter persistence URL query params vs. session storage Step 4
Document preview Client-side PDF.js vs. server-generated previews Step 4
Real-time features Soketi (configured, not wired) -- scope of use for MVP Step 4

Note: No new starter initialization needed. First implementation story should be the "Folders to Declarations" terminology migration (Pre-Phase from PRD).

Core Architectural Decisions

Decision Priority Analysis

Critical Decisions (Block Implementation):

  • Permission toggle storage (JSON on pivot) -- blocks Phase 1 RBAC
  • Archive strategy (status-based archived_at) -- blocks Phase 5 archive system
  • Queue driver (Redis) -- blocks reliable email delivery and bulk operations
  • Email service provider (Amazon SES EU) -- blocks notification reliability

Important Decisions (Shape Architecture):

  • Search implementation (MySQL FULLTEXT) -- shapes Phase 4 search/filter
  • In-app notifications (Laravel DatabaseNotification) -- shapes Phase 3 nudge system
  • Filter persistence (URL query params) -- shapes Phase 4 filtering UX
  • Caching strategy (Redis) -- shapes dashboard performance
  • File storage (S3-compatible) -- shapes document management scalability
  • Hosting provider (EU VPS + Laravel Forge) -- shapes deployment pipeline

Deferred Decisions (Post-MVP):

  • Real-time WebSocket features (Soketi) -- not needed for MVP launch
  • Full observability stack (Grafana/Prometheus) -- Laravel Pulse + Sentry sufficient for MVP
  • Meilisearch integration -- MySQL FULLTEXT sufficient at initial scale
  • Progress broadcasting for bulk operations -- synchronous sufficient for MVP batch sizes

Data Architecture

D1: Permission Toggle Storage

  • Decision: JSON column (permissions) on the workspace_user pivot table
  • Rationale: 3 fixed roles with ~12 toggle-able permissions is a small, well-bounded set. JSON avoids extra JOINs on every permission check. Permissions are read frequently, written rarely (only when Owner configures Manager permissions).
  • Implementation: Add permissions JSON column to workspace_user migration. Default permissions per role defined in a config file or enum. WorkspaceUser model casts permissions to array.
  • Affects: FR3, FR4, FR7-FR11 (all role/permission features)

D2: Archive Strategy

  • Decision: Status-based archiving with archived_at timestamp column on declarations table
  • Rationale: Declarations stay in the same table, filtered out of active views via whereNull('archived_at') scope. Re-open is trivial (archived_at = null). At 5,000 declarations per workspace, a single indexed table performs well. 10-year retention is simply not deleting rows.
  • Implementation: Add archived_at nullable timestamp to declarations migration. Add scopeActive() and scopeArchived() Eloquent scopes. Auto-set archived_at when status becomes "Closed."
  • Affects: FR21-FR23, FR45-FR51 (entire archive system)

D3: Search Implementation

  • Decision: MySQL FULLTEXT indexes for quick search across clients and declarations
  • Rationale: No additional service dependency. At 200 workspaces with 500 clients / 5,000 declarations each, MySQL FULLTEXT delivers sub-second results. Workspace-scoped queries limit the search space further. Can migrate to Meilisearch post-MVP if search demands grow.
  • Implementation: Add FULLTEXT indexes on searchable columns (client name, declaration type/notes, etc.). Use MATCH...AGAINST in natural language mode. Laravel Scout can be added later as an abstraction layer without changing the search API.
  • Affects: FR43, NFR6 (quick search, 1-second response)

D4: Caching Strategy

  • Decision: Redis for application caching
  • Rationale: Triple-duty Redis (cache + queue + sessions) is the standard Laravel production pattern. Dashboard aggregation queries (counts by status, overdue items) benefit from short-lived cache. Redis is already supported by Laravel Sail.
  • Implementation: Add Redis service to Docker Compose. Set CACHE_DRIVER=redis, QUEUE_CONNECTION=redis, SESSION_DRIVER=redis in environment. Use Cache::remember() for dashboard aggregations with 5-minute TTL. Invalidate on status changes.
  • Affects: NFR1, NFR5, NFR16-20 (performance, scalability)

Authentication & Security

D5: Queue Driver

  • Decision: Redis queue driver
  • Rationale: Already adding Redis for cache/sessions. Redis queues are fast, support delayed jobs, retries (up to 3 within 5 minutes per NFR26), and job batching. Native Laravel support.
  • Implementation: Set QUEUE_CONNECTION=redis. Email mailables dispatched via dispatch() or Mail::queue(). Failed jobs table for monitoring. queue:work process managed by Supervisor in production.
  • Affects: NFR26 (email delivery reliability), FR32-FR33 (notification scheduling)

D6: File Encryption at Rest

  • Decision: Storage-level encryption (provider-managed AES-256 SSE)
  • Rationale: Meets NFR7 (AES-256 at rest) transparently without breaking Spatie Media Library or adding application-level encryption complexity. Encryption/decryption is handled by the storage provider with zero performance overhead for the application.
  • Implementation: Enable SSE (Server-Side Encryption) on the S3-compatible bucket. All objects encrypted automatically. No application code changes needed.
  • Affects: NFR7 (encryption at rest), NFR14 (document access control)

D7: Session Storage

  • Decision: Redis sessions
  • Rationale: Consistent with Redis for cache and queue (triple-duty). Fast session reads, supports horizontal scaling if needed later. Session-based workspace resolution (current_workspace_id) benefits from Redis speed.
  • Implementation: Set SESSION_DRIVER=redis. No application code changes -- Laravel handles session management transparently.
  • Affects: Multi-tenant workspace resolution (session-based current_workspace_id)

Communication Patterns

D8: In-App Notification System

  • Decision: Laravel's built-in DatabaseNotification system
  • Rationale: Battle-tested, supports database + mail channels, read/unread tracking built-in, Notifiable trait already on User model via Fortify. Workspace scoping added via query constraints when fetching notifications.
  • Implementation: Run php artisan notifications:table migration. Create notification classes (e.g., NudgeNotification, DeclarationOverdueNotification). Query notifications with workspace scope via the related declaration/client. Mark as read via markAsRead().
  • Affects: FR29-FR31 (nudge system, notification center)

D9: Bulk Operation Processing

  • Decision: Synchronous processing for bulk creation/updates; individual email sends queued via Redis
  • Rationale: 50 DB inserts complete in <1 second synchronously. Email notifications for those 50 items are dispatched to Redis queue individually, processing asynchronously. This meets the 10-second NFR target without the complexity of progress broadcasting. Controller returns immediately after DB operations; emails process in background.
  • Implementation: Bulk creation: Declaration::insert($records) or loop with create() for model events. Each notification dispatched to queue: Notification::send($users, new BulkNotification()). Failed individual sends retry automatically via Redis queue.
  • Affects: FR17, FR32 (bulk declaration creation, bulk notification scheduling), NFR2-3 (10-second target)

D10: Real-Time Features (Soketi)

  • Decision: Deferred for MVP -- no WebSocket wiring
  • Rationale: Inertia.js pages refresh data on every navigation, which is sufficient for MVP. The nudge notification center loads on page visit. Soketi remains configured in Docker for post-MVP activation. First post-MVP use case: real-time notification badge updates.
  • Implementation: No implementation needed for MVP. Soketi stays in Docker Compose for future use.
  • Affects: Post-MVP enhancement path

Frontend Architecture

D11: Filter Persistence

  • Decision: URL query parameters
  • Rationale: Native Inertia.js pattern -- router.get(url, { status: 'en_cours', assignee: 'me' }). Filters survive page refresh, browser back/forward, and are shareable/bookmarkable. No server-side session management needed. Consistent with how Inertia handles pagination.
  • Implementation: Filter components emit changes as URL params via router.get(). Controllers read filters from Request and apply Eloquent scopes. Default filters (e.g., Worker's "assignee = me") applied when no params present.
  • Affects: FR41-FR44 (filtering, persistence, search)

D12: Document Preview

  • Decision: Client-side viewers (PDF.js for PDFs, native <img> for images)
  • Rationale: No server processing overhead. PDF.js is mature and handles most PDF files. Images render natively. A shadcn-vue Dialog component wraps the viewer for a modal experience. File is fetched via existing Spatie Media download route.
  • Implementation: Vue component with <iframe> or PDF.js viewer for PDFs, <img> for images. Triggered from archive detail page or declaration detail. Falls back to download for unsupported file types.
  • Affects: FR48 (in-app document preview)

Infrastructure & Deployment

D13: Email Service Provider

  • Decision: Amazon SES (EU region -- eu-west-1 Ireland)
  • Rationale: High deliverability (>99%), cost-effective ($0.10/1000 emails), EU region for CNDP compliance, native Laravel SES driver (aws/aws-sdk-php). At 100-150 workspaces with ~5-10 emails per client per month, monthly email volume is well within SES free/low-cost tiers.
  • Implementation: Install aws/aws-sdk-php. Set MAIL_MAILER=ses with EU region configuration. Configure SPF, DKIM, DMARC for sending domain. Monitor delivery via SES dashboard and CloudWatch.
  • Affects: NFR26 (>99% email delivery), FR33 (email notifications), NFR13 (CNDP -- EU data processing)

D14: Hosting Provider

  • Decision: EU-based VPS (Hetzner Cloud or DigitalOcean EU) managed via Laravel Forge
  • Rationale: Cost-effective for MVP scale (100-150 workspaces). EU data centers for CNDP compliance. Laravel Forge handles server provisioning, deployment, SSL, queue workers, and scheduled tasks -- reducing DevOps overhead for a 2-person team. Can scale vertically (bigger VPS) or horizontally (add servers) as needed.
  • Implementation: Provision VPS via Laravel Forge. Configure Nginx, PHP-FPM, MySQL, Redis, Supervisor (queue workers). Deploy via Forge's GitHub integration (push to deploy). SSL via Let's Encrypt.
  • Affects: NFR13 (CNDP -- EU hosting), NFR21 (99.5% uptime), NFR22-25 (backup strategy)

D15: File Storage

  • Decision: S3-compatible object storage with AES-256 SSE
  • Rationale: Scalable to 1TB+ (NFR19), encryption at rest (NFR7), works natively with Spatie Media Library's S3 disk driver. Provider options: AWS S3 (eu-west-1), Hetzner Object Storage, DigitalOcean Spaces, or Cloudflare R2 -- chosen based on hosting provider for network proximity.
  • Implementation: Configure filesystems.php S3 disk with EU endpoint. Set Spatie Media Library to use S3 disk. Enable SSE on bucket. Backup policy: bucket versioning enabled.
  • Affects: NFR7 (encryption at rest), NFR19 (1TB storage), NFR28 (zero data loss)

D16: Monitoring & Alerting

  • Decision: Laravel Pulse (application monitoring) + Sentry (error tracking/alerting)
  • Rationale: Laravel Pulse is zero-config for Laravel apps -- provides request performance, queue health, and slow query monitoring out of the box. Sentry catches exceptions with full stack traces, alerting via email/Slack. Both have generous free tiers. Post-MVP: add uptime monitoring (Oh Dear or BetterStack).
  • Implementation: Install laravel/pulse and configure dashboard (admin-only route). Install Sentry PHP SDK and configure DSN. Set up alert rules for error spikes and queue failures.
  • Affects: NFR21 (99.5% uptime), NFR27 (monitoring and alerting)

Decision Impact Analysis

Implementation Sequence:

  1. Redis integration (D4/D5/D7) -- foundational service, add to Docker Compose first
  2. Permission toggle migration (D1) -- blocks Phase 1 RBAC
  3. Archive column migration (D2) -- blocks Phase 5, but can be added early
  4. FULLTEXT indexes (D3) -- blocks Phase 4 search
  5. Notification system setup (D8) -- blocks Phase 3 nudge system
  6. S3 storage configuration (D15) -- can run parallel to feature development
  7. Email service configuration (D13) -- needed before production launch
  8. Hosting provisioning (D14) -- needed before production launch
  9. Monitoring setup (D16) -- needed before production launch
  10. Document preview component (D12) -- needed for Phase 5

Cross-Component Dependencies:

  • Redis (D4/D5/D7) must be configured before queue-dependent features (bulk email, notifications)
  • S3 storage (D15) + SSE encryption (D6) must be configured before production file uploads
  • SES (D13) must be verified and configured before any email-dependent features go to production
  • Permission system (D1) is the foundation for all role-based dashboard and feature access (D8, D9, D11)

Implementation Patterns & Consistency Rules

Pattern Categories Defined

Critical Conflict Points Identified: 12 areas where AI agents could make different choices when implementing MVP features. The existing project-context.md covers general coding conventions. These patterns cover the domain-specific implementation decisions for RBAC, declarations, notifications, bulk operations, archive, search, and filtering.

Workspace & Tenant Scoping Patterns

Every model query MUST be workspace-scoped. No exceptions.

Pattern: Use Eloquent global scopes or explicit where('workspace_id', $workspaceId) on every query that touches workspace-owned data.

// CORRECT: Workspace-scoped query
$clients = Client::where('workspace_id', $workspace->id)
    ->where('status', ClientStatus::Active)
    ->get();

// CORRECT: Via relationship (inherits workspace scope)
$declarations = $client->declarations()->active()->get();

// WRONG: Unscoped query -- potential data leak
$clients = Client::where('status', ClientStatus::Active)->get();

Workspace resolution: Always from session helper, never from request params:

// CORRECT
$workspace = auth()->user()->currentWorkspace();
$workspaceId = session('current_workspace_id');

// WRONG
$workspace = Workspace::find($request->workspace_id);

Permission Checking Patterns

Pattern: Custom authorize methods in controllers using the JSON permissions column.

// In controller base or trait:
protected function authorizePermission(string $permission): void
{
    $workspaceUser = auth()->user()->currentWorkspaceUser();

    if ($workspaceUser->role === WorkspaceUserRole::Owner) {
        return; // Owners can do everything
    }

    if ($workspaceUser->role === WorkspaceUserRole::Worker) {
        abort(404); // Workers never have configurable permissions
    }

    // Manager: check JSON permissions column
    if (!($workspaceUser->permissions[$permission] ?? false)) {
        abort(404); // 404 not 403 -- per project convention
    }
}

Permission keys follow snake_case: can_manage_team, can_view_activity_logs, can_configure_portal.

Default permissions per role defined in a config file:

// config/permissions.php
return [
    'defaults' => [
        'owner' => ['*'], // all permissions
        'manager' => [
            'can_manage_team' => false,
            'can_view_activity_logs' => true,
            'can_configure_portal' => false,
        ],
        'worker' => [], // no configurable permissions
    ],
];

Role-Scoped Query Patterns

Workers see only their assigned items. Owners/Managers see everything in the workspace.

// Pattern: scopeForUser() on Declaration model
public function scopeForUser(Builder $query, User $user, Workspace $workspace): Builder
{
    $role = $user->roleIn($workspace);

    if ($role === WorkspaceUserRole::Worker) {
        return $query->where('assigned_to', $user->id);
    }

    // Owner and Manager see all workspace declarations
    return $query;
}

// Usage in controller:
$declarations = Declaration::where('workspace_id', $workspace->id)
    ->forUser(auth()->user(), $workspace)
    ->active()
    ->get();

This pattern applies to: Declarations, Clients (via assigned declarations), Archive items, Activity logs.

Declaration Status Flow

Valid status transitions must be enforced. Not every status can transition to every other.

Created → En cours → En attente client → En cours → Terminé → Fermé → [auto-archive]
                                                   ↗
Created → En cours → Terminé → Fermé → [auto-archive]

Status enum values and their meanings:

Status Meaning Who Can Set Next Valid Statuses
created Declaration just created System en_cours
en_cours Being worked on Owner/Manager/Worker en_attente_client, termine
en_attente_client Waiting for client documents Owner/Manager/Worker en_cours
termine Work completed Owner/Manager/Worker ferme
ferme Closed (triggers auto-archive) Owner/Manager (archived)

Auto-archive trigger: When status becomes ferme, set archived_at = now().

Re-open from archive: Only Owner/Manager. Sets archived_at = null, status back to en_cours. Audit log entry required with reason.

Notification Patterns

All notifications use Laravel's notification system with database + mail channels.

// Notification class naming: {Action}Notification
class NudgeNotification extends Notification implements ShouldQueue
{
    use Queueable;

    public function via($notifiable): array
    {
        return ['database', 'mail'];
    }

    public function toArray($notifiable): array
    {
        return [
            'type' => 'nudge',
            'declaration_id' => $this->declaration->id,
            'workspace_id' => $this->declaration->workspace_id,
            'sender_id' => $this->sender->id,
            'message' => $this->message,
        ];
    }
}

Notification payload always includes workspace_id -- enables workspace-scoped notification queries.

Notification types follow snake_case: nudge, declaration_overdue, document_uploaded, bulk_notification.

Bulk Operation Patterns

Bulk creation: synchronous DB operations, queued notifications.

// Pattern for bulk declaration creation
public function bulkStore(BulkStoreDeclarationRequest $request)
{
    $declarations = [];

    DB::transaction(function () use ($request, &$declarations) {
        foreach ($request->validated()['items'] as $item) {
            $declarations[] = Declaration::create([
                'workspace_id' => $workspace->id,
                'client_id' => $item['client_id'],
                'type' => $item['type'],
                'deadline' => $item['deadline'],
                'assigned_to' => $item['assigned_to'],
                'status' => DeclarationStatus::Created,
            ]);
        }
    });

    // Queue notifications separately (non-blocking)
    if ($request->validated()['notify_clients']) {
        foreach ($declarations as $declaration) {
            $declaration->client->notify(new DeclarationCreatedNotification($declaration));
        }
    }

    return redirect()->back()->with('success', count($declarations) . ' declarations created.');
}

Bulk operations always wrapped in DB::transaction() -- all-or-nothing.

Filter & Search Patterns

Filters via URL query params. Controllers read from Request, apply Eloquent scopes.

// Pattern: Reusable filter handling in controller
protected function applyFilters(Builder $query, Request $request): Builder
{
    return $query
        ->when($request->status, fn ($q, $status) => $q->where('status', $status))
        ->when($request->assignee, fn ($q, $assignee) => $q->where('assigned_to', $assignee))
        ->when($request->client_id, fn ($q, $clientId) => $q->where('client_id', $clientId))
        ->when($request->type, fn ($q, $type) => $q->where('type', $type))
        ->when($request->search, fn ($q, $search) =>
            $q->whereRaw('MATCH(searchable_columns) AGAINST(? IN NATURAL LANGUAGE MODE)', [$search])
        )
        ->when($request->sort, fn ($q, $sort) =>
            $q->orderBy($sort, $request->input('direction', 'asc'))
        , fn ($q) => $q->orderBy('deadline', 'asc')); // default sort
}

Frontend: Filters passed as props, emitted as URL params:

// Pattern: Filter component emits changes via Inertia router
function applyFilter(key: string, value: string | null) {
    router.get(props.indexUrl, {
        ...currentFilters,
        [key]: value,
        page: 1, // reset pagination on filter change
    }, {
        preserveState: true,
        preserveScroll: true,
    });
}

Default sort: deadline ASC (most urgent first) for declarations. name ASC for clients.

Archive Query Patterns

Active queries exclude archived items by default. Archive queries include only archived items.

// Model scopes on Declaration:
public function scopeActive(Builder $query): Builder
{
    return $query->whereNull('archived_at');
}

public function scopeArchived(Builder $query): Builder
{
    return $query->whereNotNull('archived_at');
}

// Controllers ALWAYS use one or the other:
// DeclarationController: Declaration::active()->forUser()->get()
// ArchiveController: Declaration::archived()->forUser()->get()

Archive detail pages are read-only. No edit actions, no status changes. Only re-open (Owner/Manager).

Dashboard Aggregation Patterns

Dashboard queries use cached aggregation with 5-minute TTL.

// Pattern: Dashboard data with cache
$dashboardData = Cache::remember(
    "dashboard:{$workspace->id}:{$user->id}",
    300, // 5 minutes
    fn () => [
        'total_active' => Declaration::workspace($workspace)->active()->forUser($user, $workspace)->count(),
        'by_status' => Declaration::workspace($workspace)->active()->forUser($user, $workspace)
            ->selectRaw('status, count(*) as count')
            ->groupBy('status')
            ->pluck('count', 'status'),
        'overdue' => Declaration::workspace($workspace)->active()->forUser($user, $workspace)
            ->where('deadline', '<', now())
            ->count(),
        'due_this_week' => Declaration::workspace($workspace)->active()->forUser($user, $workspace)
            ->whereBetween('deadline', [now(), now()->endOfWeek()])
            ->count(),
    ]
);

Cache invalidation: Bust cache on declaration status change, creation, or deletion:

Cache::forget("dashboard:{$workspace->id}:*"); // pattern-based invalidation

Error Handling Patterns

Consistent error handling across controllers:

Scenario Response Code
Workspace boundary violation abort(404) 404
Permission denied abort(404) (intentional -- don't reveal existence) 404
Validation error Inertia automatic redirect with errors 422
Resource not found abort(404) 404
Server error Sentry captures, generic error page 500

User-facing flash messages follow this format:

return redirect()->back()->with('success', 'Declaration created successfully.');
return redirect()->back()->with('error', 'Unable to send notification. Please try again.');

Flash message types: success, error, warning, info.

Frontend Component Patterns for New Features

Summary cards above tables:

<!-- Pattern: Clickable summary cards that filter the table -->
<SummaryCards
    :items="[
        { label: 'En cours', count: stats.en_cours, filter: { status: 'en_cours' }, color: 'blue' },
        { label: 'En retard', count: stats.overdue, filter: { overdue: true }, color: 'red' },
        { label: 'En attente', count: stats.en_attente, filter: { status: 'en_attente_client' }, color: 'amber' },
    ]"
    @filter="applyFilter"
/>

Inline row actions pattern:

<!-- Pattern: Actions available at the row level -->
<DropdownMenu>
    <DropdownMenuTrigger as-child>
        <Button variant="ghost" size="icon"><MoreHorizontal /></Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent>
        <DropdownMenuItem @click="router.get(row.showUrl)">View</DropdownMenuItem>
        <DropdownMenuItem @click="openNudgeDialog(row)">Nudge</DropdownMenuItem>
        <DropdownMenuItem @click="router.get(row.editUrl)">Edit</DropdownMenuItem>
    </DropdownMenuContent>
</DropdownMenu>

Nudge dialog: Popover, not full modal. Quick action = minimal UI.

Enforcement Guidelines

All AI Agents MUST:

  1. Read project-context.md AND this architecture document before implementing any feature
  2. Scope every database query to workspace_id -- no unscoped queries on workspace-owned models
  3. Use abort(404) for all authorization failures -- never abort(403)
  4. Use the role-scoped query pattern (forUser() scope) on all declaration/client queries
  5. Wrap bulk operations in DB::transaction()
  6. Use Laravel's notification system for all in-app + email notifications
  7. Pass filters via URL query params, not session storage
  8. Use Cache::remember() for dashboard aggregations with cache invalidation on data changes
  9. Follow the declaration status flow -- no arbitrary status transitions
  10. Include workspace_id in all notification payloads

Anti-Patterns to Avoid:

  • Querying declarations without workspace scope
  • Using abort(403) instead of abort(404) for permission failures
  • Creating custom notification tables instead of using Laravel's DatabaseNotification
  • Storing filters in session instead of URL params
  • Skipping DB::transaction() on bulk operations
  • Hardcoding dashboard counts instead of using cached aggregations
  • Allowing invalid status transitions on declarations

Project Structure & Boundaries

Complete Project Directory Structure

The existing project structure is documented in docs/source-tree-analysis.md. Below shows the complete MVP structure with new additions marked with ← NEW.

l'ami fiduciaire/
├── app/
│   ├── Actions/
│   │   └── Fortify/                          # Auth actions (existing)
│   ├── Concerns/                             # Shared traits
│   │   ├── PasswordValidationRules.php
│   │   ├── ProfileValidationRules.php
│   │   ├── HasWorkspaceScope.php             ← NEW (workspace scoping trait for controllers)
│   │   └── AuthorizesPermissions.php         ← NEW (permission checking trait)
│   ├── Enums/
│   │   ├── ClientStatus.php
│   │   ├── DeclarationStatus.php             ← NEW (replaces FolderStatus)
│   │   ├── DeclarationType.php               ← NEW (replaces FolderType)
│   │   ├── DeclarationPriority.php           ← NEW (replaces FolderPriority)
│   │   ├── LegalForm.php
│   │   ├── MessageType.php
│   │   ├── NotificationType.php              ← NEW (nudge, overdue, document_uploaded, etc.)
│   │   ├── Permission.php                    ← NEW (enum of permission keys)
│   │   └── WorkspaceUserRole.php
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Client/                       # Public client portal (existing)
│   │   │   │   ├── UploadController.php
│   │   │   │   ├── ConfirmController.php
│   │   │   │   └── RefuseController.php
│   │   │   ├── Settings/                     # User settings (existing)
│   │   │   ├── Admin/                        ← NEW (SaaS admin controllers)
│   │   │   │   ├── AdminDashboardController.php    ← NEW
│   │   │   │   ├── WorkspaceController.php         ← MOVED from root
│   │   │   │   └── UserController.php              ← MOVED from root
│   │   │   ├── ClientController.php          # (existing, extended)
│   │   │   ├── DashboardController.php       # (existing, rewritten for Phase 2)
│   │   │   ├── DeclarationController.php     ← NEW (replaces FolderController)
│   │   │   ├── DeclarationMediaController.php ← NEW (replaces FolderMediaController)
│   │   │   ├── ArchiveController.php         ← NEW (Phase 5)
│   │   │   ├── NotificationController.php    # (existing, extended for Phase 3)
│   │   │   ├── TeamController.php            ← NEW (Phase 1 team management)
│   │   │   ├── BulkDeclarationController.php ← NEW (Phase 4)
│   │   │   ├── NudgeController.php           ← NEW (Phase 3)
│   │   │   └── WorkspaceSwitchController.php # (existing)
│   │   ├── Middleware/
│   │   │   ├── EnsureUserHasWorkspace.php
│   │   │   ├── EnsureUserIsAdmin.php
│   │   │   ├── HandleAppearance.php
│   │   │   ├── HandleInertiaRequests.php
│   │   │   └── ValidateClientPortalToken.php ← NEW (replaces ValidateFolderInvitation)
│   │   └── Requests/
│   │       ├── Settings/
│   │       ├── StoreClientRequest.php
│   │       ├── UpdateClientRequest.php
│   │       ├── StoreDeclarationRequest.php   ← NEW
│   │       ├── UpdateDeclarationRequest.php  ← NEW
│   │       ├── BulkStoreDeclarationRequest.php ← NEW (Phase 4)
│   │       ├── StoreNudgeRequest.php         ← NEW (Phase 3)
│   │       ├── UpdateTeamMemberRequest.php   ← NEW (Phase 1)
│   │       ├── InviteTeamMemberRequest.php   ← NEW (Phase 1)
│   │       └── UpdatePermissionsRequest.php  ← NEW (Phase 1)
│   ├── Mail/
│   │   ├── DeclarationConfirmationMail.php   ← NEW (replaces FolderConfirmationMail)
│   │   ├── DeclarationFileRequestMail.php    ← NEW (replaces FolderFileRequestMail)
│   │   ├── DeclarationInviteMail.php         ← NEW (replaces FolderInviteMail)
│   │   ├── DeclarationSituationMail.php      ← NEW (replaces FolderSituationMail)
│   │   ├── DeclarationTextMessageMail.php    ← NEW (replaces FolderTextMessageMail)
│   │   └── NudgeNotificationMail.php         ← NEW (Phase 3)
│   ├── Models/
│   │   ├── Client.php                        # (existing, extended with search scope)
│   │   ├── Declaration.php                   ← NEW (replaces Folder)
│   │   ├── DeclarationInvitation.php         ← NEW (replaces FolderInvitation)
│   │   ├── Message.php
│   │   ├── User.php                          # (existing, add Notifiable extensions)
│   │   ├── Workspace.php
│   │   └── WorkspaceUser.php                 # (existing, add permissions JSON cast)
│   ├── Notifications/                        ← NEW (Phase 3)
│   │   ├── NudgeNotification.php             ← NEW
│   │   ├── DeclarationOverdueNotification.php ← NEW
│   │   ├── DocumentUploadedNotification.php  ← NEW
│   │   └── BulkNotification.php              ← NEW (Phase 4)
│   ├── Observers/                            ← NEW
│   │   └── DeclarationObserver.php           ← NEW (status transitions, auto-archive, cache bust)
│   └── Providers/
├── config/
│   ├── permissions.php                       ← NEW (role default permissions)
│   └── ...                                   # Standard Laravel config
├── database/
│   ├── factories/
│   │   ├── ClientFactory.php                 # (existing)
│   │   ├── DeclarationFactory.php            ← NEW (replaces FolderFactory)
│   │   └── WorkspaceUserFactory.php          # (existing, extended)
│   ├── migrations/
│   │   ├── ...                               # Existing 16 migrations
│   │   ├── xxxx_rename_folders_to_declarations.php          ← NEW (Pre-Phase)
│   │   ├── xxxx_add_permissions_to_workspace_user.php       ← NEW (Phase 1)
│   │   ├── xxxx_add_archived_at_to_declarations.php         ← NEW (Phase 5)
│   │   ├── xxxx_add_fulltext_indexes.php                    ← NEW (Phase 4)
│   │   ├── xxxx_create_notifications_table.php              ← NEW (Phase 3)
│   │   └── xxxx_add_searchable_columns.php                  ← NEW (Phase 4)
│   └── seeders/
│       ├── DatabaseSeeder.php
│       └── PermissionSeeder.php              ← NEW (default permission sets)
├── resources/
│   ├── css/
│   ├── js/
│   │   ├── app.ts
│   │   ├── ssr.ts
│   │   ├── components/
│   │   │   ├── ui/                           # shadcn-vue (NEVER modify)
│   │   │   ├── app/                          ← NEW (app shell components)
│   │   │   │   ├── AppSidebar.vue            ← NEW (role-driven sidebar, Phase 2)
│   │   │   │   ├── NotificationBell.vue      ← NEW (Phase 3)
│   │   │   │   └── WorkspaceSwitcher.vue     # (existing)
│   │   │   ├── declarations/                 ← NEW (replaces folders/)
│   │   │   │   ├── DeclarationForm.vue       ← NEW
│   │   │   │   ├── DeclarationStatusBadge.vue ← NEW
│   │   │   │   ├── DeclarationRowActions.vue ← NEW
│   │   │   │   └── BulkCreateForm.vue        ← NEW (Phase 4)
│   │   │   ├── clients/                      # (existing, extended)
│   │   │   │   ├── ClientForm.vue
│   │   │   │   └── ClientStatusBadge.vue     ← NEW
│   │   │   ├── dashboard/                    ← NEW (Phase 2)
│   │   │   │   ├── SummaryCards.vue          ← NEW
│   │   │   │   ├── OwnerDashboard.vue        ← NEW
│   │   │   │   ├── WorkerDashboard.vue       ← NEW
│   │   │   │   └── OverdueAlerts.vue         ← NEW
│   │   │   ├── notifications/                ← NEW (Phase 3)
│   │   │   │   ├── NotificationList.vue      ← NEW
│   │   │   │   ├── NudgePopover.vue          ← NEW
│   │   │   │   └── NotificationItem.vue      ← NEW
│   │   │   ├── archive/                      ← NEW (Phase 5)
│   │   │   │   ├── ArchiveDetailView.vue     ← NEW
│   │   │   │   └── DocumentPreview.vue       ← NEW (PDF.js viewer)
│   │   │   ├── filters/                      ← NEW (Phase 4)
│   │   │   │   ├── FilterBar.vue             ← NEW
│   │   │   │   ├── FilterSelect.vue          ← NEW
│   │   │   │   ├── SearchInput.vue           ← NEW
│   │   │   │   └── ActiveFilters.vue         ← NEW
│   │   │   ├── team/                         ← NEW (Phase 1)
│   │   │   │   ├── TeamMemberRow.vue         ← NEW
│   │   │   │   ├── InviteMemberForm.vue      ← NEW
│   │   │   │   └── PermissionToggles.vue     ← NEW
│   │   │   ├── Pagination.vue                # (existing)
│   │   │   └── DataTable.vue                 ← NEW (reusable table with sort/filter)
│   │   ├── composables/
│   │   │   ├── useAppearance.ts
│   │   │   ├── useCurrentUrl.ts
│   │   │   ├── useInitials.ts
│   │   │   ├── useTwoFactorAuth.ts
│   │   │   ├── useFilters.ts                 ← NEW (Phase 4, URL param filter management)
│   │   │   ├── usePermissions.ts             ← NEW (Phase 1, check user permissions)
│   │   │   └── useBulkSelection.ts           ← NEW (Phase 4, table row selection)
│   │   ├── layouts/
│   │   │   ├── app/                          # App layouts (existing, modified for role sidebar)
│   │   │   ├── auth/
│   │   │   ├── settings/
│   │   │   ├── AppLayout.vue
│   │   │   └── AuthLayout.vue
│   │   ├── lib/
│   │   │   └── utils.ts                      # (existing)
│   │   ├── pages/
│   │   │   ├── auth/                         # Auth pages (existing)
│   │   │   ├── client/                       # Public client portal (existing)
│   │   │   ├── clients/                      # Client management (existing)
│   │   │   │   ├── Index.vue                 # (extended with filters)
│   │   │   │   ├── Show.vue
│   │   │   │   ├── Create.vue
│   │   │   │   └── Edit.vue
│   │   │   ├── declarations/                 ← NEW (replaces folders/)
│   │   │   │   ├── Index.vue                 ← NEW
│   │   │   │   ├── Show.vue                  ← NEW
│   │   │   │   ├── Create.vue                ← NEW
│   │   │   │   ├── Edit.vue                  ← NEW
│   │   │   │   └── BulkCreate.vue            ← NEW (Phase 4)
│   │   │   ├── archive/                      ← NEW (Phase 5)
│   │   │   │   ├── Index.vue                 ← NEW
│   │   │   │   └── Show.vue                  ← NEW (read-only detail)
│   │   │   ├── team/                         ← NEW (Phase 1)
│   │   │   │   ├── Index.vue                 ← NEW
│   │   │   │   └── Permissions.vue           ← NEW
│   │   │   ├── notifications/                ← NEW (Phase 3)
│   │   │   │   └── Index.vue                 ← NEW
│   │   │   ├── admin/                        ← NEW (SaaS admin)
│   │   │   │   ├── Dashboard.vue             ← NEW
│   │   │   │   ├── workspaces/               ← MOVED
│   │   │   │   └── users/                    ← MOVED
│   │   │   ├── settings/                     # (existing)
│   │   │   ├── Dashboard.vue                 # (existing, rewritten Phase 2)
│   │   │   └── Welcome.vue
│   │   └── types/
│   │       ├── index.ts                      # (extended with new types)
│   │       ├── declaration.ts                ← NEW
│   │       ├── notification.ts               ← NEW
│   │       ├── team.ts                       ← NEW
│   │       ├── archive.ts                    ← NEW
│   │       ├── filters.ts                    ← NEW
│   │       └── dashboard.ts                  ← NEW
│   └── views/
│       ├── app.blade.php
│       └── emails/
│           ├── declarations/                 ← NEW (replaces folder email templates)
│           └── nudge.blade.php               ← NEW
├── routes/
│   ├── web.php                               # (extended with new routes)
│   ├── settings.php
│   └── console.php
├── tests/
│   ├── Feature/
│   │   ├── Auth/                             # (existing)
│   │   ├── Settings/                         # (existing)
│   │   ├── Declarations/                     ← NEW
│   │   │   ├── CreateDeclarationTest.php     ← NEW
│   │   │   ├── UpdateDeclarationTest.php     ← NEW
│   │   │   ├── StatusTransitionTest.php      ← NEW
│   │   │   ├── BulkCreateTest.php            ← NEW
│   │   │   └── ArchiveTest.php               ← NEW
│   │   ├── Team/                             ← NEW
│   │   │   ├── ManageTeamTest.php            ← NEW
│   │   │   ├── PermissionsTest.php           ← NEW
│   │   │   └── RoleScopingTest.php           ← NEW
│   │   ├── Notifications/                    ← NEW
│   │   │   ├── NudgeTest.php                 ← NEW
│   │   │   └── NotificationCenterTest.php    ← NEW
│   │   ├── Filters/                          ← NEW
│   │   │   └── FilterPersistenceTest.php     ← NEW
│   │   ├── Dashboard/                        ← NEW
│   │   │   ├── OwnerDashboardTest.php        ← NEW
│   │   │   └── WorkerDashboardTest.php       ← NEW
│   │   ├── DashboardTest.php                 # (existing)
│   │   └── WorkspaceScopingTest.php          ← NEW (cross-cutting)
│   └── Unit/
│       ├── DeclarationStatusTest.php         ← NEW (state machine unit tests)
│       └── PermissionCheckTest.php           ← NEW
├── .github/workflows/
│   ├── lint.yml                              # (existing)
│   └── tests.yml                             # (existing)
├── compose.yaml                              # (add Redis service)
├── composer.json
├── package.json
├── vite.config.ts
├── tsconfig.json
└── AGENTS.md

Architectural Boundaries

Layer Boundaries (Request Flow):

Browser → Inertia Request
    → Middleware (auth, workspace, appearance)
        → Controller (authorize, validate, query, render)
            → Model (Eloquent scopes, relationships, casts)
                → Database (MySQL, workspace-scoped)
            → Notification (database + mail channels)
                → Queue (Redis)
            → Cache (Redis)
        → Inertia Response (page component + props)
    → Vue Page (layout + components + composables)

Controller Boundary: Controllers are the ONLY layer that orchestrates business logic. No service classes for MVP -- keep logic in controllers with extracted traits for shared patterns (HasWorkspaceScope, AuthorizesPermissions).

API Boundaries:

Boundary Pattern Notes
Inertia pages Controller → Inertia::render() No REST API. All data via Inertia props.
Client portal Token-based public routes Isolated from authenticated routes. No session.
Admin panel EnsureUserIsAdmin middleware group Separate controller namespace Admin/
Notifications Laravel notification channels database + mail channels via queue
File access Spatie Media Library routes Authenticated download via signed URLs

Data Boundaries:

Model Workspace-Scoped Role-Scoped Cacheable
Client Yes (workspace_id) No (all roles see all clients) No
Declaration Yes (workspace_id) Yes (forUser() scope) Dashboard counts only
WorkspaceUser Yes (pivot on workspace) No No
Notification Via declaration's workspace_id Yes (per-user) No
Message Via declaration relationship Via declaration scope No

Component Boundaries (Frontend):

Boundary Communication Data Source
Page → Layout Slots, layout props Inertia shared data
Page → Feature Component Props down, events up Page-level Inertia props
Feature Component → UI Component Props only Parent component
Filter Component → Page router.get() URL params URL query string
Summary Card → Table @filter event → applyFilter() Shared filter state

Requirements to Structure Mapping

Pre-Phase: Terminology Migration

  • Rename: FolderDeclaration across all models, controllers, enums, pages, tests, migrations
  • Files affected: ~40 files (models, controllers, requests, pages, components, enums, mail, tests)
  • New migration: xxxx_rename_folders_to_declarations.php

Phase 1: Role System Foundation (FR3-FR4, FR7-FR11)

  • Backend: app/Concerns/AuthorizesPermissions.php, config/permissions.php, app/Enums/Permission.php
  • Backend: app/Http/Controllers/TeamController.php, app/Http/Requests/UpdatePermissionsRequest.php
  • Frontend: resources/js/pages/team/, resources/js/components/team/
  • Composable: resources/js/composables/usePermissions.ts
  • Migration: xxxx_add_permissions_to_workspace_user.php
  • Tests: tests/Feature/Team/

Phase 2: Dashboard Separation (FR24-FR28)

  • Backend: app/Http/Controllers/DashboardController.php (rewrite)
  • Frontend: resources/js/pages/Dashboard.vue (rewrite), resources/js/components/dashboard/
  • Types: resources/js/types/dashboard.ts
  • Tests: tests/Feature/Dashboard/

Phase 3: Collaboration Features (FR29-FR33)

  • Backend: app/Http/Controllers/NudgeController.php, app/Notifications/
  • Frontend: resources/js/pages/notifications/, resources/js/components/notifications/
  • Frontend: resources/js/components/app/NotificationBell.vue
  • Migration: xxxx_create_notifications_table.php
  • Tests: tests/Feature/Notifications/

Phase 4: Workflow Efficiency (FR17, FR32, FR41-FR44)

  • Backend: app/Http/Controllers/BulkDeclarationController.php
  • Frontend: resources/js/pages/declarations/BulkCreate.vue, resources/js/components/filters/
  • Composables: resources/js/composables/useFilters.ts, useBulkSelection.ts
  • Migration: xxxx_add_fulltext_indexes.php
  • Tests: tests/Feature/Declarations/BulkCreateTest.php, tests/Feature/Filters/

Phase 5: Archive System (FR21-FR23, FR45-FR51)

  • Backend: app/Http/Controllers/ArchiveController.php, app/Observers/DeclarationObserver.php
  • Frontend: resources/js/pages/archive/, resources/js/components/archive/
  • Migration: xxxx_add_archived_at_to_declarations.php
  • Tests: tests/Feature/Declarations/ArchiveTest.php

Cross-Cutting Concerns:

Concern Location
Workspace scoping app/Concerns/HasWorkspaceScope.php → used by all controllers
Permission checking app/Concerns/AuthorizesPermissions.php → used by all controllers
Role-scoped queries scopeForUser() on Declaration model
Audit logging Spatie Activity Log trait on all models (existing)
Cache invalidation DeclarationObserver → busts dashboard cache on status change
Filter handling useFilters.ts composable → applyFilters() controller method
Error handling abort(404) convention → all controllers

Integration Points

Internal Communication:

  • Controller → Notification: Via $user->notify(new XxxNotification()) (queued)
  • Controller → Cache: Via Cache::remember() / Cache::forget() (Redis)
  • Observer → Cache: DeclarationObserver fires cache invalidation on model events
  • Frontend → Backend: Inertia router.get() / router.post() / router.put() / router.delete()

External Integrations:

Service Integration Point Configuration
Amazon SES (eu-west-1) Mail facade via ses driver config/mail.php, .env
S3-compatible storage Storage facade via s3 disk config/filesystems.php, .env
Sentry Error reporting SDK config/sentry.php, .env
Laravel Pulse Admin dashboard route config/pulse.php

Data Flow (Declaration Lifecycle):

Create → DB::transaction → Declaration model → Activity Log
    → Cache bust (dashboard)
    → Optional: notify client (queued mail)

Status Change → Controller validates transition → Update model → Observer fires
    → Cache bust (dashboard)
    → If "ferme": set archived_at
    → Activity Log

Nudge → NudgeController → NudgeNotification (database + mail channels)
    → Database: notifications table
    → Mail: Redis queue → SES

File Organization Patterns

New files follow existing conventions:

  • Controllers: app/Http/Controllers/{Resource}Controller.php (resource controllers)
  • Requests: app/Http/Requests/{Action}{Resource}Request.php (Store/Update prefix)
  • Models: app/Models/{Resource}.php (singular PascalCase)
  • Enums: app/Enums/{ResourceProperty}.php (PascalCase)
  • Notifications: app/Notifications/{Action}Notification.php
  • Pages: resources/js/pages/{resource}/{Action}.vue (lowercase plural folder, PascalCase file)
  • Components: resources/js/components/{feature}/{ComponentName}.vue (lowercase folder, PascalCase file)
  • Types: resources/js/types/{feature}.ts (lowercase singular)
  • Tests: tests/Feature/{Resource}/{ActionTest}.php (PascalCase folder and file)

Test Organization:

  • Feature tests mirror controller structure: one test file per controller action or user flow
  • Unit tests for pure logic: status transitions, permission checks
  • All tests use RefreshDatabase, test() closures, expect() assertions

Development Workflow Integration

Route Registration (web.php):

// Authenticated + workspace routes
Route::middleware(['auth', 'verified', 'workspace'])->group(function () {
    Route::get('/dashboard', DashboardController::class)->name('dashboard');
    Route::resource('clients', ClientController::class);
    Route::resource('declarations', DeclarationController::class);
    Route::post('declarations/bulk', [BulkDeclarationController::class, 'store'])->name('declarations.bulk.store');
    Route::get('archive', [ArchiveController::class, 'index'])->name('archive.index');
    Route::get('archive/{declaration}', [ArchiveController::class, 'show'])->name('archive.show');
    Route::post('archive/{declaration}/reopen', [ArchiveController::class, 'reopen'])->name('archive.reopen');
    Route::get('team', [TeamController::class, 'index'])->name('team.index');
    Route::put('team/{workspaceUser}/permissions', [TeamController::class, 'updatePermissions'])->name('team.permissions');
    Route::post('nudge', [NudgeController::class, 'store'])->name('nudge.store');
    Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index');
    Route::post('notifications/{notification}/read', [NotificationController::class, 'markAsRead'])->name('notifications.read');
});

// Admin routes
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () { ... });

// Public client portal
Route::middleware(['client.token'])->prefix('portal')->name('portal.')->group(function () { ... });

Build & Deployment:

  • Development: composer dev (PHP server + queue worker + logs + Vite HMR)
  • Testing: composer test (Pint + Pest)
  • Production: Laravel Forge push-to-deploy → Nginx + PHP-FPM + Supervisor (queue:work) + Redis

Architecture Validation Results

Coherence Validation

Decision Compatibility: PASS All 16 decisions (D1-D16) are mutually compatible. Redis triple-duty (D4/D5/D7) is standard Laravel production. JSON permissions (D1) + abort(404) works cleanly with Inertia.js. MySQL FULLTEXT (D3) + status-based archive (D2) coexist on the same declarations table. S3 SSE (D6) is transparent to Spatie Media Library. SES (D13) + Redis queue (D5) = standard mail pipeline. No version conflicts across the stack (Laravel 12, Vue 3.5, Inertia.js 2.0, PHP 8.4, MySQL 8.4).

Pattern Consistency: PASS All patterns use workspace_id scoping consistently. All permission checks use abort(404) (never 403). All filters use URL query params (never session). All notifications use Laravel's DatabaseNotification. All bulk operations wrapped in DB::transaction(). Naming conventions consistent: snake_case PHP/DB, camelCase TypeScript/Vue, PascalCase components.

Structure Alignment: PASS Project structure follows existing Laravel conventions. New files organized in the same pattern as existing files. Phase-to-directory mapping is explicit and non-overlapping. Controller traits (HasWorkspaceScope, AuthorizesPermissions) provide shared behavior without service layer complexity.

Requirements Coverage Validation

Functional Requirements (58 FRs) -- All Covered:

FR Group FRs Architectural Support
Workspace & Onboarding FR1-FR6 Existing auth + workspace model. No new architecture needed.
Team & Role Management FR7-FR11 D1 (JSON permissions), AuthorizesPermissions trait, TeamController, Phase 1 structure
Client Management FR12-FR15 Existing ClientController + extended with filters (Phase 4)
Declaration Lifecycle FR16-FR23 DeclarationController, DeclarationObserver, status state machine, D2 (archive)
Dashboard & Visibility FR24-FR28 DashboardController rewrite, dashboard components, D4 (Redis cache), role-scoped queries
Collaboration & Notifications FR29-FR33 D8 (DatabaseNotification), NudgeController, Phase 3 components, D9 (bulk + queue)
Client Portal & Documents FR34-FR40 Existing client portal controllers, token-based access, Spatie Media Library
Search/Filtering/Navigation FR41-FR44 D3 (MySQL FULLTEXT), D11 (URL params), FilterBar component, useFilters composable
Archive System FR45-FR51 D2 (archived_at), ArchiveController, scopeArchived(), D12 (PDF.js preview)
Activity & Audit FR52-FR55 Spatie Activity Log (existing), extended to new models
Platform Administration FR56-FR58 Admin/ controller namespace, EnsureUserIsAdmin middleware

Non-Functional Requirements (28 NFRs) -- All Covered:

NFR Category Key NFRs Architectural Support
Performance (NFR1-6) 2s pages, 10s bulk, 1s search, 3s dashboard SSR via Inertia, Redis cache (D4), FULLTEXT indexes (D3), synchronous bulk (D9)
Security (NFR7-15) TLS, AES-256, tenant isolation, 2FA, CNDP S3 SSE (D6), workspace scoping pattern, Fortify 2FA (existing), EU hosting (D14)
Scalability (NFR16-20) 200 workspaces, 1K users, 500 clients, 5K declarations MySQL indexing, Redis cache, pagination, workspace-scoped queries
Reliability (NFR21-28) 99.5% uptime, 1hr RPO/RTO, >99% email Laravel Forge (D14), SES (D13), Redis queue retries (D5), Sentry + Pulse (D16)

Implementation Readiness Validation

Decision Completeness: PASS -- All 16 decisions have choice, rationale, implementation details, and affected FRs. Technology versions specified. Implementation sequence defined.

Structure Completeness: PASS -- Every new file has a specific location. Phase-to-file mapping is explicit for all 5 phases + pre-phase. Route registration example covers all new endpoints.

Pattern Completeness: PASS -- 12 pattern categories with PHP and Vue code examples. Anti-patterns explicitly listed. 10 enforcement guidelines for AI agents.

Gap Analysis Results

No Critical Gaps Found.

Minor Gaps (non-blocking, addressable during implementation):

  1. Client portal token rotation -- Token security pattern (expiry, single-use, revocation) not detailed. Existing FolderInvitation model handles this; pattern carries over to DeclarationInvitation.
  2. Backup/restore procedure -- D14 mentions Laravel Forge but no specific backup schedule. Recommendation: daily MySQL dump + S3 versioning. Configure during infrastructure provisioning.
  3. Rate limiting -- Not explicitly defined. Laravel's built-in ThrottleRequests middleware (60 req/min default) is sufficient for MVP.
  4. Email template styling -- No pattern defined for email HTML structure. Recommendation: use Laravel's built-in Markdown mailables for consistent styling.

Architecture Completeness Checklist

Requirements Analysis

  • Project context thoroughly analyzed (58 FRs, 28 NFRs, 6 user journeys)
  • Scale and complexity assessed (High complexity, ~15-20 components)
  • Technical constraints identified (brownfield, 12 locked-in dependencies)
  • Cross-cutting concerns mapped (7 concerns)

Architectural Decisions

  • Critical decisions documented with versions (16 decisions, D1-D16)
  • Technology stack fully specified (Laravel 12, Vue 3.5, MySQL 8.4, Redis, S3, SES)
  • Integration patterns defined (Inertia props, notification channels, cache, queue)
  • Performance considerations addressed (Redis cache, FULLTEXT, SSR, pagination)

Implementation Patterns

  • Naming conventions established (snake_case PHP/DB, camelCase TS, PascalCase components)
  • Structure patterns defined (controller traits, model scopes, composables)
  • Communication patterns specified (Inertia router, notification channels, cache invalidation)
  • Process patterns documented (error handling, status transitions, bulk operations)

Project Structure

  • Complete directory structure defined (~60 new files mapped)
  • Component boundaries established (5 boundaries)
  • Integration points mapped (4 external services)
  • Requirements to structure mapping complete (all 5 phases + pre-phase)

Architecture Readiness Assessment

Overall Status: READY FOR IMPLEMENTATION

Confidence Level: High

Key Strengths:

  • Brownfield foundation eliminates boilerplate -- auth, multi-tenancy, file uploads, audit logging already work
  • Every decision leverages Laravel's built-in capabilities (no exotic dependencies)
  • Patterns are concrete with code examples, not abstract guidelines
  • Phase-to-file mapping gives AI agents unambiguous implementation targets
  • 10 enforcement rules prevent the most common cross-agent conflicts

Areas for Future Enhancement (Post-MVP):

  • Soketi WebSocket integration for real-time notification badges
  • Meilisearch migration if search volume exceeds MySQL FULLTEXT capacity
  • Full observability stack (Grafana/Prometheus) beyond Pulse + Sentry
  • API layer if mobile app or third-party integrations are needed

Implementation Handoff

AI Agent Guidelines:

  • Read project-context.md AND this architecture document before implementing any feature
  • Follow all 16 architectural decisions exactly as documented
  • Use implementation patterns consistently across all components
  • Respect project structure and boundaries defined in Project Structure section
  • Follow the implementation sequence: Redis → Permissions → Archive column → FULLTEXT → Notifications → S3 → SES → Hosting → Monitoring

First Implementation Priority: Pre-Phase: "Folders → Declarations" terminology migration across all models, controllers, enums, pages, tests, and database (migration to rename table + columns). This must complete before any Phase 1-5 work begins.