Technical Specification

Complete implementation reference for the Ready1Go WhatsApp AI Agent platform — packages, architecture, data models, API surface, and module wiring.

PHP 8.2 · Laravel 12 MariaDB 10.11 pgvector (PostgreSQL) Apache 2.4 · mod_proxy JWT Auth · TOTP 2FA Meta Cloud API v20+

Architecture Overview

The platform is a multi-tenant SaaS application. A single Laravel backend serves all tenants; tenant isolation is enforced at the query level using tenant_id scoping on every table. Inbound WhatsApp messages arrive via Meta webhook, get dispatched to a queue job, and flow through a decision pipeline: session lookup → language detection → flow engine → RAG engine → live agent escalation.
External WhatsApp User
via Meta Cloud API
Meta Webhook
HTTPS POST
Admin / Agent
Browser UI
Transport Apache 2.4
SSL + mod_proxy
Laravel 12 Dev Server
port 8787 (localhost)
HTTP Layer Middleware Stack
TenantAuth · SuperAdminAuth · AuditLogger · VerifyMetaSig
Controllers AuthController KnowledgebaseController ConversationController CampaignController AnalyticsController TenantController WhatsAppWebhookController
Queue Jobs ProcessInboundMessage IngestDocument DispatchCampaign ProcessStatusUpdate
Business Modules WebhookRouter SessionManager LanguageDetector FlowEngine RagEngine AgentQueue CampaignDispatcher MessageDispatcher
Data MariaDB 10.11
r1g_whatsapp DB — all relational data
+ PostgreSQL + pgvector
kb_chunks · embeddings (pending)
External APIs Meta Cloud API v20
CloudApiClient
OpenAI / Azure OpenAI
LlmRouter · EmbeddingService
AWS S3
Document / media storage

Technology Stack

Core Framework

PackageVersionPurpose
laravel/framework^12.0Application framework — routing, ORM, queue, scheduler, DI container
tymon/jwt-auth^2.3Stateless JWT authentication. Uses HS256 signing with JWT_SECRET. Supports multiple user models via custom guards.
spatie/laravel-permission^6.25Role/permission management for granular tenant user permissions
laravel/horizon^5.47Redis-backed queue worker dashboard (configured, pending Redis activation)
openai-php/laravel^0.19.1OpenAI SDK integration — used by LlmRouter and EmbeddingService
pgvector/pgvector^0.2.2pgvector PHP client for similarity search against PostgreSQL vector store
guzzlehttp/guzzle^7.10HTTP client used by CloudApiClient to call Meta WhatsApp API
aws/aws-sdk-php^3.383S3 document storage for uploaded knowledgebase files
league/flysystem-aws-s3-v3^3.34Laravel filesystem adapter for S3
predis/predis^3.4Redis client (used when Redis is enabled for queue/cache/sessions)
otphp/totp(transitive)TOTP 2FA generation and verification — RFC 6238 compliant, 30-second window with ±1 leeway

Runtime Environment

Server

Raspberry Pi CM4 · Linux 6.12 · Apache 2.4 · PHP 8.2 FPM · MariaDB 10.11

Configuration

Session driver: database · Queue driver: database · Cache: database · Laravel backend: localhost:8787 proxied via Apache ProxyPass

Deployment Topology

# Public internet → ready1go.com → Apache 2.4 (SSL, port 443)
#
#  Apache VirtualHost: ready1go.com
#  DocumentRoot: /var/www/ready1go.com/public_html
#
#  .htaccess rule (mod_proxy + mod_rewrite):
#    /whatsapp_agent/api/*  →  ProxyPass  http://127.0.0.1:8787/api/*
#
#  Laravel (php artisan serve) running at 127.0.0.1:8787
#  Process: nohup php artisan serve --host=0.0.0.0 --port=8787
#  Working dir: /var/www/ready1go.com/public_html/whatsapp_agent/backend
#
#  Static HTML/JS/CSS served directly by Apache from:
#    /var/www/ready1go.com/public_html/whatsapp_agent/
#
#  Database: MariaDB 10.11 — r1g_whatsapp (user: sandbox)
#  PostgreSQL + pgvector: pending installation

File Structure

whatsapp_agent/
├── index.html                   # Portal selector
├── assets/
│   └── api.js                   # Shared JWT API client
├── admin/                       # Client Admin portal (35 pages)
├── super/                       # Super Admin portal (14 pages)
├── agent/                       # Agent portal (5 pages)
├── docs/                        # This page + user guide
└── backend/                     # Laravel 12 application
    ├── app/
    │   ├── Http/
    │   │   ├── Controllers/Api/V1/
    │   │   │   ├── Admin/       # Auth, KB, Conversation, Campaign, Analytics
    │   │   │   ├── Super/       # TenantController
    │   │   │   └── Webhook/     # WhatsAppWebhookController
    │   │   └── Middleware/      # TenantAuth, SuperAdminAuth, AuditLogger, VerifyMetaSig
    │   ├── Models/              # 30+ Eloquent models
    │   ├── Modules/
    │   │   ├── Core/            # WebhookRouter, SessionManager, LanguageDetector, MessageDispatcher
    │   │   ├── RAG/             # RagEngine, LlmRouter, EmbeddingService, TextChunker, TextExtractor, DocumentIngestionPipeline
    │   │   ├── Flow/            # FlowEngine
    │   │   ├── LiveAgent/       # AgentQueue
    │   │   ├── Broadcast/       # CampaignDispatcher, AudienceResolver
    │   │   └── WhatsApp/        # CloudApiClient
    │   ├── Jobs/                # ProcessInboundMessage, IngestDocument, DispatchCampaign, ProcessStatusUpdate
    │   ├── Events/              # ConversationQueued, AgentMessageReceived
    │   └── Services/            # EncryptionService (AES-256-GCM)
    ├── database/
    │   ├── migrations/
    │   │   ├── platform/        # resellers, plans, tenants, platform_users
    │   │   ├── tenant/          # all per-tenant tables (9 migration files)
    │   │   └── pgvector/        # kb_chunks with vector column
    │   └── seeders/DatabaseSeeder.php
    └── routes/api.php

Authentication & Auth Guards

Two completely separate user models share the same login endpoint. The JWT payload includes a role and portal claim. The middleware inspects the resolved model class to enforce isolation — a tenant user's JWT will never grant super admin access even if roles match.

User Models & Guards

ModelTableGuardRolesPortal
App\Models\TenantUsertenant_usersapitenant_admin, agent, supervisortenant / agent
App\Models\PlatformUserplatform_userssupersuper_adminsuper

Login Flow

Step 1
Rate limit check
Query account_lockouts — locked if 5 failures within 15 min. Lock duration: 30 min.
Step 2
Credential lookup
First query tenant_users; if null, fall through to platform_users. Hash::check() with bcrypt.
Step 3
TOTP 2FA
If totp_enabled=true and no code: return {"totp_required":true}. Secret decrypted from AES-256-GCM field.
Step 4
JWT issued
JWTAuth::fromUser($user) — payload includes role, portal claims. TTL from jwt.ttl config.
Step 5
Audit logged
Insert to login_attempts with IP, success, timestamp.

Token Usage Pattern

# All protected endpoints:
Authorization: Bearer <jwt>

# Middleware resolves user:
$user = JWTAuth::parseToken()->toUser();
# Injected into request:
$request->merge(['_tenant_id' => $user->tenant_id, '_auth_user' => $user]);

API Routes

Base path: /whatsapp_agent/api/v1 (proxied via Apache to Laravel at localhost:8787/api/v1)

Webhook (Public — signature-verified)
GET/webhook/whatsappMeta verification challenge (hub.verify_token)public
POST/webhook/whatsappInbound messages & status updates from Metasig-verified
Admin — Authentication (Public)
POST/admin/auth/loginLogin — returns JWT or totp_required flagpublic
POST/admin/auth/refreshRefresh JWT tokentenant.auth
POST/admin/auth/logoutInvalidate tokentenant.auth
GET/admin/auth/meCurrent user infotenant.auth
Admin — Knowledgebase
GET/admin/kbList knowledge bases for tenanttenant.auth
POST/admin/kbCreate new knowledge basetenant.auth
GET/admin/kb/{kbId}/documentsList documents in a KBtenant.auth
POST/admin/kb/{kbId}/documentsUpload document — dispatches IngestDocument jobtenant.auth
DELETE/admin/kb/{kbId}/documents/{docId}Delete document and its chunkstenant.auth
POST/admin/kb/{kbId}/testTest semantic query against KBtenant.auth
Admin — Conversations
GET/admin/conversationsList conversations with filters (status, agent, date)tenant.auth
GET/admin/conversations/{id}Conversation detail with full message historytenant.auth
POST/admin/conversations/{id}/messagesSend message in conversationtenant.auth
POST/admin/conversations/{id}/transferTransfer to another agenttenant.auth
POST/admin/conversations/{id}/closeClose conversation with CSAT triggertenant.auth
POST/admin/conversations/{id}/notesAdd internal agent notetenant.auth
POST/admin/conversations/{id}/csatSubmit CSAT ratingtenant.auth
Admin — Campaigns
GET/admin/campaignsList campaigns for tenanttenant.auth
POST/admin/campaignsCreate new campaign (broadcast or drip)tenant.auth
POST/admin/campaigns/{id}/launchLaunch campaign — dispatches DispatchCampaign jobtenant.auth
GET/admin/campaigns/{id}/statsCampaign delivery stats (sent/delivered/read/failed)tenant.auth
Admin — Analytics
GET/admin/analytics/dashboardKPIs — conversations, resolution rate, avg handle time, CSATtenant.auth
GET/admin/analytics/ragRAG stats — queries, hit rate, avg confidence, hallucination flagstenant.auth
GET/admin/analytics/agentsAgent performance — conversations handled, avg response timetenant.auth
GET/admin/analytics/tokensLLM token usage by day, model, tenanttenant.auth
Super Admin — Tenants
GET/super/tenantsPaginated tenant list with statussuper.auth
POST/super/tenantsProvision new tenantsuper.auth
GET/super/tenants/{id}Tenant detail with usage statssuper.auth
POST/super/tenants/{id}/approveApprove tenant to go livesuper.auth
POST/super/tenants/{id}/suspendSuspend tenant accountsuper.auth
DELETE/super/tenants/{id}Delete tenant and all datasuper.auth
GET/super/audit-logFiltered audit trail across all tenantssuper.auth
GET/super/analyticsPlatform-wide aggregated KPIssuper.auth
GET/super/api-keysAll tenant API keyssuper.auth
Agent Portal
GET/agent/queueConversations waiting in queue + agent's active conversationstenant.auth:agent
POST/agent/availabilitySet agent online/away/offline statustenant.auth:agent
POST/agent/conversations/{id}/claimClaim conversation from queuetenant.auth:agent

Middleware

AliasClassWhat it does
tenant.auth TenantAuth Parses JWT via JWTAuth::parseToken()->toUser(). Checks is_active. Optionally enforces role list (e.g. :agent,supervisor). Checks tenant IP allowlist if configured. Injects _tenant_id and _auth_user into request.
super.auth SuperAdminAuth Same JWT parse — but asserts $user->role === 'super_admin'. Returns 403 for any non-super user including tenant admins.
audit.log AuditLogger After-response middleware. Writes every mutating request (POST/PUT/PATCH/DELETE) to audit_log table with actor, action, resource, IP, timestamp.
verify.meta.signature VerifyMetaWebhookSignature Validates X-Hub-Signature-256 header using HMAC-SHA256 against META_APP_SECRET. Rejects with 403 if invalid — prevents spoofed webhook delivery.

Inbound Message Processing Pipeline

Every WhatsApp message from a user triggers this pipeline. The process is intentionally async — the webhook controller responds 200 OK immediately and offloads processing to a queue job.

1 · Webhook
WhatsAppWebhookController
Signature verified. Dispatches ProcessInboundMessage or ProcessStatusUpdate job. Returns 200.
2 · Job
ProcessInboundMessage
Resolves tenant by phone number. Looks up or creates WaUser record. Stores raw Message record.
3 · Session
SessionManager
Finds or opens a Conversation record. Updates last_message_at. Determines current session state.
4 · Language
LanguageDetector
Detects language from message text. Checks tenant's supported language list. Falls back to default language.
5 · Router
WebhookRouter
Checks opt-out keywords. Checks escalation keywords. Checks if in business hours. Routes to: Flow Engine → RAG Engine → Agent Queue.
6a · Bot
FlowEngine / RagEngine
Runs active flow if matched. Otherwise: embed query, vector search in pgvector, send LLM response with cited sources.
6b · Human
AgentQueue
Moves conversation to agent_queue status. Fires ConversationQueued event (Pusher broadcast). Agent sees it in live queue.
7 · Send
MessageDispatcher → CloudApiClient
Formats payload for Meta Cloud API v20. Sends via Guzzle HTTP. Stores wa_message_id for delivery tracking.

RAG Engine

Retrieval-Augmented Generation: converts knowledgebase documents into semantic vector embeddings, stores them in pgvector, and retrieves the most relevant chunks at query time to ground the LLM response.

Document Ingestion Pipeline (IngestDocument job)

1
TextExtractor
Reads PDF/DOCX/TXT from S3. Extracts raw text using file-type-appropriate parser.
2
TextChunker
Splits text into overlapping chunks (~512 tokens each, 50-token overlap). Preserves sentence boundaries.
3
EmbeddingService
Calls OpenAI text-embedding-3-small (or Azure equivalent). Returns 1536-dim float vector per chunk.
4
pgvector store
Inserts chunk text + vector into kb_chunks PostgreSQL table. Indexed with ivfflat for ANN search.

Query-time Retrieval

# RagEngine::answer($query, $tenantId, $kbId)
1. EmbeddingService::embed($query)           # → 1536-dim vector
2. pgvector cosine similarity search         # top-K chunks (K=5)
   SELECT *, 1 - (embedding <=> $vec) AS sim
   FROM kb_chunks WHERE kb_id = $kbId
   ORDER BY embedding <=> $vec LIMIT 5;
3. LlmRouter::complete($prompt, $context)    # inject retrieved chunks
4. Store MessageCitation records             # chunk_id → message_id
5. Return response + confidence score

LLM Router

The LlmRouter selects the LLM provider per request based on the tenant's tenant_llm_routing configuration. Supported providers: OpenAI (gpt-4o, gpt-4o-mini), Azure OpenAI, Anthropic Claude. Credentials per tenant are stored AES-256-GCM encrypted in tenant_llm_credentials.

Flow Engine

Flows are visual conversation scripts stored in flowsflow_versionsflow_nodes. Each node has a type (message, question, condition, api_call, transfer, end) and a config JSON blob. The FlowEngine evaluates the current node, dispatches the output via MessageDispatcher, then advances the session pointer to the next node based on user input matching.

Node Types

TypeBehaviour
messageSends a static text, image, or template message. Advances automatically.
questionSends a message and waits for user reply. Saves answer to session variable. Advances on any input.
conditionEvaluates a session variable against a value. Branches to true_node_id or false_node_id.
api_callHTTP GET/POST to a configured Integration endpoint. Saves JSON path result to session variable.
transferMoves conversation to agent_queue — fires ConversationQueued event.
endCloses the conversation. Optionally sends CSAT survey.

Live Agent Queue

When a conversation is escalated (escalation keyword, out-of-hours, low RAG confidence, or user request), AgentQueue::escalate() sets conversations.status = 'agent_queue' and fires the ConversationQueued event. Agents see this appear in real-time via Pusher WebSocket. When an agent claims the conversation, AgentQueue::assign() sets assigned_agent_id and status to agent_active. The AgentMessageReceived event notifies all supervisors of agent replies.

Availability States

Each agent has a row in agent_availability: online (takes new conversations), away (visible but not auto-assigned), offline (hidden from queue). The supervisor view shows all agent statuses in real-time.

Broadcast & Campaigns

The CampaignDispatcher resolves the target audience via AudienceResolver (applies tag filters, opt-in status, last active date), then splits into batches and dispatches DispatchCampaign jobs. Each job calls CloudApiClient::sendTemplate() and records a campaign_sends row. Delivery status callbacks from Meta's webhook update campaign_sends.status (sent → delivered → read / failed).

A/B testing: campaigns can have multiple CampaignAbVariant records. The audience is split evenly and each variant's template sent to its share. Open/read rates are compared after 24h.

Drip sequences: DripSequence + DripSequenceStep define timed message series. DripSequenceEnrollment tracks each user's position. The scheduler's DispatchScheduledCampaigns command runs every minute and dispatches any step whose send_at has passed.

WhatsApp Cloud API Client

CloudApiClient wraps the Meta Cloud API v20. It is instantiated with the tenant's phone number ID and access token (stored encrypted). All messages route through MessageDispatcher which picks the correct client per tenant.

MethodMeta endpointUsed for
sendText()POST /{phoneId}/messagesBot replies, agent messages
sendTemplate()POST /{phoneId}/messagesCampaign broadcasts, CSAT surveys, opt-in confirmations
sendInteractive()POST /{phoneId}/messagesFlow quick-replies, list messages
markRead()POST /{phoneId}/messagesMark message read (shows blue ticks)
getProfile()GET /{phoneId}/contactsSync WA display name to wa_users

Queue Jobs

Job ClassQueueTriggered byDoes
ProcessInboundMessagedefaultWhatsApp webhook POSTFull inbound message pipeline (session → language → router → bot/agent)
ProcessStatusUpdatedefaultWhatsApp status callbackUpdates message delivery status (sent/delivered/read/failed) in messages + campaign_sends
IngestDocumentingestionKB document upload APIExtract text → chunk → embed → store in pgvector
DispatchCampaigncampaignsCampaign launch API + schedulerResolve audience → batch send via CloudApiClient → record campaign_sends

Scheduled Commands

Run via php artisan schedule:run in crontab (* * * * *).

CommandScheduleDoes
app:aggregate-analyticsDaily 02:00Summarises conversation/message counts into daily_conversation_stats and daily_rag_stats for fast dashboard queries
app:dispatch-scheduled-campaignsEvery minuteFinds drip sequence steps and scheduled campaigns due for dispatch, dispatches DispatchCampaign jobs
app:poll-number-healthEvery 30 minPings Meta API for quality rating and tier status of each registered phone number. Writes to number_health_logs

Events & WebSocket Broadcasts

EventBroadcast channelPayloadConsumers
ConversationQueuedtenant.{id}conversation_id, wa_user name, queue positionAgent queue page — live conversation appears
AgentMessageReceivedconversation.{id}message text, agent name, timestampOpen conversation page — message appears in real time

Database Schema

All tables reside in the r1g_whatsapp MariaDB database. UUIDs (v4) are used as primary keys throughout. Tenant isolation is via tenant_id column — no row-level security, enforced in application code. pgvector tables live in a separate PostgreSQL database.

Platform Layer
platform_usersSuper admin accounts (METPL staff)
id (UUID PK)emailpassword_hashroletotp_secret_enctotp_enabledis_activelast_login_atlast_login_ip
resellersChannel partners who white-label the platform
idnamecontact_emailcommission_pctis_active
subscription_plansTiered pricing plans (Starter, Pro, Enterprise)
idnamemonthly_pricemax_conversationsmax_agentsmax_kb_docsllm_tokens_included
tenantsClient organisations
idnameslugplan_idreseller_idstatus (pending/active/suspended)timezonedefault_language
Tenant Configuration
tenant_bot_configBot persona and behaviour settings
idtenant_idbot_namesystem_promptfallback_messageescalation_messageconfidence_thresholdmax_bot_turns
tenant_llm_routingPer-tenant LLM model selection
idtenant_idprovider (openai/azure/anthropic)modeluse_case (chat/embed/classify)priority
tenant_languagesLanguages the tenant's bot supports
idtenant_idlanguage_codeis_defaultis_active
business_hoursOperating hours per day-of-week
idtenant_idday_of_week (0-6)open_timeclose_timeis_closed
tenant_optout_keywords / tenant_escalation_keywordsKeyword triggers for opt-out and escalation
idtenant_idlanguage_codekeyword
tenant_phone_numbersWhatsApp Business phone numbers registered to tenant
idtenant_idphone_numberwa_phone_number_idwa_access_token_encquality_ratingmessaging_limit_tier
tenant_ip_allowlistIP/CIDR ranges allowed for tenant API access
tenant_idcidrlabel
Auth & Users
tenant_usersTenant admin, agent, supervisor accounts
idtenant_idemailpassword_hashdisplay_nameroletotp_enabledtotp_secret_encis_activelast_login_at
agent_availabilityLive agent online/away/offline state
agent_idstatusupdated_at
login_attempts / account_lockoutsBrute-force protection log and lockout records
emailportal_typeip_addresssuccessfailure_reasonattempted_at | locked_atunlock_at
api_keysTenant API keys for external integrations
idtenant_idkey_prefixkey_hashnamelast_used_atexpires_at
WhatsApp Core
wa_usersWhatsApp end-users who have messaged the tenant's bot
idtenant_idwa_id (phone)display_namelanguage_codelast_seen_atopted_in
wa_user_tagsSegmentation tags applied to WA users
wa_user_idtagcreated_at
wa_user_opt_insExplicit opt-in records per channel type
wa_user_idchannel_typeopted_in_atopted_out_at
Conversations & Messages
conversationsA session between a WA user and the tenant
idtenant_idwa_user_idstatus (bot_active/agent_queue/agent_active/closed)assigned_agent_idlanguage_codeflow_session_data (JSON)bot_turnsopened_atclosed_atcsat_score
messagesEvery message in every conversation
idconversation_idtenant_iddirection (inbound/outbound)sender_type (user/bot/agent/system)sender_agent_idwa_message_idmessage_typecontent (JSON)statusllm_model_usedprompt_tokenscompletion_tokens
message_citationsWhich KB chunks were used to generate an AI response
message_idchunk_idsimilarity_score
message_feedbackThumbs up/down on AI responses from agents
message_idrating (1/-1)rated_bycomment
conversation_notesInternal agent notes on a conversation
idconversation_idagent_idbody
Knowledge Base
kb_documentsUploaded knowledge base source files
idtenant_idkb_idfilenames3_keystatus (pending/processing/ready/error)chunk_count
kb_gap_logQueries with no confident answer — gaps in the KB
tenant_idquery_texttop_similarityoccurred_at
faq_categories / faq_pairsManual Q&A pairs as an alternative to document-based KB
idtenant_idcategory_name | category_idquestionansweris_active
kb_chunks (PostgreSQL + pgvector)Text chunks with 1536-dim embedding vectors
iddocument_idtenant_idchunk_indexchunk_textembedding vector(1536)
Flows & Templates
flows / flow_versions / flow_nodesVisual conversation flow scripts with versioning
flow.idtenant_idnametrigger_keywordsactive_version_id | version.idversion_numberpublished_at | node.idtypeconfig (JSON)next_node_id
wa_templates / wa_template_languages / wa_template_variablesMeta-approved WhatsApp message templates
template.idtenant_idnamecategory | language_codewa_template_idstatus | componentvariable_indexsample_value
wa_flowsMeta WhatsApp Flows (native interactive screens)
idtenant_idwa_flow_idnamestatusjson_payload
Campaigns & Drip
campaignsBroadcast or drip campaign definitions
idtenant_idnametype (broadcast/drip)statustemplate_idscheduled_atsent_countdelivered_countread_count
campaign_ab_variantsA/B test variants per campaign
idcampaign_idvariant_labeltemplate_idaudience_split_pct
campaign_sendsPer-recipient delivery tracking
campaign_idwa_user_idwa_message_idstatussent_atdelivered_atread_at
drip_sequences / drip_sequence_steps / drip_sequence_enrollmentsTimed multi-message sequences
seq.idname | step.iddelay_hourstemplate_id | wa_user_idcurrent_stepcompleted_at
Analytics & Audit
daily_conversation_statsPre-aggregated daily summary for fast dashboard queries
tenant_idstat_datetotal_conversationsbot_resolvedescalatedavg_handle_time_secavg_csat
daily_rag_statsRAG engine performance metrics by day
tenant_idstat_datetotal_queriesabove_thresholdavg_similarityavg_tokens_used
audit_logImmutable record of every admin/super action
idtenant_idactor_typeactor_idactionresource_typeresource_idip_addresspayload (JSON)created_at
number_health_logsPhone number quality ratings over time
phone_number_idquality_rating (GREEN/YELLOW/RED)messaging_limit_tierchecked_at

Encryption

Sensitive secrets (TOTP seeds, WhatsApp access tokens, LLM API keys) are stored encrypted in the database. The EncryptionService uses AES-256-GCM with a 256-bit key stored in .env as hex (CREDENTIAL_ENCRYPTION_KEY). Each encryption call generates a unique 12-byte random nonce prepended to the ciphertext. Key loading is lazy — the key is only read from config when encrypt() or decrypt() is first called, preventing artisan command boot failures if the key is not configured.

# Ciphertext format (base64-encoded):
[ 12-byte nonce ][ N-byte ciphertext ][ 16-byte GCM auth tag ]

# Key: 32 bytes loaded from 64-char hex string in .env
CREDENTIAL_ENCRYPTION_KEY=387975dfc840dfbb... # 64 hex chars = 32 bytes

Security

Brute-force protection

5 failed login attempts within 15 minutes locks the account for 30 minutes. Tracked in login_attempts + account_lockouts. Lock applies per email + portal type.

TOTP 2FA

Per-user opt-in. Uses otphp/totp (RFC 6238). 30-second window with ±1 period leeway. Secret stored AES-256-GCM encrypted.

Webhook signature

Every inbound Meta webhook is verified via HMAC-SHA256 of the raw body against META_APP_SECRET. Rejects with HTTP 403 on mismatch.

IP allowlist

Tenants can configure CIDR ranges in tenant_ip_allowlist. Enforced in TenantAuth middleware on every API request. Supports IPv4 CIDR notation.

Tenant isolation

Every query in every controller is scoped with ->where('tenant_id', $request->get('_tenant_id')). Middleware injects the tenant_id from the validated JWT — no user-supplied value is trusted.

Audit trail

All POST/PUT/PATCH/DELETE actions by admins and super admins are logged to audit_log with actor, resource, IP, and request payload. Immutable (no update/delete routes).

Frontend Architecture

The frontend is intentionally server-free: plain HTML + CSS + vanilla JavaScript served directly by Apache as static files. There is no build step, no bundler, and no framework. All dynamic data is fetched via the shared api.js client module.

PortalPathPagesAuth
Super Admin/whatsapp_agent/super/14 pagessuper.auth — role: super_admin
Client Admin/whatsapp_agent/admin/35 pagestenant.auth — role: tenant_admin
Agent/whatsapp_agent/agent/5 pagestenant.auth — role: agent/supervisor

Shared API Client (api.js)

Located at /whatsapp_agent/assets/api.js and included via <script> tag on every page. Exposes a global API object.

// Token management
API.getToken()          // reads from localStorage 'r1g_token'
API.setToken(jwt)       // stores token
API.getUser()           // parses JWT payload (no verify — frontend only)
API.requireAuth()       // redirect to login if no token
API.logout()            // POST /auth/logout then clear localStorage

// Namespaced API methods
API.admin.login(email, pass, totpCode)
API.admin.analytics(days)          // GET /admin/analytics/dashboard
API.admin.ragStats(days)           // GET /admin/analytics/rag
API.admin.conversations(filters)   // GET /admin/conversations
API.admin.campaigns()              // GET /admin/campaigns
API.admin.kb()                     // GET /admin/kb

API.super.tenants()                // GET /super/tenants
API.super.analytics()              // GET /super/analytics
API.super.auditLog(filters)        // GET /super/audit-log

API.agent.queue()                  // GET /agent/queue
API.agent.setAvailability(status)  // POST /agent/availability
API.agent.claim(convId)            // POST /agent/conversations/:id/claim