Skip to main content
wondeya puts an AI agent on a public page, speaking in a business’s name, answering from material that business uploaded. Three questions follow from that, and this page answers them: can one customer’s material ever reach another’s page, can the agent say something the business did not publish, and what happens when you delete something.

Workspace isolation

Your workspace is the boundary. Pages, knowledge, images, keys and members all belong to exactly one, and nothing crosses. That is enforced in four independent layers, each of which fails closed on its own:
1

At the edge

Every route that names a workspace verifies that the caller is a member of that workspace before anything else runs. A malformed identifier is rejected as a bad request, never turned into an internal error.
2

In the request context

That membership check is the only thing allowed to establish which workspace a request is operating in. Code that somehow runs without one fails; it never falls back to “everything”.
3

In the data layer

Application code cannot ask for data without a workspace, and cannot override the workspace it was given: the discriminator is not a parameter callers get to supply.
4

In the database driver

A final safety net re-applies the workspace filter to every query and every aggregation, including vector searches, so a query written by hand still comes back scoped.
Three consequences worth spelling out:
  • The vector index has no per-customer namespaces, it has a pre-filter. Your workspace is part of the filter of every single search, applied before similarity is computed. It is the isolation guarantee, not a performance detail.
  • Stored files live under a per-workspace prefix and are served through short-lived signed links, never from a public bucket. The one exception is brand art (your logo and favicon), which a public page needs at a stable address: a fixed route on your own site fetches the private object with a server-side signature, so the address is public and the bucket never is.
  • Every route that takes a resource id is born with a cross-workspace test. The case that matters is not a stranger asking for someone else’s data; the edge answers that. It is a legitimate member of workspace A pointing an A route at an id that belongs to B, which only the deeper layers can catch. That suite runs on every change, together with an adversarial HTTP suite that includes smuggled identifiers, operator-shaped ids and forged signed links.

The agent does not invent

The single most expensive failure for a product like this is an agent that promises a price, a guarantee or a delivery date the business never offered. Four mechanics prevent it:
  1. Answers are grounded in retrieved material. Figures, claims and attributions come from the passages your knowledge returned for that question.
  2. The model selects; it never writes addresses. Components come from a closed catalog, images from your registry by id, links from the list you approved by id. Their schemas make a URL structurally impossible to express, so the model has no field in which to invent one.
  3. When the material does not support an answer, the agent says so and offers a next step instead of filling the gap.
  4. Every component is validated, sanitized and resolved on the server, in that order, before it renders. A component that fails is dropped and the rest of the answer still arrives.
The same design is what makes hostile content in an uploaded document harmless. Instructions hidden in a PDF cannot produce markup, a script or an arbitrary link on your visitor’s page: the only channel out of the model is a typed component, and anything that does not validate never renders.

The AI notice

Every page a visitor can talk to, including the embedded widget, states that they are talking to an AI. The notice is server-rendered, so it exists before any JavaScript runs; it has no dismiss control, and there is no setting that can add one; and it renders regardless of the page’s theme. This is EU AI Act Article 50 transparency, applicable since 2 August 2026: a customer can restyle the page around it but cannot remove it. Document pages (plain markdown pages) carry no agent, so they carry no notice.

Sessions and credentials

The console session is a cookie, never a token in your browser’s storage. It is an opaque value in a __Host- cookie: HttpOnly, Secure, SameSite=Strict. The database keeps only its hash, so a database dump does not hand anyone a live session. It slides with activity up to a 30-minute idle window, with a hard 30-day cap, and it is revocable from the server the moment you sign out. Cross-site request forgery is blocked in three layers: the SameSite cookie, an origin check on every mutation, and a double-submit token bound cryptographically to the session. Machine credentials are wondeya’s own, hashed, and scoped: There is no platform-wide key, and a key with no scope is invalid: “no scope” never reads as “every scope”. Revoking takes effect on the next request. The MCP connection is OAuth 2.1. A browser sign-in and a console consent screen bind the connection to exactly one workspace, and every token is checked against this API as its audience. No MCP tool accepts a workspace identifier: the workspace comes from the credential, so pointing a tool at someone else’s workspace is not a request that can be expressed. API keys do not work on MCP; they belong to the REST API only. Details in MCP.

Untrusted input, handled as untrusted

  • Uploads are parsed off the request. A file is stored and queued; the queue does the reading. A malformed PDF cannot stall the API.
  • The declared type only selects a parser; it grants nothing. A file whose bytes do not match its parser fails as unsupported: a mislabeled file is refused, never renamed into working.
  • A URL you add is fetched through a hardened path: only http/https, private, loopback and link-local ranges refused, the resolved address pinned so it cannot change between the check and the connection, every redirect re-validated from scratch, and hard caps on time, size and hops.
  • Immediate refusals stay vague on purpose. Adding a blocked URL answers a generic “not allowed” with no reason, so the add endpoint cannot be used as a network scanner. A URL blocked later, during the crawl, records a machine-readable reason on the source (for example fetch_blocked_address:link_local) so its owner can act on it.

The widget boundary

The embeddable widget is an iframe on a dedicated origin, so it shares no cookies, no storage and no DOM with the page hosting it. Its document ships default-src 'none' plus only what it needs, and its frame-ancestors lists only the origins the agent’s owner registered on its site key. No wildcards are accepted, and an empty list becomes 'none', never *, so somebody holding a public site key still cannot frame that agent on a site of their own. Messages between the page and the frame use an explicit target origin in both directions and a versioned envelope, and nothing sensitive travels on it.

Abuse and spend

The endpoint that answers visitors is anonymous by nature, which makes it a target both for scraping and for running up someone’s bill. It is defended in layers: an origin allowlist, per-IP rate limits, an invisible bot check, a per-session cooldown, caps on input, output and history length, a cache for identical opening questions, and a global daily fuse. Beneath all of that, each workspace has its own per-minute limit, its own daily spend ceiling, and a message-credit balance that each turn spends atomically and that cuts at zero. A refused turn fails closed.

Data, deletion and models

Deletion is real. Deleting a knowledge source removes the source, its passages, the vectors inside those passages and the stored file, in one cascade. Because each passage holds its own vector, there is no window where the database has forgotten something a search index still remembers. Deleting an agent removes everything that belonged to it (knowledge, images, stored files and its widget site keys), while material attached to the whole workspace survives; deleting a page removes the page and its public address, never its agent’s material. The server owns the conversation. A visitor’s transcript lives in a conversations collection so the browser can never forge what the agent reads: the client holds only an opaque conversation id, and the history the model sees is derived server-side. The transcript expires on a sliding 24-hour clock (an active chat never expires; an abandoned tab does not live in the database), keeps at most 40 messages, and never contains the visitor’s IP address, only coarse dimensions such as country and device class. It is scoped to the workspace and to the visitor’s anonymous session. Turns can be traced to wondeya’s observability tooling for debugging and answer quality. Your material never enters a model’s weights. Knowledge reaches an answer only through retrieval at question time. There is no fine-tuning on customer data. That is a deletion decision as much as an isolation one, because what never entered the weights never has to be removed from them.

Reporting a problem

Found something that looks wrong? Write to team@wondeya.com with what you did and what you saw. Do not include credentials in the report; if a key may have leaked, rotate it first and say so.