CRM architecture
┌─────────────────┐
│ tenant-frontend │ (Vue 3 + Vuetify + Pinia)
│ CRM modules │ src/modules/{crm,tasks,documents,hr,...}
└────────┬─────────┘
│ Bearer JWT (from identity)
▼
┌───────────────────────────────────────────────────────────┐
│ tenant-backend-crm │
│ Laravel 12 + PHP 8.4 │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
│ │ VerifyOAuthToken │→ │ CheckPermission │ │
│ │ (calls /api/user │ │ (policy gate, scope-gate, │ │
│ │ on identity) │ │ tenants[] membership) │ │
│ └─────────────────────┘ └──────────┬───────────────┘ │
│ │ │
│ ┌───────────────────┐ ┌───────────▼─────────────────┐ │
│ │ Per-resource │ │ Controllers (split into │ │
│ │ Policies │ │ ChatController + │ │
│ │ (BasePolicy + │ │ ChatFileController + │ │
│ │ TaskPolicy) │ │ ChatParticipantController +│ │
│ └───────────────────┘ │ ChatBotMembershipController│ │
│ │ ChatReactionController …) │ │
│ └─────────────────────────────┘ │
└────┬───────────────┬────────────────┬───────────┬────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────┐
│PostgreSQL│ │ Redis │ │ RabbitMQ │ │ Filesys/ │
│ (single │ │ (cache, │ │ (vecton. │ │ S3 (chat │
│ tenant) │ │ policy) │ │ events + │ │ files, │
│ │ │ │ │ webhooks) │ │ docs) │
└──────────┘ └──────────┘ └──────────────┘ └──────────┘
Layering
- Edge —
VerifyOAuthTokenvalidates the Bearer token against identity's/api/user. Result cached for 60s by token hash. Returns 503identity_unavailableon transient failures so the SPA doesn't force-logout the user when the upstream is sick. - Authorisation —
CheckPermissionreadsauth_user, builds the FQN action (tenant.{tenant}.crm.{verb}), runs three gates: tenant-membership (againsttenants[]), service-account scope (if the token hasscopes[]), and policy evaluation viaPolicyEvaluator. Policies are cached per (tenant, user, token-sig) for 1h with tag-based flush. - Controllers — thin, validation + delegation. Some heavy
controllers are being split into per-resource sub-controllers (F3 —
chat done, task pending).
BaseController::auditActor()is the canonical helper for audit-trail enrichment. - Models — Eloquent with
HasUuids(all entities),SoftDeletes(Task/Contact/Company/Document/ProjectMember/...), and per-domain state-machines exposed as model constants (LeaveRequest::TRANSITIONS,TaskHistory::ALLOWED_ACTIONS,CommunicationLog::ALLOWED_RELATED_TYPES,ProjectMember::ALLOWED_ROLES). - Support —
App\Support\Searchfor diacritic-insensitive LIKE,App\Support\TenantClonefor the cross-tenant seeding helper (see tenant-clone.md),App\DTO\PolicyDTO+App\Services\PolicyEvaluatorfor identity policy parsing / deny-overrides evaluation,App\Services\IdentityServicefor outbound identity HTTP, dedicated service classes per integration (SmsSendService,EventPublisher,WebhookEventPublisher).
Authentication path
[SPA] ──── Bearer access_token ────▶ [CRM /api/...]
│
├─ VerifyOAuthToken
│ │
│ ├─ Cache lookup identity.user:{sha256(token)} (60s)
│ │ miss ─▶ GET identity /api/user
│ │ → cache + auth_user
│ │
│ └─ merge auth_user into Request
│
├─ CheckPermission(action)
│ │
│ ├─ admin bypass (is_system_admin)
│ ├─ tenants[] membership check
│ ├─ service-account scope-gate
│ │ (segment-by-segment glob)
│ └─ policy fetch + PolicyEvaluator
│ cache key: policies:{tid}:{uid}:{sig}
│
└─ Controller method
Event-driven integration
CRM is a consumer-only for cross-cluster identity events on the
vecton.events RabbitMQ exchange. See events.md for
the 5 consumed event types and their handlers.
CRM emits internal events to:
EventPublisher→ WebSocket fan-out for SPA real-time updates (chat.message.new,chat.reaction.updated, etc.)WebhookEventPublisher→ outbound HTTP webhooks for tenant-configured integrationsBotWebhookDispatcher→ per-channel bot webhook dispatch
See webhook-events.md for the full event catalogue + payload shapes.
Per-tenant deployment isolation
Each tenant gets a full deployment: own DB, own Redis namespace, own
RabbitMQ queue. There is no tenant_id discriminator column on CRM
tables — the column was dropped (2026-05-12) under the per-deployment
model. The remaining tenant binding lives in:
TENANT_IDenv (the source of truth, see environment-reference.md)auth_user['tenants']JWT claim (cross-checked atCheckPermission)
See multi-tenant.md for the full discussion.
Frontend layering
pages/companies/index.vue
│
├─ useCompanyStore (Pinia)
│ │
│ ├─ useApiError(error) ← composable, 11 stores use it
│ │ ├─ notifyError → useNotificationStore().error()
│ │ └─ notifySuccess
│ │
│ └─ crmClient.get/post/put/delete
│ └─ src/lib/axios (interceptor: 401 logout, 503 wake-up overlay)
│
└─ useDeleteDialog<string>(deleter) ← composable, 2 list pages
├─ isOpen / target / submitting
└─ open / close / confirm
Generated SDK calls (@hey-api/openapi-ts) are used where the store
exists (Blog, Page, SmsTemplate, Webhook). Stores predating the
generated SDK era still use crmClient.get({ url: ... }). The api:sync
- api:check workflow keeps the generated layer current.
Testing layers
| Level | Tool | Location |
|---|---|---|
| Backend Unit | PHPUnit | tenant/tenant-backend-crm/tests/Unit/ |
| Backend Feature | PHPUnit + Laravel TestCase | tenant/tenant-backend-crm/tests/Feature/ |
| Frontend Unit (composables, stores) | Vitest + happy-dom | tenant/tenant-frontend/src/**/*.test.ts |
| Frontend E2E | Cypress | tenant/tenant-frontend/cypress/ |
Vitest scaffold landed in the F8 batch (vitest.config.ts,
tests/setup.ts, first composable tests). The user installs the dev
deps once with npm i -D vitest @vitest/ui happy-dom @vue/test-utils.
Where things live
tenant/tenant-backend-crm/
├── app/
│ ├── Http/
│ │ ├── Controllers/ ← thin
│ │ ├── Middleware/ ← VerifyOAuthToken, CheckPermission, VerifySystemToken, VerifyBotToken
│ │ ├── Requests/ ← FormRequest validators (StoreMediaRequest, ...)
│ │ └── Resources/ ← API Resource serialization
│ ├── Models/ ← Eloquent
│ ├── Policies/Crm/ ← BasePolicy + per-resource children
│ ├── Services/ ← IdentityService, EventPublisher, IdentityEventHandler, ...
│ ├── Support/Search.php ← diacritic-insensitive LIKE
│ ├── Jobs/ ← CheckSlaBreachesJob, ...
│ └── Console/Commands/ ← crm:list-permissions, crm:consume-identity-events
└── database/migrations/
tenant/tenant-frontend/src/
├── api/generated/crm/ ← @hey-api/openapi-ts output (DO NOT EDIT)
├── composables/ ← useApiError, useDeleteDialog
├── modules/crm/stores/ ← contact, company, contactTag, ...
├── modules/hr/stores/ ← project, timeEntry, shift, leaveRequest, leaveType
├── modules/tasks/stores/ ← task, taskLabel, taskView
├── modules/content/stores/ ← blog, page, smsTemplate
└── plugins/i18n/locales/ ← hu/en/de JSON