Skip to content

Architecture ​

Last updated: 2026-09-27 (Phase 8: chat_widgets, widget_clicks, contact_fields.is_system. Phase 6: ai_credentials, ai_model_prices, ai_usage_logs, knowledge_documents, knowledge_chunks, cost_alerts, ai_spend_caps, service_message_counts. Phase 5: bot_flows, bot_flow_versions, bot_flow_runs, bot_flow_run_logs, automation_rules, automation_rule_hits; conversations.automation_paused_until; messages.bot_flow_run_id/automation_rule_id)

1. Runtime model ​

The same codebase has to run in three environments. Every feature has to work in the most limited one, shared cPanel.

ConcernShared cPanel (buyer default and staging)VPS (advanced buyers; a test VPS comes later)
WebApache/LiteSpeed + PHP-FPM, PHP 8.3+Nginx + PHP-FPM 8.3
Queuedatabase driver. Cron runs schedule:run every minute, and the scheduler runs queue:work --stop-when-empty --max-time=50 without overlap (DESORIX_QUEUE_MODE=cron).Supervised queue:work with DESORIX_QUEUE_MODE=worker. Redis + Horizon optional and documented.
WebhooksRaw event stored → 200 returned → processed in defer() after the response. The queue plus a cron sweep retries anything left unprocessed.Same path. The daemon picks up retries immediately.
RealtimePolling: the inbox reloads its data every 5s (admin-configurable). Hosted Pusher optional.Laravel Reverb (supervised process) or Pusher; see installation/realtime.md.
SchedulercPanel cron * * * * * php artisan schedule:run. A heartbeat is shown on Admin → System.System cron.
Storagelocal disk (private) for media and documents. S3-compatible optional.Same.

WhatsApp pipeline (built in Phase 2).

  • app/WhatsApp/HttpGraphApi is the Graph client (version from settings), behind Contracts\GraphApi. FakeGraphApi is used in tests and the demo.
  • Http/Controllers/Webhooks/WhatsAppWebhookController verifies X-Hub-Signature-256 with the account's App Secret, inserts the raw body into webhook_events (unique sha256), answers 200 and defer()s Webhooks/WebhookProcessor.
  • The processor dispatches InboundMessageHandler (contact via BSUID/wa_id → conversation window → message → media job) and StatusHandler (monotonic status, pricing via Pricing/CostEstimator).
  • Outbound goes through Messaging/MessageSender: window check, category, estimate, then send.
  • ConnectionTester backs the wizard's test step.

Inbox (Phase 3).

  • app/Inbox/InboxAccess is the single source of truth for which numbers, and therefore which conversations and websocket channels, a member may use.
  • InboxController renders one Inertia page, inbox/Index, with the list, the selected thread (messages + notes + events, via InboxPresenter) and lazy templates. ConversationController handles replies, templates, media, notes, assignment, status, tags and the contact name.
  • Model saved/created hooks call InboxNotifier. When websockets are configured it broadcasts InboxChanged (conversation ID only) on private-inbox.number.{id}. The page (useInboxLive) then reloads its props: on each nudge with websockets, otherwise on a timer.

Contacts, templates and broadcasts (Phase 4).

  • app/Contacts: Consent (with its log), ConsentKeywords (STOP/START), ContactImporter with the ProcessContactImport job (resumable CSV), SegmentQuery (rules → query), ContactEraser (GDPR).
  • app/WhatsApp/Templates: TemplateBuilder (form ↔ Meta components, with rule checks) and TemplateManager (drafts, submit with sample upload, sync, delete, webhooks).
  • app/Broadcasts: Audience, BroadcastPlanner (confirmation summary + cost), BroadcastMessage (per-recipient variables → components), MessagingLimit, and BroadcastRunner (launch with confirmation check, test send, prepare/send/recount, pause/resume/cancel), driven by desorix:run-broadcasts every minute.

Automation and chatbot flows (Phase 5). See automation.md for the user-facing behaviour.

  • app/Automation/Flows/FlowRuntime walks a graph for one run. It is storage- and channel-agnostic: RunState holds the position, answers and wait, and an Environment does the side effects. FlowGraph reads the Vue Flow JSON ({nodes, edges} with named source handles), Conditions and Placeholders evaluate conditions and fill {{contact.*}} / {{flow.*}}, and FlowValidator checks a graph before it is published.
  • LiveEnvironment is the real channel: MessageSender (so bot messages are costed and carry bot_flow_run_id / automation_rule_id), contact tags, assignment, handoff, WebhookCaller (public addresses only via Support/Http/SafeUrl) and bot_flow_run_logs. SimulatedEnvironment records a transcript instead, for Test flow (FlowSimulator, sessions in the cache for 2 hours).
  • FlowRunner persists runs: starts a published version (replacing any active run), feeds replies, resumes due delays and timeouts from desorix:run-flows (every minute), and cancels. bot_flow_runs.active_conversation_id is unique while a run is live, so the database allows one active run per conversation. A cache lock per conversation serialises webhook replies and cron resumes.
  • AutomationEngine::handleInbound() is called by WebhookProcessor after the inbound message's transaction commits (duplicates never reach it). It applies consent keywords → pause → waiting run (exact keywords interrupt) → keyword / welcome / away rules with per-conversation cooldowns (automation_rule_hits). BusinessHours reads the workspace settings. AutomationPause is called by the inbox after an agent sends and when a conversation is closed.
  • UI: pages/flows/Builder.vue (Vue Flow canvas, components/flows/*: one FlowNode for every type, NodeEditor, TestPanel), pages/flows/Index.vue, pages/flows/Runs.vue, pages/automation/Index.vue, and the Chatbot section of the inbox ContactPanel.

AI, costs and the service-message allowance (Phase 6). See ai-providers.md and costs.md.

  • app/Ai/AiGateway is the only way AI is called. It resolves the key (the workspace's own first, then the platform key), the provider and the model per feature (AiSettings: workspace settings over the admin defaults). It prices the call with AiPricing from ai_model_prices and writes ai_usage_logs, including failures.
  • Providers implement TextProvider / EmbeddingProvider: Providers\AnthropicProvider (official anthropic-ai/sdk, Guzzle transport with real timeouts), Providers\OpenAiProvider (HTTP, OpenAI-compatible base_url), and Providers\FakeProvider (tests, DESORIX_AI_FAKE).
  • Ai\Assistant\AssistantPrompt builds the business-scoped prompt (profile + passages + recent turns, ending on a user turn). AutoReplier is called by AutomationEngine as the last step, with the spend cap (SpendCap) and loop guard first. DraftWriter serves the composer's Draft reply.
  • Knowledge: TextExtractor (PDF via smalot/pdfparser, .docx via ZipArchive, text), Chunker (~1,200-character passages with overlap), the IndexKnowledgeDocument job, and KnowledgeSearch (streamed cosine over packed float32 vectors that record their model and dimensions, a FULLTEXT pre-filter above 3,000 chunks, keyword fallback).
  • Pricing\ServiceMessageCounter counts delivered service messages per number per billing month (service_message_counts). StatusHandler increments it once per message, claimed with a conditional update. CostEstimator uses it for the free allowance, and backfill() recounts the current month on upgrade.
  • Costs\CostReport aggregates messages and ai_usage_logs live per workspace month. Costs\Money handles the pricing currency, USD and the admin rate. CostAlertChecker runs from desorix:check-cost-alerts hourly.
  • UI: pages/ai/Settings.vue, pages/costs/Index.vue, pages/admin/Ai.vue, the composer's Draft reply, and a single date formatter in lib/dates.ts.

Tenancy. Workspace routes are prefixed with the workspace slug (/{current_workspace}/…). EnsureWorkspaceMembership checks membership, and a minimum role when one is given, then switches users.current_workspace_id. From Phase 2, every tenant model uses a BelongsToWorkspace trait: a global scope on workspace_id that is auto-filled on create from the route's workspace (API: the token's workspace). Roles live on workspace_members.role. WorkspaceRole::permissions() maps each role to WorkspacePermission cases, and policies check permissions. Admin routes (/admin) require users.is_super_admin and bypass the scope explicitly.

WhatsApp pipeline.

mermaid
flowchart LR
  Meta[(Meta Cloud API)] -- POST /webhooks/whatsapp/{account uuid} --> WH[WebhookController<br/>verify HMAC with app secret]
  WH --> WE[(webhook_events<br/>unique payload hash)]
  WE -- defer() / job --> P[WebhookProcessor]
  P --> M[(messages)]
  P --> S[status updater<br/>monotonic ranks]
  P --> T[template status sync]
  P --> MD[DownloadMedia job]
  M --> C[(conversations<br/>window_expires_at)]
  C --> RT[Broadcast event → Reverb / polling]
  C --> AUTO[Automation: rules → bot flow runtime → AI]
  AUTO --> SEND[MessageSender]
  SEND --> G[GraphClient v26.0]
  G --> Meta
  SEND --> COST[CostEstimator<br/>rate table × category × country]
  S --> COSTF[Finalize cost on delivered<br/>using pricing.billable/category]

2. Folder structure ​

app/
  Actions/                Fortify + workspace actions (from the starter kit)
  Concerns/               HasWorkspaces, validation rule sets
  Enums/                  WorkspaceRole, WorkspacePermission, later MessageStatus, PricingCategory, ...
  Http/
    Controllers/
      App/                workspace UI (Inertia)
      Admin/              super-admin UI
      Api/V1/             public REST API (Sanctum)
      Webhooks/           WhatsApp, generic inbound (WooCommerce is a module)
      Widget/             website chat widget settings and click beacon (Phase 8)
      InstallController   web installer (Blade, runs before a DB exists)
    Middleware/           EnsureWorkspaceMembership, EnsureInstalled, EnsureSuperAdmin
    Requests/
  Models/
  Policies/
  Jobs/  Events/  Listeners/  Notifications/  Console/Commands/
  Support/
    Tenancy/              BelongsToWorkspace trait (Phase 2)
    Settings/             Settings (global key/value, cached)
    System/               RequirementsChecker, Heartbeat, CronLine
    Install/              Installation (installed.json), EnvWriter, DatabaseProbe, InstallRunner
    Updates/              ReleaseVerifier, UpdateServer, LicenseManager, UpdateChecker,
                          Updater, FinishUpdate, DatabaseDumper, Maintenance
    Routing/              WorkspaceSlug (the workspace URL pattern)
  Modules/                ModuleManager, ModuleManifest, ModuleServiceProvider (base class)
    Help/                 HelpArticles (resources/help/{locale}/*.md)
    Locales.php           supported languages
  WhatsApp/               GraphClient, WebhookProcessor, MessageSender, Templates, Media, FakeGraphClient
  Pricing/                CostEstimator, RateTable, CountryResolver
  Inbox/                  WindowCalculator, Assignment
  Automation/             Rules, BotFlows/Runtime, BotFlows/Nodes/*
  Ai/                     Contracts/{TextProvider,EmbeddingProvider}, Drivers/{Anthropic,OpenAi}, Knowledge/
  Billing/                Plans, Credits, PayPal driver
  Integrations/           OutboundWebhooks (Phase 8)
modules/                  add-ons (D-115): modules/Woocommerce/{module.json,src,database,routes,lang,resources/js,dist}
bootstrap/  config/  database/{migrations,factories,seeders}
lang/en/*.php, lang/en.json   (single source for PHP and Vue strings)
resources/
  js/
    pages/{app,admin,install,help,auth}/
    components/{ui,inbox,flows,...}
    layouts/  composables/  stores/ (Pinia)  types/  lib/
  css/app.css
routes/ web.php admin.php api.php webhooks.php install.php channels.php console.php
tests/ Feature/ Unit/ (Pest)   resources/js/**/*.spec.ts (Vitest)
docs/                          VitePress site (from Phase 1)
resources/help/{en,ar}/*.md    in-app help articles
scripts/lang-sync.mjs          translation key extraction / CI check (core and modules)
scripts/build-modules.mjs      module pages → modules/*/dist (IIFE + scoped CSS)
scripts/release.sh             make release: signed install/update zip + CodeCanyon package
scripts/release-key.php, release-sign.php, module-release.sh
deploy/                        cPanel deploy scripts (staging); deploy/release/ root forwarder
resources/views/install/       the installer's Blade pages
.github/workflows/ci.yml

The code is organised by feature (app/WhatsApp, app/Automation, ...) for domain logic. The usual Laravel layers (Http, Models, Jobs) stay in their standard places, so a buyer's developer finds things where they expect.

3. ERD (proposed) ​

Conventions: every table has id (bigint) and timestamps. Tables marked T carry workspace_id (FK, indexed first in composite indexes). Secrets use Laravel's encrypted cast. Money is decimal(12,6) with a currency column.

3.1 Tenancy, WhatsApp core, inbox ​

mermaid
erDiagram
  users ||--o{ workspace_members : "member of"
  workspaces ||--o{ workspace_members : has
  workspaces ||--o{ workspace_invitations : has
  workspaces ||--o{ whatsapp_accounts : owns
  whatsapp_accounts ||--o{ phone_numbers : has
  whatsapp_accounts ||--o{ message_templates : has
  whatsapp_accounts ||--o{ webhook_events : receives
  workspaces ||--o{ contacts : has
  workspaces ||--o{ chat_widgets : has
  chat_widgets ||--o{ widget_clicks : records
  widget_clicks }o--o| conversations : started
  phone_numbers ||--o{ conversations : has
  contacts ||--o{ conversations : in
  contacts ||--o{ contact_identities : "BSUIDs"
  conversations ||--o{ messages : contains
  conversations ||--o{ conversation_notes : has
  messages ||--o| media_files : attaches
  users ||--o{ conversations : "assigned to"
  users ||--o{ messages : "sent by"
  message_templates ||--o{ messages : uses
  tags ||--o{ taggables : applied
  workspaces ||--o{ canned_replies : has
  workspaces ||--o{ contact_fields : defines

  users {
    string name
    string email UK
    string password
    bool is_super_admin
    string locale
    bigint current_workspace_id
  }
  workspaces {
    string name
    string slug UK "used in URLs"
    bool is_personal
    string timezone
    json settings "business hours, AI mode, alert thresholds"
    bigint plan_id "Phase 8"
    timestamp deleted_at
  }
  workspace_members {
    bigint workspace_id FK
    bigint user_id FK "UK with workspace_id"
    string role "owner admin agent viewer"
  }
  whatsapp_accounts {
    bigint workspace_id FK "T"
    char uuid UK "used in webhook URL"
    string waba_id
    string business_portfolio_id
    string app_id
    text app_secret "encrypted"
    text access_token "encrypted"
    string verify_token
    string messaging_limit "250 2K 10K 100K unlimited"
    timestamp last_tested_at
  }
  phone_numbers {
    bigint workspace_id FK "T"
    bigint whatsapp_account_id FK
    string phone_number_id UK
    string display_phone
    string verified_name
    string quality_rating
    int send_rate_per_second
  }
  webhook_events {
    bigint whatsapp_account_id FK
    char payload_hash UK "sha256"
    json payload
    enum state "pending processed failed"
    int attempts
    timestamp processed_at
  }
  contact_identities {
    bigint workspace_id FK "T"
    bigint contact_id FK
    string business_portfolio_id
    string user_id "BSUID, UK with workspace_id"
    string parent_user_id
  }
  contacts {
    bigint workspace_id FK "T"
    string wa_id "UK with workspace_id, nullable"
    string phone_e164 "nullable"
    string username
    string name
    char country "ISO2"
    enum opt_in "unknown opted_in opted_out"
    timestamp opt_in_at
    string opt_in_source
    json attributes "custom field values"
    timestamp anonymized_at
  }
  conversations {
    bigint workspace_id FK "T"
    bigint phone_number_id FK
    bigint contact_id FK "UK with phone_number_id"
    enum status "open pending closed"
    bigint assigned_user_id
    timestamp last_inbound_at
    timestamp window_expires_at
    timestamp last_message_at
    int unread_count
    timestamp automation_paused_until
  }
  messages {
    bigint workspace_id FK "T"
    bigint conversation_id FK
    bigint phone_number_id FK
    bigint contact_id
    string wamid UK
    enum direction "in out"
    string type "text image template interactive ..."
    text body
    json payload
    enum status "pending sent delivered read failed"
    timestamp sent_at
    timestamp delivered_at
    timestamp read_at
    timestamp failed_at
    json error
    enum sender_type "user bot ai system api broadcast"
    bigint sender_user_id
    bigint template_id
    bigint broadcast_id
    enum pricing_category "marketing utility authentication authentication_international service"
    bool billable
    string pricing_type "regular free_customer_service free_entry_point"
    char destination_country "ISO2"
    string pricing_market
    decimal estimated_cost
    decimal cost "final after delivered"
    char currency
  }
  media_files {
    bigint workspace_id FK "T"
    bigint message_id FK
    string meta_media_id
    string disk
    string path
    string mime
    int size
    enum state "pending stored failed"
  }
  conversation_notes {
    bigint workspace_id FK "T"
    bigint conversation_id FK
    bigint user_id FK
    text body
  }
  tags {
    bigint workspace_id FK "T"
    string name
    string color
  }
  taggables {
    bigint tag_id FK
    string taggable_type "contact conversation"
    bigint taggable_id
  }
  canned_replies {
    bigint workspace_id FK "T"
    string shortcut
    text body
  }
  contact_fields {
    bigint workspace_id FK "T"
    string key
    string label
    enum type "text number date bool select"
    json options
    bool is_system "Phase 8: widget source fields"
  }
  chat_widgets {
    bigint workspace_id FK "T"
    char public_key UK "wk_..."
    bigint phone_number_id FK "nullable"
    bool enabled
    json settings "look, texts, labels, tag, assignee"
    json allowed_domains
  }
  widget_clicks {
    bigint workspace_id FK "T"
    bigint chat_widget_id FK
    char ref "unique per workspace"
    string page_url "no query string"
    string referrer_host
    string utm_source_medium_campaign_term_content
    string origin "beacon | message"
    bigint conversation_id FK "nullable"
    bigint contact_id FK "nullable"
    timestamp matched_at
  }
  message_templates {
    bigint workspace_id FK "T"
    bigint whatsapp_account_id FK
    string meta_template_id
    string name
    string language
    enum category "marketing utility authentication"
    string status "APPROVED PENDING REJECTED PAUSED DISABLED"
    json components
    string rejected_reason
    string quality_score
  }
  workspace_invitations {
    bigint workspace_id FK "T"
    string code UK
    string email
    string role
    bigint invited_by FK
    timestamp expires_at
    timestamp accepted_at
  }

3.2 Broadcasts, automation, AI, cost, integrations, admin ​

mermaid
erDiagram
  segments ||--o{ broadcasts : targets
  message_templates ||--o{ broadcasts : uses
  broadcasts ||--o{ broadcast_recipients : has
  contacts ||--o{ broadcast_recipients : is
  broadcast_recipients ||--o| messages : produced
  automation_rules }o--o| bot_flows : starts
  automation_rules ||--o{ automation_rule_hits : cooldown
  bot_flows ||--o{ bot_flow_versions : publishes
  bot_flow_versions ||--o{ bot_flow_runs : "pinned to"
  bot_flow_runs ||--o{ bot_flow_run_logs : trace
  contacts ||--o{ bot_flow_runs : "per-contact state"
  knowledge_documents ||--o{ knowledge_chunks : split
  ai_credentials ||--o{ ai_usage_logs : bills
  phone_numbers ||--o{ service_message_counts : "free allowance"
  workspaces ||--o{ cost_alerts : ""
  workspaces ||--o| ai_spend_caps : ""
  pricing_country_markets }o--|| pricing_rates : "market lookup"
  plans ||--o{ workspaces : subscribed
  workspaces ||--o{ credit_transactions : ledger
  webhook_endpoints ||--o{ webhook_deliveries : sends

  segments {
    bigint workspace_id FK "T"
    string name
    json rules "tag/field/opt-in conditions"
  }
  broadcasts {
    bigint workspace_id FK "T"
    bigint phone_number_id FK
    bigint template_id FK
    bigint segment_id FK
    json variable_map
    timestamp scheduled_at
    enum status "draft scheduled sending paused done cancelled"
    int total
    int sent
    int delivered
    int read
    int failed
    decimal estimated_cost
    decimal actual_cost
  }
  broadcast_recipients {
    bigint broadcast_id FK
    bigint contact_id FK
    bigint message_id
    enum status "queued sent delivered read failed skipped"
    string error_code
  }
  automation_rules {
    bigint workspace_id FK "T"
    string trigger "keyword welcome away"
    json config "keywords + match | returning_after_days"
    json phone_number_ids "null = all"
    string action "reply start_flow"
    json reply "text, media_url, media_type"
    bigint bot_flow_id FK
    int cooldown_minutes
    int priority
    bool is_active
  }
  automation_rule_hits {
    bigint automation_rule_id FK
    bigint conversation_id FK "UK with rule"
    timestamp last_fired_at
  }
  bot_flows {
    bigint workspace_id FK "T"
    string name
    json draft_graph "vue-flow nodes/edges"
    bigint published_version_id FK
    bool is_active
  }
  bot_flow_versions {
    bigint bot_flow_id FK
    int version "UK with flow"
    json graph "frozen"
    timestamp published_at
  }
  bot_flow_runs {
    bigint workspace_id FK "T"
    bigint bot_flow_id FK
    bigint bot_flow_version_id FK
    bigint conversation_id FK
    bigint contact_id FK
    bigint active_conversation_id "UK, null when ended"
    bigint automation_rule_id FK
    string status "running waiting_input waiting_delay completed handed_off cancelled failed expired"
    string current_node_id
    json variables
    json awaiting
    timestamp resume_at
    string end_reason
  }
  bot_flow_run_logs {
    bigint bot_flow_run_id FK
    string node_id
    string event "entered sent answer branch webhook error ended"
    json data
  }
  ai_credentials {
    bigint workspace_id "null = platform key"
    string provider "anthropic openai"
    text api_key "encrypted"
    string base_url "OpenAI-compatible, optional"
    timestamp last_verified_at
  }
  ai_model_prices {
    string provider
    string model
    decimal input_per_mtok
    decimal output_per_mtok
    decimal cache_write_per_mtok
    decimal cache_read_per_mtok
    date effective_from
  }
  ai_usage_logs {
    bigint workspace_id FK "T"
    string key_source "platform workspace"
    string provider
    string model
    string feature "draft auto_reply embedding test"
    bigint conversation_id
    bigint message_id "AI reply or answered message"
    bigint user_id
    int input_tokens
    int output_tokens
    decimal cost "USD"
    decimal confidence
    string outcome "replied handed_off drafted draft_used capped loop_guard error"
  }
  knowledge_documents {
    bigint workspace_id FK "T"
    string title
    string status "pending processing indexed failed"
    string embedding_model
    int embedding_dimensions
  }
  knowledge_chunks {
    bigint workspace_id FK "T"
    bigint knowledge_document_id FK
    text content "FULLTEXT"
    binary embedding "packed float32"
    string embedding_model
    int embedding_dimensions
  }
  cost_alerts {
    bigint workspace_id FK "T"
    string metric "total whatsapp ai whatsapp:category"
    string basis "actual forecast"
    decimal threshold
    char last_notified_period
  }
  ai_spend_caps {
    bigint workspace_id FK "UK"
    decimal monthly_limit
    string scope "auto_reply all_ai"
    char paused_period
  }
  service_message_counts {
    bigint phone_number_id FK
    char period "UK with number"
    int delivered
  }
  pricing_rates {
    string market
    enum category
    decimal rate
    char currency
    date effective_from
  }
  pricing_country_markets {
    char country UK "ISO2"
    string market
  }
  cost_alerts {
    bigint workspace_id FK "T"
    decimal monthly_threshold
    json recipients
    timestamp last_triggered_at
  }
  plans {
    string name
    json limits "numbers seats contacts monthly_sends"
    int ai_credits_monthly
    string stripe_price_id
    string paypal_plan_id
  }
  credit_transactions {
    bigint workspace_id FK "T"
    int amount
    string reason
    string reference
  }
  licenses {
    string product UK "desorix or a module slug"
    text purchase_code "encrypted"
    string code_hint
    string activation_id
    text token "encrypted"
    string status "unverified active invalid revoked"
    timestamp supported_until
    timestamp last_checked_at
  }
  modules {
    string slug UK
    string version
    string source "bundled uploaded"
    bool enabled
  }
  update_runs {
    string target "core or module slug"
    string from_version
    string to_version
    string source "server upload"
    string status
    json cursor
    string backup_path
    string db_backup_path
    bigint started_by FK
  }
  woo_stores {
    bigint workspace_id FK "T (WooCommerce module)"
    uuid uuid UK
    text webhook_secret "encrypted"
    bigint phone_number_id FK
    string consent_mode
  }
  woo_notification_rules {
    bigint woo_store_id FK
    string order_status "unique per store"
    bigint message_template_id FK
    json variables
  }
  woo_order_events {
    bigint woo_store_id FK
    bigint wc_order_id
    string status "unique store+order+status"
    string result
    bigint message_id FK
  }
  webhook_endpoints {
    bigint workspace_id FK "T"
    string url
    text secret "encrypted"
    json events
  }
  webhook_deliveries {
    bigint webhook_endpoint_id FK
    string event
    int response_code
    int attempts
  }
  settings {
    string key UK
    json value
  }
  announcements {
    string title
    text body
    timestamp starts_at
    timestamp ends_at
  }
  email_templates {
    string key UK
    string locale
    string subject
    text body
  }
  audit_logs {
    bigint workspace_id "nullable"
    bigint user_id
    string action
    json context
  }

These two diagrams leave out tables owned by packages: sessions, cache, jobs, failed_jobs, personal_access_tokens (Sanctum) and Cashier's subscriptions / subscription_items. Cashier's billable model is Workspace, not User.

Indexes that matter from day one

  • messages(conversation_id, id) for the inbox thread.
  • messages(workspace_id, created_at) and messages(workspace_id, pricing_category, created_at) for cost reports.
  • conversations(workspace_id, phone_number_id, status, last_message_at).
  • contacts(workspace_id, phone_e164) unique.

Phase 6 adds a cost_daily_rollups table if the reports get slow on raw messages.

4. Dependencies (proposed; each is added in the phase that needs it) ​

PackageWhy
inertiajs/inertia-laravel, @inertiajs/vue3The Inertia + Vue stack the brief requires.
laravel/fortifyHeadless auth (login, 2FA, reset) used by the official Vue starter kit.
laravel/sanctumTokens for the public API.
laravel/reverb, laravel-echo, @laravel/echo-vue, pusher-jsFirst-party websockets. Echo works with both Reverb and Pusher. Added in Phase 3; Guzzle is pinned to 7.x as a result (D-058).
laravel/cashierStripe subscriptions for resale plans.
laravel/wayfinderTyped route helpers for TS. Ships with the starter kit.
giggsey/libphonenumber-for-php-liteE.164 parsing and country detection, needed for costing. Added in Phase 2 (lite build: no geocoder/carrier data, smaller zip).
league/csvStreaming CSV import/export without memory blow-ups on shared hosts.
@vue-flow/core (+background, controls, minimap)Flow builder canvas (required by the brief).
piniaState management (required by the brief).
laravel-vue-i18nLets Vue read Laravel's lang/ files, so all strings live in one place.
reka-ui, lucide-vue-next, @vueuse/coreAccessible headless components, icons and utilities used by the starter kit.
dev: pestphp/pest, larastan/larastan, laravel/pint, laravel/boostTests, PHPStan level 6, code style, AI-agent guidelines (dev only).
dev: vitest, @vue/test-utils, jsdom, eslint (+ typescript-eslint, eslint-plugin-vue), prettier (+ tailwind plugin), vue-tscFrontend tests, lint, formatting and type-check.
docs: vitepressDocs site (required by the brief).
laravel/passkeys, vue-input-otp, vue-sonner, class-variance-authority, tailwind-merge, clsx, tw-animate-cssStarter-kit UI plumbing: passkeys, OTP input, toasts, class utilities, animations.

No WhatsApp SDK, no AI SDK and no PayPal SDK: we use thin in-house clients on Laravel's Http facade. That means fewer transitive dependencies to ship in the buyer zip.