Skip to content
DeskbellGitHub

Security

Where your data goes, and where it doesn't

Support conversations are dense with personal data, which makes “where does this run” the first question a privacy review asks. Deskbell runs on your infrastructure: one outbound call to the model provider you chose, or none at all with a local model. This page also lists the security work that isn't finished.

Last updated: 2026-09-22

Data flow

One boundary, and you control which side things sit on

Stays in your infrastructure

  • Your documentation, chunked and embedded
  • Every question a user asks
  • Every answer, citation and refusal
  • Conversation transcripts and session identifiers
  • Analytics, including unanswered-question reports
  • Nothing about visitor IP addresses: raw addresses are never stored

All of it lives in the Postgres instance you operate. We never receive it, which means there is nothing for us to leak, subpoena or retain.

Crosses the boundary

  • The model call

    The retrieved context and the user's question go to the model provider you configured, over their API. This is the only outbound call the answering path makes.

  • Nothing, with a local model

    Point the model interface at Ollama and the outbound call disappears. The entire loop then runs inside your network.

Provider SDKs are confined to the model-configuration module, so switching providers — or moving to a local model — changes one value, not your architecture.

Compliance

What self-hosting actually changes for a privacy review

Not legal advice — get your own. But the structural facts are worth stating plainly, because they're the reason regulated teams end up here.

GDPR

There is no processor agreement to negotiate with us and no international transfer to justify, because your data never reaches us. You stay the controller; your model provider is the only processor in the answering path, and a local model removes even that.

Data residency

Deskbell runs wherever you run Postgres. If that's Frankfurt, your conversations are in Frankfurt. Choosing an EU-hosted model provider — or a local model — keeps the entire path inside the region.

Retention

Transcript retention is configurable per project in the open core, and deletion is a database operation you control. Audit-grade logging built for EU AI Act Article 12 evidence is an enterprise feature.

Sources: EU AI Act

Threat model

A public widget is an untrusted input surface. We treat it as one.

Why answering comes before acting

Anyone on the internet can type into a widget embedded on public documentation. While the agent only reads and answers, the worst outcome of a manipulated prompt is a bad answer. The moment it can write — cancel an order, issue a refund — the worst outcome changes category entirely.

So write actions ship behind that model, never ahead of it: every tool is off until an admin chooses, a lookup can be allowed outright, a write runs only after the visitor confirms a card, the visitor’s identity reaches the server as a signed token, and every call lands in an audit log. The model was specified before the first tool was wired in. One incident would end this project’s credibility permanently.

Where the widget sits

  • Rendered in a shadow DOM, so it can't read or restyle your page and your CSS can't break it.
  • Authenticated with a publishable project key — the same class of credential as a publishable payments key. It grants asking, nothing else.
  • No third-party bot-scoring service is loaded onto your documentation page.
  • Origin binding, rate limits and a daily token budget stand between the widget and your model account. See below.

Running in public

What protects a public widget

The widget endpoints are unauthenticated by design and the project key ships in your page source. These controls are what protect you, not key secrecy. The security guide has the request path and every limit.

Origin binding

Each project carries an allowlist of origins. In production, a project with no allowed origins refuses to answer, so a forgotten setting fails closed.

Rate limits

Per IP and per project, with Retry-After. Buckets are keyed by an HMAC of the address, and the raw address is never stored.

Daily token budget

A spend ceiling per project. When it is reached the widget stops answering until the next day instead of spending your model account.

Context is data, never instructions

Retrieved chunks reach the model in a user turn; the system prompt is byte-identical whatever was retrieved. Instruction-shaped text is flagged at ingest and counted per source in the dashboard.

Not finished

Security work that isn't done

A security page that only lists strengths is not a security page. These are the known gaps as of 2026-09-22. If you’re evaluating Deskbell for anything public-facing, read this list first.

open

Row-level security

Tenant isolation is enforced in the query layer through scoped helpers, not yet by Postgres row-level security. The schema carries an organisation id on every row so RLS can be switched on, but that work isn't finished.

open

Injection resistance is measured, not guaranteed

Flagged chunks still answer: flagging is visibility, not filtering, because silently dropping a chunk deletes part of your documentation on a heuristic's say-so. Whether a payload gets through is a property of your model; the eval harness ships an injection dataset so you can measure yours.

open

Third-party security audit

None has been performed. There is no SOC 2 report, no penetration test and no certification, and there won't be before a stable release.

Nothing on this list is hidden — each one is tracked in the repository. If a gap here is a blocker for you, saying so is the fastest way to move it up the queue.

Disclosure

Reporting a vulnerability

Email security@deskbell.dev with the details and a reproduction if you have one. Please don't open a public issue for anything exploitable. We'll acknowledge it, work a fix in private, and credit you in the release notes unless you'd rather we didn't.

For anything non-exploitable, please open an issue instead.

Questions

Security questions

What data leaves my infrastructure?

One thing: the model call. The user's question and the retrieved documentation context go to the provider you configured, and the answer comes back. Documents, embeddings, transcripts and analytics all stay in your Postgres. With a local model through Ollama, nothing leaves at all.

Does Deskbell help with GDPR compliance?

Self-hosting removes an entire class of problem: there is no processor to contract with, no transfer mechanism to justify and no sub-processor list to track, because we never receive your data. You remain the controller, and your model provider becomes your only processor in the answering path.

What about the EU AI Act?

A support agent answering customer questions is generally limited-risk, and the practical obligations are transparency and record-keeping. Self-hosting gives you the records; audit-grade logging built for Article 12 evidence is an enterprise feature. This is not legal advice — get your own.

Can the widget be abused to spend my model budget?

Not without passing three controls. The widget endpoints are public by design and the project key is not a secret, so origin binding, per-IP and per-project rate limits, and a daily token budget are what protect you. Read the security guide before putting an instance on the internet; in production a project with no allowed origins refuses to answer.

How do you handle prompt injection?

Two layers, neither a guarantee. Retrieved context reaches the model as data in a user turn with a fixed system prompt, and instruction-shaped chunks are flagged at ingest. The answering path is read-only, so the blast radius of a successful injection is a wrong answer. Write actions stay unshipped until the permission model has real miles on it.

How do I report a vulnerability?

Email security@deskbell.dev with the details and a reproduction if you have one. Please don't open a public issue for anything exploitable. We'll acknowledge, work a fix, and credit you unless you'd rather we didn't.