Skip to main content

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

  1. EdgeVerifyOAuthToken validates the Bearer token against identity's /api/user. Result cached for 60s by token hash. Returns 503 identity_unavailable on transient failures so the SPA doesn't force-logout the user when the upstream is sick.
  2. AuthorisationCheckPermission reads auth_user, builds the FQN action (tenant.{tenant}.crm.{verb}), runs three gates: tenant-membership (against tenants[]), service-account scope (if the token has scopes[]), and policy evaluation via PolicyEvaluator. Policies are cached per (tenant, user, token-sig) for 1h with tag-based flush.
  3. 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.
  4. 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).
  5. SupportApp\Support\Search for diacritic-insensitive LIKE, App\Support\TenantClone for the cross-tenant seeding helper (see tenant-clone.md), App\DTO\PolicyDTO + App\Services\PolicyEvaluator for identity policy parsing / deny-overrides evaluation, App\Services\IdentityService for 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 integrations
  • BotWebhookDispatcher → 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_ID env (the source of truth, see environment-reference.md)
  • auth_user['tenants'] JWT claim (cross-checked at CheckPermission)

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

LevelToolLocation
Backend UnitPHPUnittenant/tenant-backend-crm/tests/Unit/
Backend FeaturePHPUnit + Laravel TestCasetenant/tenant-backend-crm/tests/Feature/
Frontend Unit (composables, stores)Vitest + happy-domtenant/tenant-frontend/src/**/*.test.ts
Frontend E2ECypresstenant/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