Security & data handling
TL;DR. Your tenant data is isolated by a strict
tenant_idboundary on every row and every query. Sensitive secrets (API keys, OAuth tokens, BYO-LLM config) are encrypted at rest. The agent's system prompt is server-side only and is never returned to the visitor's browser. Webhooks are HMAC-signed and outbound URLs are SSRF-guarded. PII in voice recordings is scrubbed before storage. Action history (leads, webhook deliveries, voice audits) is append-only.
This page is a plain-English summary of the security posture so you can answer the question "is my customer data safe?" without reading the source. For the threat-model deep dive see chapter 15 — External integrations & MCP tokens.
Tenant isolation
Every table that holds tenant data has a tenant_id column. Every query in the application code scopes by it. There is no admin "see-all" view that crosses tenants except for the website operator's own admin panel — which is gated by a separate role.
The vector store (where your knowledge base is embedded) uses per-tenant + per-agent namespaces: tenant:{tenant_id}:agent:{agent_id}. A query from one tenant cannot retrieve another tenant's chunks even if the underlying provider had a bug, because the namespace is part of the query, not a filter.
In plain English: Your competitor's agent and your agent live in two different rooms. The doors are not locked from the same key.
What's encrypted at rest
The following columns are stored as Laravel-encrypted ciphertext (AES-256-CBC + HMAC), unreadable without the application key:
tenants.llm_provider_config— your OpenAI / Azure / Ollama / Anthropic key + base URL when you bring your own LLM.- OAuth tokens for connected integrations (HubSpot, etc.).
- Webhook signing secrets (also masked in the dashboard — you can rotate but not view them after creation).
Everything else is encrypted at the disk layer (managed cloud database — your hosting provider's encryption-at-rest, plus daily backups).
What's never sent to the visitor
A visitor's browser receives the chat widget, the conversation transcript, and any tool responses you've configured. They cannot see:
- The agent's system prompt (your "How the agent should behave" text). It's substituted on the server before the LLM call.
- Other visitors' conversations.
- Your tenant or agent IDs (only opaque session JWTs).
- Knowledge-base content that hasn't been retrieved for the current question. The vector store is queried server-side; only the matching chunks are passed to the LLM, and only the LLM's reply text is returned.
- Internal API keys, install keys for other agents, or tenant-level configuration.
The session JWT signed for the embed widget is short-lived, scoped to one agent, and revocable.
Append-only audit trails
Four tables in the system are write-once and visible in the dashboard:
agent_action_runs— every tool call the agent made (lead capture, booking, escalation, webhook).agent_usage_events— every LLM token + every embed turn billed against your plan.agent_webhook_deliveries— every outbound webhook attempt (status, response code, retry count).voice_pii_audits— every PII scrub on a voice recording.
You cannot edit or soft-delete these rows from the UI. If a question arises about "what did the system do on Tuesday at 3 PM", the audit row is the answer.
Outbound webhooks
When you configure a webhook endpoint to receive lead / booking / quote / support events:
- The body is JSON, signed with your endpoint's signing secret using HMAC-SHA-256.
- The signature is in the
X-Coffield-Signatureheader — verify it on your receiver using the secret you saved at endpoint-creation time. - The destination URL is SSRF-guarded before delivery: links pointing at private/loopback IP ranges (
10.0.0.0/8,192.168.0.0/16,127.0.0.1,169.254.0.0/16, etc.) are rejected. Localhost is permitted only when the operator opts in viaAGENT_WEBHOOK_ALLOW_HTTP=true. - Failed deliveries retry with exponential backoff. Final state — success / failed / blocked — is visible at AI Agents → Webhook Deliveries.
What this protects against. A malicious tenant cannot register
http://localhost/adminas a webhook URL to probe internal services, and nobody can spoof a webhook payload to your receiver because they don't have your signing secret.
See chapter 9 — Outbound webhooks for the payload shape and verification snippets.
Voice recordings
Voice recordings on the Pro plan go through a PII scrub before transcripts are stored:
- Email addresses, phone numbers, SSNs, credit-card-shaped patterns are redacted.
- Each scrub creates a row in
voice_pii_auditsshowing what kind of PII was redacted (not the value itself). - The original audio is encrypted in object storage; signed-URL access expires per request.
See chapter 14 — Voice recording.
File sharing
When you share a file from the File Manager:
- Public share links use Laravel signed URLs with an explicit expiry (1 hour, 24 hours, 7 days, 30 days, or permanent).
- The signature is keyed on your
APP_KEY— links cannot be forged. - Files served via authenticated download go through
TenantFileControllerwhich checks tenant ownership AND the file's scan status before streaming bytes. - Files flagged by the virus scanner (when enabled) return HTTP 451 (legal-block code) instead of the file body.
MCP tokens (REST + automation)
If you're using MCP tokens to let Claude Desktop, n8n, Zapier, or another tool read or write to your workspace:
- Each token has a scoped permission set — read-only, lead-write, etc.
- Failed authentication attempts trigger a per-IP brute-force lockout.
- Each token has a per-token request burst throttle.
- Tokens have a configurable lifetime (
AGENT_MCP_TOKEN_LIFETIME_DAYS) and can be revoked from the dashboard at any time.
See chapter 15 — External integrations & MCP tokens.
What we log
For diagnostics and abuse detection, we log:
- HTTP requests + their status codes (no request bodies for embed traffic).
- LLM token counts per turn (for billing).
- Tool-call inputs and outputs (visible in the conversation transcript).
- Webhook delivery attempts and response status.
We do not log:
- Visitor IP addresses beyond the lifetime of the session (anonymous embed traffic).
- The text of an LLM response after it's returned to the visitor (the transcript is stored in
agent_messages, but the LLM provider's intermediate state is not retained). - Your encrypted secrets or BYO-LLM keys at any point in the request path.
Reporting a security concern
If you believe you've found a security issue — an information leak, a way to read another tenant's data, a way to bypass tenant isolation — email support@coffield.io with:
- The reproduction steps (URL, request body, expected vs actual).
- Any account / agent IDs involved.
- Your contact info for follow-up.
Please do not post in public forums until we've had a chance to fix the issue. We aim to acknowledge security reports within 24 hours and ship a fix on a priority track.