All docs

Security and compliance

Data we store

| Data | Where | Retention | | --- | --- | --- | | Visitor chat messages | conversations, messages in Supabase | 90 days by default. Adjust in /admin/ai-settings. | | Leads (name, email, phone, reg, vehicle, notes) | leads in Supabase | 24 months by default. Adjust in /admin/ai-settings. | | KB content | kb_articles, kb_chunks | Until deleted. | | Admin users | users | Until deleted by an Owner. |

We never store payment details. We never store DVLA / MOT history. The widget does not read the host page DOM beyond the URL.

GDPR

QCD is the data controller; the developer is the data processor. The bot tells visitors before they type that "we may store your message to help with your enquiry". That consent string is configurable in /admin/widget.

Right-to-erasure requests can be honoured by:

1. Looking up the visitor's email or phone in /admin/leads. 2. Deleting the lead and the linked conversation. RLS and foreign keys cascade the deletion.

Prompt safety

src/lib/security/guards.ts blocks:

  • Common prompt-injection patterns ("ignore previous instructions", system-role smuggling, fenced override blocks).
  • Off-topic queries (politics, medical advice, etc.) - the bot politely declines.
  • Inputs longer than 2,000 characters.

src/lib/ai/system-prompt.ts further requires the model to:

  • Cite KB chunks when used.
  • Refuse to invent pricing, opening times, guarantees, approvals or availability.
  • Offer to pass to the team when unsure.

Origin allowlist

The /api/chat and /api/leads endpoints check the Origin header against WIDGET_ALLOWED_ORIGINS (CSV). Requests from other origins are rejected with 403.

Rate limits

In-process token bucket per client IP. Defaults:

  • /api/chat: 30 messages / minute.
  • /api/leads: 6 submissions / minute.

For production traffic spikes, swap the in-memory store in src/lib/security/ratelimit.ts for Upstash Redis - the interface is intentionally a single rateLimit(key, opts) function.

Secrets

  • SUPABASE_SERVICE_ROLE_KEY and all *_API_KEY values are server-side only and never sent to the browser. They are referenced from API routes (runtime = "nodejs").
  • The widget loader reads only public config from /api/embed/config.

Auditing

The events table records significant actions (user logins, KB approvals, settings changes). Use it for compliance reporting.

Row-Level Security (RLS) checklist

Supabase RLS is a defense-in-depth layer. All API routes use the service role key (bypasses RLS), so RLS is not strictly required for operation. However, enabling RLS provides additional protection against misconfiguration or direct database access.

| Table | Public access | Admin access | RLS enabled? | |-------|---------------|-------------|--------------| | conversations | INSERT only | SELECT, UPDATE, DELETE | Verify in Supabase dashboard | | messages | INSERT only | SELECT, DELETE | Verify | | leads | INSERT only | SELECT, UPDATE, DELETE | Verify | | booking_requests | INSERT only | SELECT, UPDATE, DELETE | Verify | | email_logs | None | SELECT, INSERT | Verify (recently added table) | | kb_articles | None | SELECT, UPDATE, DELETE | Verify | | kb_chunks | None | SELECT, INSERT, DELETE | Verify | | pricing_rules | None | SELECT, UPDATE, DELETE | Verify | | service_catalogue | None | SELECT, UPDATE, DELETE | Verify | | settings | None | SELECT, UPDATE | Verify | | audit_logs | None | SELECT | Verify | | email_templates | None | SELECT, UPDATE, DELETE | Verify | | customer_consents | None | SELECT | Verify | | reminder_preferences | None | SELECT | Verify | | motasoft_handoff_logs | None | SELECT | Verify | | org_members | None | SELECT | Verify | | organisations | None | SELECT | Verify |

To verify in Supabase dashboard: 1. Go to Authentication → Policies for each table 2. Check that each table has RLS enabled (toggle on) 3. Add policies matching the access levels above

For public tables (conversations, messages, leads, booking_requests):


CREATE POLICY "Public can insert" ON conversations FOR INSERT WITH CHECK (true);

For admin-only tables:


CREATE POLICY "Service role full access" ON kb_articles FOR ALL USING (true);

Since all API routes use the service role key, the service role bypasses all RLS policies. The policies are a safety net, not the primary access control.