Architecture
Appilot is a multi-tenant platform composed of several services. Understanding the architecture helps you make better integration decisions.
System overview
┌─────────────────────────────────────────────────────────────┐
│ Clients │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Extension │ │ Widget │ │ Backoffice │ │
│ │ (Chrome) │ │ (Embedded) │ │ (Org Admin) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────────┘ │
└─────────┼─────────────────┼─────────────────┼───────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Backend API (Express) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐ │
│ │ Auth │ │Knowledge │ │ RAG │ │ Conversations│ │
│ │ (JWT) │ │ CRUD │ │ Files │ │ + AI Engine │ │
│ └─────────┘ └──────────┘ └─────────┘ └──────────────┘ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐ │
│ │Activity │ │Analytics │ │Notific. │ │ Domains │ │
│ │ Events │ │ Engine │ │ System │ │ + Coverage │ │
│ └─────────┘ └──────────┘ └─────────┘ └──────────────┘ │
└──────────────────────┬──────────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌────────────┐ ┌─────────┐ ┌──────────┐
│ PostgreSQL │ │ AI │ │ File │
│ (Data) │ │Provider │ │ Storage │
└────────────┘ └─────────┘ └──────────┘
Key components
Extension & Widget (Client Layer)
Both the Chrome Extension and the Embeddable Widget:
- Capture page context (DOM structure, forms, fields)
- Provide a conversational UI for the AI assistant
- Apply client-side PII masking before sending data
- Communicate with the backend via REST API + WebSocket
The key difference is how they're delivered and how they authenticate:
| Extension | Widget | |
|---|---|---|
| Delivered by | The user installs it. During the closed beta that is a ZIP loaded unpacked | The developer adds a script tag or boots it from the appilot package |
| Authentication | JWT via user login (stored in chrome.storage) | Widget API key + optional user JWT |
| HTTP communication | Via background worker (chrome.runtime) | Direct fetch from page |
| DOM access | Content script (isolated world) | Web Component (light DOM access) |
| CSS isolation | Manual style injection | Shadow DOM (native) |
| Page scope | All pages the user visits | Only the page where embedded |
Backend API
A Node.js/Express server that handles:
- Authentication: JWT-based with organization context
- Knowledge management: CRUD for knowledge content scoped to domains/URLs
- RAG: File upload, embedding, and retrieval for document-grounded answers
- Conversations: AI-powered chat with context injection
- Activity tracking: Anonymized interaction signals for analytics
- Notifications: Real-time updates via WebSocket
Database
PostgreSQL with multi-tenant data isolation:
- Organization-scoped data with row-level filtering
- Knowledge content, requests, domains, user management
- Activity and analytics data
AI Provider
Configurable per organization:
- Default: DeepSeek (
deepseek-v4-flash), chosen because it is the cheapest engine that handles the agent loop's tool use well and because its weights are open, so the same engine can later run on a customer's own hardware. In the closed beta it points at DeepSeek's managed API, which processes in China, so an organization with data residency obligations selects a different provider before it deploys. A self-hosted DeepSeek works through a custom base URL but is not yet recognised as sovereign; onlygemma-on-premis. - Adapters wired today: DeepSeek, OpenAI, Anthropic, Gemini (env-configured
only), and an OpenAI-compatible on-premise engine (
gemma-on-prem) - Configurable model selection per organization
- Tool use support for structured actions
- RAG integration for document-grounded responses
Data flow: User asks a question
Extension flow
1. User types question in Extension
2. Content script captures page context (forms, fields, URL)
3. PII masking filters sensitive data client-side
4. Message sent to background worker via chrome.runtime.sendMessage
5. Background worker sends POST /assistant/message with JWT
6. Backend resolves the organization from the caller's live membership row
7. The domain named in the request must belong to that organization, or the turn
is refused
8. Knowledge + RAG + AI provider generates response
9. Response returned to background worker → content script → UI
Every DOM read and action in a turn goes to the tab the turn started in. The tab
is resolved once, when the user sends, and each dispatch re-checks it: a closed
tab or a tab that navigated to another origin refuses rather than falling back to
whatever is focused now. Resolving the active tab per request pointed
read_form_state and http_fetch at whichever tab the user had switched to
while waiting, with that tab's field values and cookies.
Widget flow
1. User types question in Widget
2. Widget scans host page DOM for context (forms, fields, URL)
3. PII masking filters sensitive data client-side
4. Widget sends POST /widget/message with API key + session
5. Backend resolves organization from API key (widget_api_keys table)
6. Backend validates Origin header against allowed domains
7. Knowledge + RAG + AI provider generates response
8. Response returned directly to widget
Multi-tenancy
Every API request carries an organization context:
- Extension/Backoffice: organization and role read from the caller's live membership row on each request. The JWT names an identity; it is not the authority on what that identity may do
- Widget: Organization resolved from Widget API key (
widget_api_keys.organization_id)
Data queries are scoped to the active organization. AI configuration, knowledge, domains, and users are all isolated per tenant. A handler acting on a record by id proves ownership by loading the record, never by reading an id out of the request body.
See Multi-Tenancy for details.
Security model
Extension
- JWT authentication (user login required)
- Token stored in
chrome.storage.local - All API calls go through the background worker
Widget
- Widget API Key authenticates the integration (not the user)
- Keys are domain-bound: backend validates
Originheader against the key's allowed domains list - Every conversation is attributed to an identified user (no anonymous usage): via federation (the host app's signed handshake), the widget's built-in login, or enterprise SSO. The API key + ephemeral session identify the embed, not the person.
- Rate limiting per API key (configurable
rate_limit_per_minute) - PII masking runs client-side (same
appilot-pii-detectionpackage as the extension)
API Rate Limiting
All API endpoints are protected by tiered rate limiting:
- Global: 300 requests per minute per IP
- AI endpoints: 20 requests per minute (cost protection)
- Auth endpoints: 20 requests per 15 minutes (brute-force protection)
Security headers are enforced via Helmet middleware.
Both
- HTTPS enforced in production
- Backend PII sanitization middleware as second defense layer
- Multi-tenant data isolation via organization scoping
Request logging
The always-on record of a request is correlation only: method, path without its query string, status, duration, the request id that surfaces in an error message, and the calling user and organization. Headers and bodies are captured only when a deployment switches them on, separately for inbound and outbound traffic, and they pass a redaction step even then. Redaction is defined in one place and combines a key rule with a value-shape rule, because a customer names its own vault headers and a deny list of key names can therefore never be enough. These rows are swept daily and kept seven days.
Declarative Site Adapters
Site Adapters extend Appilot's compatibility to any web framework by providing per-domain configuration:
Admin configures adapter (Backoffice)
→ Stored in database (domains.adapter_config)
→ Fetched by extension on page load
→ Applied to Shadow DOM traversal, page detection, interactions
This architecture decouples framework-specific behavior from code. Supporting a new framework means writing a JSON configuration; the extension itself stays unchanged.