Technical Specification
Complete implementation reference for the Ready1Go WhatsApp AI Agent platform — packages, architecture, data models, API surface, and module wiring.
Architecture Overview
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.
via Meta Cloud API ⟷ Meta Webhook
HTTPS POST ⟷ Admin / Agent
Browser UI
SSL + mod_proxy → Laravel 12 Dev Server
port 8787 (localhost)
TenantAuth · SuperAdminAuth · AuditLogger · VerifyMetaSig
r1g_whatsapp DB — all relational data + PostgreSQL + pgvector
kb_chunks · embeddings (pending)
CloudApiClient OpenAI / Azure OpenAI
LlmRouter · EmbeddingService AWS S3
Document / media storage
Technology Stack
Core Framework
| Package | Version | Purpose |
|---|---|---|
laravel/framework | ^12.0 | Application framework — routing, ORM, queue, scheduler, DI container |
tymon/jwt-auth | ^2.3 | Stateless JWT authentication. Uses HS256 signing with JWT_SECRET. Supports multiple user models via custom guards. |
spatie/laravel-permission | ^6.25 | Role/permission management for granular tenant user permissions |
laravel/horizon | ^5.47 | Redis-backed queue worker dashboard (configured, pending Redis activation) |
openai-php/laravel | ^0.19.1 | OpenAI SDK integration — used by LlmRouter and EmbeddingService |
pgvector/pgvector | ^0.2.2 | pgvector PHP client for similarity search against PostgreSQL vector store |
guzzlehttp/guzzle | ^7.10 | HTTP client used by CloudApiClient to call Meta WhatsApp API |
aws/aws-sdk-php | ^3.383 | S3 document storage for uploaded knowledgebase files |
league/flysystem-aws-s3-v3 | ^3.34 | Laravel filesystem adapter for S3 |
predis/predis | ^3.4 | Redis 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
Raspberry Pi CM4 · Linux 6.12 · Apache 2.4 · PHP 8.2 FPM · MariaDB 10.11
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
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
| Model | Table | Guard | Roles | Portal |
|---|---|---|---|---|
App\Models\TenantUser | tenant_users | api | tenant_admin, agent, supervisor | tenant / agent |
App\Models\PlatformUser | platform_users | super | super_admin | super |
Login Flow
account_lockouts — locked if 5 failures within 15 min. Lock duration: 30 min.tenant_users; if null, fall through to platform_users. Hash::check() with bcrypt.totp_enabled=true and no code: return {"totp_required":true}. Secret decrypted from AES-256-GCM field.JWTAuth::fromUser($user) — payload includes role, portal claims. TTL from jwt.ttl config.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)
Middleware
| Alias | Class | What 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.
ProcessInboundMessage or ProcessStatusUpdate job. Returns 200.WaUser record. Stores raw Message record.Conversation record. Updates last_message_at. Determines current session state.agent_queue status. Fires ConversationQueued event (Pusher broadcast). Agent sees it in live queue.wa_message_id for delivery tracking.RAG Engine
Document Ingestion Pipeline (IngestDocument job)
text-embedding-3-small (or Azure equivalent). Returns 1536-dim float vector per chunk.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 flows → flow_versions → flow_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
| Type | Behaviour |
|---|---|
message | Sends a static text, image, or template message. Advances automatically. |
question | Sends a message and waits for user reply. Saves answer to session variable. Advances on any input. |
condition | Evaluates a session variable against a value. Branches to true_node_id or false_node_id. |
api_call | HTTP GET/POST to a configured Integration endpoint. Saves JSON path result to session variable. |
transfer | Moves conversation to agent_queue — fires ConversationQueued event. |
end | Closes 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.
| Method | Meta endpoint | Used for |
|---|---|---|
sendText() | POST /{phoneId}/messages | Bot replies, agent messages |
sendTemplate() | POST /{phoneId}/messages | Campaign broadcasts, CSAT surveys, opt-in confirmations |
sendInteractive() | POST /{phoneId}/messages | Flow quick-replies, list messages |
markRead() | POST /{phoneId}/messages | Mark message read (shows blue ticks) |
getProfile() | GET /{phoneId}/contacts | Sync WA display name to wa_users |
Queue Jobs
| Job Class | Queue | Triggered by | Does |
|---|---|---|---|
ProcessInboundMessage | default | WhatsApp webhook POST | Full inbound message pipeline (session → language → router → bot/agent) |
ProcessStatusUpdate | default | WhatsApp status callback | Updates message delivery status (sent/delivered/read/failed) in messages + campaign_sends |
IngestDocument | ingestion | KB document upload API | Extract text → chunk → embed → store in pgvector |
DispatchCampaign | campaigns | Campaign launch API + scheduler | Resolve audience → batch send via CloudApiClient → record campaign_sends |
Scheduled Commands
Run via php artisan schedule:run in crontab (* * * * *).
| Command | Schedule | Does |
|---|---|---|
app:aggregate-analytics | Daily 02:00 | Summarises conversation/message counts into daily_conversation_stats and daily_rag_stats for fast dashboard queries |
app:dispatch-scheduled-campaigns | Every minute | Finds drip sequence steps and scheduled campaigns due for dispatch, dispatches DispatchCampaign jobs |
app:poll-number-health | Every 30 min | Pings Meta API for quality rating and tier status of each registered phone number. Writes to number_health_logs |
Events & WebSocket Broadcasts
| Event | Broadcast channel | Payload | Consumers |
|---|---|---|---|
ConversationQueued | tenant.{id} | conversation_id, wa_user name, queue position | Agent queue page — live conversation appears |
AgentMessageReceived | conversation.{id} | message text, agent name, timestamp | Open 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.
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
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.
Per-user opt-in. Uses otphp/totp (RFC 6238). 30-second window with ±1 period leeway. Secret stored AES-256-GCM encrypted.
Every inbound Meta webhook is verified via HMAC-SHA256 of the raw body against META_APP_SECRET. Rejects with HTTP 403 on mismatch.
Tenants can configure CIDR ranges in tenant_ip_allowlist. Enforced in TenantAuth middleware on every API request. Supports IPv4 CIDR notation.
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.
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.
| Portal | Path | Pages | Auth |
|---|---|---|---|
| Super Admin | /whatsapp_agent/super/ | 14 pages | super.auth — role: super_admin |
| Client Admin | /whatsapp_agent/admin/ | 35 pages | tenant.auth — role: tenant_admin |
| Agent | /whatsapp_agent/agent/ | 5 pages | tenant.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