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):**
| 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:
| 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).
- 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.
- **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."
- **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.
- **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.
- **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.
- **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.
- **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()`.
- **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.
- **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.
- **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.
- **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)
- **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
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.
**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
│ ├── 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/` |
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 |
**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.
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.