Security and data handling
What Strata stores, where it goes, and what we do and do not promise.
Written for the technical or legal reviewer at a company deciding whether to put Strata on its site. Every claim here is checkable against the code that runs the product. Where something is not true yet, this page says so instead of rounding up. Last verified against the code: 6 September 2026.
What we store
Everything lives in one PostgreSQL database (Supabase, AWS us-east-2), scoped per workspace. Connecting your own Azure Blob Storage container adds a copy rather than moving anything out of that database: the original file bytes go to your container and the path is recorded, while the database still holds the document's extracted text and its retrieval chunks.
- Accounts: email address and a bcrypt password hash. No plaintext passwords, ever.
- Workspace knowledge: the documents, FAQs, and crawled pages you add, chunked for retrieval. Three kinds: public FAQ content, page captures, and user-private documents. User-private is a label on the row, not an access control we enforce on the public chat route: separation between members is not enforced there today, so a user-private document should not be treated as protected from other people who can reach the same workspace. Separately, a session whose role is owner, admin, or staff retrieves user-private documents regardless, and that role is read from the sign-in cookie rather than checked against membership of the workspace being asked. Workspace scoping is applied on every retrieval query; member scoping on this route is not. Do not put anything in a user-private document that would be a problem in someone else's hands.
- Conversations and messages: what visitors ask and what the assistant replies, with retrieval confidence and citations. Anonymous visitor threads carry no account identity. Where a thread does carry one, see the attribution caveat under Authentication and sessions: it is a label, not proof.
- Support queue: questions a visitor escalated, with the email address or phone number they chose to leave.
- Agent audit log: one row per agent decision round. It holds a truncated goal preview (120 characters), the page URL, the decision kind, the control the agent acted on and what happened, the model and provider that served the decision, and an inventory of up to 40 controls the agent could see on that page, each with its visible label and kind. Those labels are your own button, link, and field text, so over time this table holds a structural map of the pages the agent works on. It records no field values. If a capability returns an error, up to 300 characters of that error text are stored with the row.
- Member memory: a fixed allowlist of facts (name, phone, email, and a few ordering preferences) a signed-in member's assistant has learned, held against the membership and deleted with it. Which member's memory loads into a question is decided by the same unverified member id described above, so it carries the same limitation. The whole feature is off unless a flag turns it on. When it is on, the widget also reads that same allowlist from a Strata key in the visitor's own browser, on your domain, and sends whatever it finds along with the question, so those facts reach us and the model provider like any other part of the prompt. Nothing in the widget we ship writes that browser key today; the code only reads it.
What the widget sends from your page
The widget runs inside your page, so this is the part your own privacy notice has to cover. Its calls to us go to the Strata API, and data travels onward only as described below. Two things in this list are not calls to us at all: the widget loads its fonts from Google, and voice uses the browser's own speech recognition. Neither is a service we contract with.
- Secret field values never leave the page. A field typed as a password, or named or labelled like a password, passcode, one-time code, or API key, is stripped in the browser. The assistant is told the field has a value, never what it is. That list is exact: it does not match payment or identity fields by name, so a card number or a government ID number typed into a plain text field is not covered. Do not put Strata on pages that collect those.
- Other field values do travel, on one of the two paths. When the assistant answers a question, the current value of each non-secret field on the page is sent with the page context, truncated to 40 characters, and shown to the model. A half-typed name, email, or note can reach the model that way. When the assistant decides what to click, a free-text value is replaced by the marker
(set)instead. A checkbox or radio still reports whether it is ticked, and a dropdown still reports which option is selected, because those are your page's own words rather than something the visitor typed. The two paths do not agree today, and the answering path is the looser of the two. - Page text is structure by default, not prose. The assistant normally sees headings, controls, and cart state. A wider slice of page text sits behind a flag that is off by default. Separately, a visitor can press "Read this page", which sends the visible text of the page they are on to us and stores it in your workspace as a page capture, which is then kept. That only happens when the visitor asks for it.
- Images are off by default. There is no camera access and no screenshot of the visitor's screen. Behind the flag, the widget can send the URLs of up to six visible product images, or a small composite of visible images, to the model. With that flag on it is not a rare fallback: the first look at a page in an agent turn already asks for it, and so does a retry after an action the assistant could not confirm worked, or the assistant asking to look more closely.
- The widget loads its fonts from Google. Its stylesheet imports three families from
fonts.googleapis.com, and the font files come fromfonts.gstatic.com. Every visitor who loads a page carrying the widget therefore makes a request to Google carrying their IP address and browser details, with your site as the referrer. Google is not a service we contract with, and the fonts are not self-hosted yet. - Voice uses the browser's own speech recognition. In Chrome and Edge that streams the audio to the browser vendor's speech service. That is the vendor's data flow, not ours, and it is not covered by the list below. The sign-up card can be filled by voice, and a spoken password fills the form on the page without ever becoming a message or reaching us, but the audio still left the device. Type passwords rather than speaking them.
Authentication and sessions
- Session cookies are HMAC-SHA256 signed and httpOnly. A tampered, unsigned, or legacy cookie is rejected. The payload is signed, not encrypted, so anyone holding the cookie can read the email address, role, and workspace list inside it.
- Cookies are
SameSite=Noneand the API answers cross-origin requests with credentials, reflecting back whichever origin asked. That exists so the widget can call the API from your domain, but it is set on every API route, the admin API included, so a signed-in admin's browser can be made to call that API from any other site they visit. There is no CSRF token layer and no origin allowlist yet. - Role and workspace are written into the cookie at sign-in, and it lasts seven days. Some admin routes re-check membership in the database on every request; most read the cookie. There is no server-side session store, so removing a member or changing a role takes effect at their next sign-in, not immediately.
- Passwords are hashed with bcrypt. There is no password recovery flow yet; resets are manual.
- Six roles: OWNER, ADMIN, STAFF, EMPLOYEE, STUDENT, END_USER. ADMIN, STAFF, and EMPLOYEE are the invitable ones, with EMPLOYEE shown as Member in the team UI. STUDENT is created by widget sign-up. END_USER is chat only.
- Member identity on the public chat routes is not bound to the session yet, so an unverified id decides both whose private documents and whose stored memory an answer is built from. Read "this message came from this member" in a transcript as a label, not as proof of identity.
Keys you give us
Workspace LLM keys and capability bearer tokens are write-only at the API: they are stored, used server-side to call the provider you chose, and never returned by any endpoint, including to the admin who set them. The widget never sees them.
Write-only is a property of the API, not of the disk. Those keys, and an Azure storage connection string if you connect one, sit in ordinary database columns with no application-layer encryption on top of the database's own.
Where data goes
Data leaves our system to the services below, each for the purpose named.
| Service | Purpose | What it sees |
|---|---|---|
| Vercel | Application hosting, and web analytics on our own site | All request traffic to our API and dashboard. Analytics covers page views on strata.fyi only (path, referrer, coarse location, device). The widget on your site does not call it. |
| Supabase (AWS us-east-2) | PostgreSQL database | All stored data above. |
| Groq | Default LLM inference, image understanding included | Conversation text and retrieved knowledge chunks, per request. Images too, when the image flag is on: image requests go to whichever provider is configured, and Groq is the default. |
| OpenAI | Retrieval embeddings, moderation, and image understanding, when an OpenAI key is configured | The text of every knowledge chunk at ingest, because embeddings are pinned to OpenAI. Questions sent to the answering route, for the moderation check, which runs on that route and nowhere else. Images, when the image flag is on and OpenAI is the provider in use. |
| Anthropic | Optional LLM | Same as Groq, when selected. Selected either per workspace with your own key, or across the deployment by our own configuration. |
| Azure Blob Storage | Optional document storage, per workspace | The bytes of documents uploaded to a workspace that has connected a container. |
| Resend | Email (support replies, admin notifications) | Recipient address and message content. |
Three further paths can send data somewhere not on that list, and all three are switched on by a workspace admin. A capability is an HTTP endpoint you register, and it receives the visitor's question, the conversation so far, and a user identifier on every invoke. A custom LLM base URL points inference at any host you choose. The crawler fetches the URLs you give it, identifying itself as chat-widget-crawler/1.0. Two further recipients sit outside that list because the visitor's browser reaches them rather than our servers: the speech service behind the microphone, and Google's font service, which the widget's stylesheet loads from on every page it appears on.
LLM requests are inference calls. We have no training pipeline, so nothing you give us is used to train a model by us. Whether a provider trains on API traffic is governed by that provider's own API terms, which you should read for the providers you enable. Nothing in our code sends a no-training or no-retention instruction on your behalf.
What we do not have yet
Stated plainly so nobody has to discover it.
- No SOC 2, ISO 27001, or HIPAA certification. We will not claim these until an auditor says so. Do not send us regulated health data.
- No third-party penetration test on record yet.
- No database row-level security. Workspace and member scoping is written into the application's own queries. The database enforces no policy underneath them, so a query written without the scope is not caught by anything below it.
- No automated data retention or expiry. Data persists until deleted by an admin or by workspace deletion, which cascades to everything under it. That includes the agent audit log, which has no expiry at all. Deletion on request: email us or the workspace owner. We do not publish a deletion window, and there is no automated process behind the request: someone does it by hand.
- No self-serve erasure for a visitor. Someone who chatted anonymously has no account to sign in to, so the request has to come through you or through us by email. Whatever the widget left in their own browser is theirs to clear with the site data for your domain; we cannot clear it for them.
- No breach notification window in writing. We have not committed to one yet. If your review needs a number, put it in the contract.
- No subprocessor change notice. The table above changes when the product changes. There is no notification or objection process yet.
- No formal SLA or public status page. There is a health endpoint; monitoring is internal.
- Single region (US), though not literally one region. The database is in AWS us-east-2 and our serverless functions run in Vercel's default US region. Both are in the United States. There are no data-residency options, and no transfer mechanism is named for visitors outside the US.
Operational practices
- The seed, document-processing, and migration scripts refuse any remote database target unless explicitly forced with
I_MEAN_PROD=1and a named host. That guard is per script, not a property of the database, so it covers the scripts that import it and nothing else. - A visitor's IP address is read from the request to rate-limit abuse and held only in process memory. It is not written to the database. Our host keeps its own request logs, which is separate from this page and governed by their terms.
- Every agent decision that acts on a page is audit-logged with its outcome, and the analytics page reads those same rows, not a parallel set of books. The write is best effort: if it fails, the turn still completes and the failure is logged as a warning rather than costing the visitor an answer.
SESSION_SECRETis a required production environment variable. The app fails closed and refuses to run auth without it, rather than falling back to something guessable.- Full-fidelity capture of agent requests, including the conversation and the whole page observation, exists for debugging behind an environment variable. It writes nothing unless that variable is set.
Reporting a vulnerability
Email lee.jungro@gmail.com. We read everything and answer honestly about impact and timeline.