Appearance
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.
| Concern | Shared cPanel (buyer default and staging) | VPS (advanced buyers; a test VPS comes later) |
|---|---|---|
| Web | Apache/LiteSpeed + PHP-FPM, PHP 8.3+ | Nginx + PHP-FPM 8.3 |
| Queue | database 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. |
| Webhooks | Raw 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. |
| Realtime | Polling: the inbox reloads its data every 5s (admin-configurable). Hosted Pusher optional. | Laravel Reverb (supervised process) or Pusher; see installation/realtime.md. |
| Scheduler | cPanel cron * * * * * php artisan schedule:run. A heartbeat is shown on Admin → System. | System cron. |
| Storage | local disk (private) for media and documents. S3-compatible optional. | Same. |
WhatsApp pipeline (built in Phase 2).
app/WhatsApp/HttpGraphApiis the Graph client (version from settings), behindContracts\GraphApi.FakeGraphApiis used in tests and the demo.Http/Controllers/Webhooks/WhatsAppWebhookControllerverifiesX-Hub-Signature-256with the account's App Secret, inserts the raw body intowebhook_events(unique sha256), answers 200 anddefer()sWebhooks/WebhookProcessor.- The processor dispatches
InboundMessageHandler(contact via BSUID/wa_id → conversation window → message → media job) andStatusHandler(monotonic status, pricing viaPricing/CostEstimator). - Outbound goes through
Messaging/MessageSender: window check, category, estimate, then send. ConnectionTesterbacks the wizard's test step.
Inbox (Phase 3).
app/Inbox/InboxAccessis the single source of truth for which numbers, and therefore which conversations and websocket channels, a member may use.InboxControllerrenders one Inertia page,inbox/Index, with the list, the selected thread (messages + notes + events, viaInboxPresenter) and lazy templates.ConversationControllerhandles replies, templates, media, notes, assignment, status, tags and the contact name.- Model
saved/createdhooks callInboxNotifier. When websockets are configured it broadcastsInboxChanged(conversation ID only) onprivate-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),ContactImporterwith theProcessContactImportjob (resumable CSV),SegmentQuery(rules → query),ContactEraser(GDPR).app/WhatsApp/Templates:TemplateBuilder(form ↔ Meta components, with rule checks) andTemplateManager(drafts, submit with sample upload, sync, delete, webhooks).app/Broadcasts:Audience,BroadcastPlanner(confirmation summary + cost),BroadcastMessage(per-recipient variables → components),MessagingLimit, andBroadcastRunner(launch with confirmation check, test send, prepare/send/recount, pause/resume/cancel), driven bydesorix:run-broadcastsevery minute.
Automation and chatbot flows (Phase 5). See automation.md for the user-facing behaviour.
app/Automation/Flows/FlowRuntimewalks a graph for one run. It is storage- and channel-agnostic:RunStateholds the position, answers and wait, and anEnvironmentdoes the side effects.FlowGraphreads the Vue Flow JSON ({nodes, edges}with named source handles),ConditionsandPlaceholdersevaluate conditions and fill{{contact.*}}/{{flow.*}}, andFlowValidatorchecks a graph before it is published.LiveEnvironmentis the real channel:MessageSender(so bot messages are costed and carrybot_flow_run_id/automation_rule_id), contact tags, assignment, handoff,WebhookCaller(public addresses only viaSupport/Http/SafeUrl) andbot_flow_run_logs.SimulatedEnvironmentrecords a transcript instead, for Test flow (FlowSimulator, sessions in the cache for 2 hours).FlowRunnerpersists runs: starts a published version (replacing any active run), feeds replies, resumes due delays and timeouts fromdesorix:run-flows(every minute), and cancels.bot_flow_runs.active_conversation_idis 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 byWebhookProcessorafter 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).BusinessHoursreads the workspace settings.AutomationPauseis called by the inbox after an agent sends and when a conversation is closed.- UI:
pages/flows/Builder.vue(Vue Flow canvas,components/flows/*: oneFlowNodefor every type,NodeEditor,TestPanel),pages/flows/Index.vue,pages/flows/Runs.vue,pages/automation/Index.vue, and the Chatbot section of the inboxContactPanel.
AI, costs and the service-message allowance (Phase 6). See ai-providers.md and costs.md.
app/Ai/AiGatewayis 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 withAiPricingfromai_model_pricesand writesai_usage_logs, including failures.- Providers implement
TextProvider/EmbeddingProvider:Providers\AnthropicProvider(officialanthropic-ai/sdk, Guzzle transport with real timeouts),Providers\OpenAiProvider(HTTP, OpenAI-compatiblebase_url), andProviders\FakeProvider(tests,DESORIX_AI_FAKE). Ai\Assistant\AssistantPromptbuilds the business-scoped prompt (profile + passages + recent turns, ending on a user turn).AutoReplieris called byAutomationEngineas the last step, with the spend cap (SpendCap) and loop guard first.DraftWriterserves the composer's Draft reply.- Knowledge:
TextExtractor(PDF via smalot/pdfparser, .docx via ZipArchive, text),Chunker(~1,200-character passages with overlap), theIndexKnowledgeDocumentjob, andKnowledgeSearch(streamed cosine over packed float32 vectors that record their model and dimensions, a FULLTEXT pre-filter above 3,000 chunks, keyword fallback). Pricing\ServiceMessageCountercounts delivered service messages per number per billing month (service_message_counts).StatusHandlerincrements it once per message, claimed with a conditional update.CostEstimatoruses it for the free allowance, andbackfill()recounts the current month on upgrade.Costs\CostReportaggregatesmessagesandai_usage_logslive per workspace month.Costs\Moneyhandles the pricing currency, USD and the admin rate.CostAlertCheckerruns fromdesorix:check-cost-alertshourly.- UI:
pages/ai/Settings.vue,pages/costs/Index.vue,pages/admin/Ai.vue, the composer's Draft reply, and a single date formatter inlib/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.ymlThe 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)andmessages(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)
| Package | Why |
|---|---|
| inertiajs/inertia-laravel, @inertiajs/vue3 | The Inertia + Vue stack the brief requires. |
| laravel/fortify | Headless auth (login, 2FA, reset) used by the official Vue starter kit. |
| laravel/sanctum | Tokens for the public API. |
| laravel/reverb, laravel-echo, @laravel/echo-vue, pusher-js | First-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/cashier | Stripe subscriptions for resale plans. |
| laravel/wayfinder | Typed route helpers for TS. Ships with the starter kit. |
| giggsey/libphonenumber-for-php-lite | E.164 parsing and country detection, needed for costing. Added in Phase 2 (lite build: no geocoder/carrier data, smaller zip). |
| league/csv | Streaming CSV import/export without memory blow-ups on shared hosts. |
| @vue-flow/core (+background, controls, minimap) | Flow builder canvas (required by the brief). |
| pinia | State management (required by the brief). |
| laravel-vue-i18n | Lets Vue read Laravel's lang/ files, so all strings live in one place. |
| reka-ui, lucide-vue-next, @vueuse/core | Accessible headless components, icons and utilities used by the starter kit. |
| dev: pestphp/pest, larastan/larastan, laravel/pint, laravel/boost | Tests, 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-tsc | Frontend tests, lint, formatting and type-check. |
| docs: vitepress | Docs site (required by the brief). |
| laravel/passkeys, vue-input-otp, vue-sonner, class-variance-authority, tailwind-merge, clsx, tw-animate-css | Starter-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.