Where Sonela runs
One rung is the page your users are already on. The other two are the choice you make
about your database, and they carry different trust boundaries — both are described
here rather than only the flattering one.
In the page
One script tag and a widget key — the whole change to your application. The page
holds nothing secret: the widget key is public by design, and the tenant identity
is bound server-side from a signed token the browser cannot mint. Editing the
page's JavaScript reaches no other tenant's rows, and no key of yours. An Origin
header is a courtesy, never identity: sessions held to signed-in users open only
on a signed voucher outsiders cannot forge — ours do, and yours can. The whole
mechanism is set out under tenant identity verification below. The person on the
page sees the guarantees too: a shield in the assistant's header opens them in
plain words, and when page capture is on, the person being read is told.
In your network — the Sonela Gateway
One small service, on one server of yours, next to the data it reads — never on a
staff machine. It dials out to us over port 443, so you open no inbound port and
expose nothing to the internet. What it needs to read your data is configured on
your side and stays there — run this way there is nothing for us to store, and a
row holding null cannot leak. We keep a hash of the gateway key,
and both validation and execution run on your side of the firewall.
On our infrastructure — a direct connection
The setup with nothing to install, and it costs you a trust decision the gateway
does not. You give us what your database expects at sign-in, and our servers open
the connection themselves. Those details sit on our infrastructure. We hold them
encrypted with AES-256-GCM, bound to the one workspace and data source they belong
to, and no API we expose reads them back — but encryption is what we do about that
exposure, not a reason there isn't one, and the exposure is the thing to weigh. We
refuse a target that points at our own database, and we test the connection before
accepting it.
- The three layers below all still apply — on this path they run on our infrastructure rather than yours.
- The hop to your database is secured by what you configure on it; we do not impose that setting, so set it if your review needs it.
- There is no fixed outbound address to put in your firewall today. We do not offer one and will not name one. If your review needs an allowlisted source, choose the gateway.
Read-only, enforced three times
Three independent layers: all of them have to fail before the assistant can change
anything, and each is stated per engine below rather than borrowed across engines.
Behind the gateway they run inside your network; on a direct connection they run on
ours. Which machine they run on changes; that they run does not.
1
An allowlist grammar, before the query runs
Model-written SQL is refused unless every part of it is explicitly permitted —
syntax nobody anticipated is denied by default.
- Must begin with
SELECT. Any semicolon is refused, so the input stays one statement.
- Every keyword and function must be on the allowlist, per engine, and each engine's system catalogue is unreachable —
pg_*, information_schema and dblink on PostgreSQL; xp_*/sp_* procedures, OPENROWSET, hints, WAITFOR and the sys* views on SQL Server, whose lexer refuses @ and [ outright; LOAD_FILE, INTO OUTFILE and the query-modifier keywords on MySQL, whose lexer refuses @, backticks and # comments outright.
- Schema-qualified names, comma-separated
FROM lists and parenthesised join groups are refused — each one evades the rewrite.
2
The engine's own guard, while it runs
Every question runs under a statement timeout of three seconds and a cap of 500
rows, so the timeout bounds what any one question can cost you. On PostgreSQL and
MySQL it also runs inside a READ ONLY transaction: the database
session itself refuses a write, whatever reached it. SQL Server offers no such
session mode, so there the guarantee is the two layers beside this one — the only
statement of the model's that can reach the server is a single validated
SELECT, and every session ends in a rollback.
3
A discard, not a commit
The transaction is thrown away rather than committed: the success path rolls it
back and every failure path unwinds it, and there is no commit in the query code
for either to reach. A write that somehow executed would not survive the request.
Tenant isolation by rewrite, not by prompt
Every table reference is replaced with a subquery projecting only the columns you
approved and filtering on the tenant column you declared. Isolation never depends on
the model behaving, and
a table absent from your manifest is rejected. The tenant value is a bound parameter,
server-bound from a signed session: the browser cannot assert it, the model never
sees it. If the database holds one organisation's data and you told us so, there is no
tenant column and no such filter — you are asked which it is, and nothing is read from
your schema until you answer.
What the model wrote
SELECT "Status", count(*) FROM "Orders" GROUP BY "Status"
What actually ran (PostgreSQL shown; on SQL Server and MySQL an integer flag compares with = 0)
SELECT "Status", count(*) FROM (SELECT "Id", "PlacedOn", "Status", "Total" FROM "Orders" WHERE "TenantId" = @__ai_tenant AND "IsDeleted" = false) AS "Orders" GROUP BY "Status"
Tenant identity verification — signed embeds
A widget key is public by design, so it can open a session but never prove who is
asking. Proof is a voucher your own server signs with a secret the page never holds,
and you climb to it in three states: with no secret the widget is open to the page it
is on; generating the secret arms the workspace and changes nothing a
visitor sees; enforcement is a separate switch you flip yourself,
after which an unsigned page is refused a new session. Turning enforcement on with no secret is
refused too — there would be nothing to verify against.
Two shapes your server can sign
Both are HMAC-SHA256 under your workspace's own signing secret. There is
deliberately no issuer check: the secret is the trust root, and a signature that
verifies is itself the workspace binding.
- A JWT. HMAC-SHA256 is the only algorithm accepted, the audience is pinned, and an expiry is required: a token whose expiry sits more than eight hours out is refused whole, and clocks are allowed thirty seconds of skew.
- A raw triple — user id, timestamp, signature — for a backend without a JWT library. The signature is HMAC-SHA256 over the timestamp and the user id joined by a colon; the timestamp is bare unix seconds, digits only; the signature is hex that must decode to exactly 32 bytes. Freshness is eight hours behind and thirty seconds ahead, and the comparison is fixed-time.
Verification always runs
A signature that arrives is verified whether or not the workspace demands one, and
a failure is refused — a bad voucher is evidence of spoofing, never noise: it is
written to your workspace's security feed, and five in 24 hours raise a probing
warning in your dashboard. A valid
one earns the vouched session even before you enforce; only the total absence of a
signature consults your enforcement switch.
- A partial triple — one or two of its three fields — is refused outright.
- If both shapes arrive, both must verify.
- A voucher sent to a workspace that has no signing secret is refused, not waved through.
- The refusal never says which check failed.
The secret, and rotating it
sk_ plus 256 bits from a cryptographic generator, shown exactly once,
at generation; at rest we hold it encrypted with AES-256-GCM, bound to its one
workspace. Rotation is an immediate cutover: the moment you
rotate, signatures made with the old secret stop verifying. There is no grace
window today, so rotate when your servers are ready to sign with the new one.
What a voucher proves — and what it does not
It proves who your application's server says is asking. It is identity
attestation, not transport secrecy: the voucher travels in the page, any script in
that page can read it, and there is no replay cache — so until it expires it can
be replayed by whoever captured it. The window is the expiry your signer chose,
up to the eight-hour ceiling, and it is the whole blast radius, so sign short.
- An allowed-origins list is a browser courtesy: it stops other websites, not other programs. The voucher is the one check a script cannot forge, because the secret never reaches the page.
- Until you enforce, an unvouched session still answers — rate-limited to a per-visitor-address slice (IPv6 grouped by /64 subnet) of the workspace's own request rate, so anonymous traffic divides your capacity and never widens it. A vouched session is served at the workspace's full rate, with no per-address slice.
What the assistant sees of the page someone is on
Two separate things, and they are worth separating: a picture of a screen, and the
data the page had already fetched for the person looking at it. One is sent. The other
is not sent anywhere at all.
A screenshot, and only when someone asks for one
Someone stuck on a record can press the camera in the composer and send the screen
with their question, so they do not have to describe what is in front of them.
There is no other way to take one: nothing automatic, nothing scheduled, nothing
on open.
- It captures the whole page, not only the part on screen. Our own widget is cut out of it; nothing else is.
- It rides one message and is detached from the composer as that message goes. It never joins the conversation history, so it is not re-sent with the next question.
- It goes to the same AI provider as the question — yours under your key, or ours. Never logged, never written to our disks, never cached; our metering has no field an image could ride in.
- The image is checked before it is used: an allowlist of PNG, JPEG and WebP, and a two-megabyte ceiling measured before and after decoding.
What the page's own data reader does — and where it stops
The bundle can read the data your application had already delivered to its own
user's browser, and hold it in memory for the life of that page. That is the whole
behaviour. It is transmitted nowhere. The request that carries a
question has four fields — the message, the recent turns, and a screenshot with
its type — and there is no field a captured entry could travel in. It is never
written to browser storage either, so closing the tab ends it.
- This is groundwork for a later capability, and it is honest to say it is groundwork. The assistant cannot answer from it today, and this page will not be quiet on the day that changes.
- Before anything is held, obvious secrets are stripped: values under names like password, token, secret, api_key, card or ssn, and values shaped like a signed token or a random key. Structure is trimmed too — depth, string length and array length are all capped, and only the names of query parameters are kept, never their values.
- That stripping is a heuristic and we call it one. It matches names and shapes, so it will not recognise personal data sitting under an ordinary business name — a notes field, a delivery address, a date of birth. It is deliberately biased towards removing too much. It is not a guarantee, and a review should not treat it as one.
Two switches over what the assistant sees, and what off actually does
Both are on when a workspace is made, because the assistant is more useful with them
and you should get the useful version first. Both are yours to turn off. They are set
for a whole workspace rather than for one embed, so every key in that workspace moves
together — you decide the trade between context and exposure, and you decide it once
per workspace.
| Switch |
Default |
What turning it off does |
| Reading the page's own data |
On |
On a production embed the reader is never installed, so nothing is captured at
all. The setting is held by us and read from us, so a page cannot quietly
restore it while the assistant runs normally. Nothing was being transmitted in
either state.
|
| Page screenshots |
On |
Refused at the API, whatever the page does: a question carrying an image to a
workspace with the camera off is answered with 400 and the picture reaches no
model. On the page itself the composer offers no camera once the setting
arrives, and the code that takes a picture is never loaded. One limit worth a
reviewer's time: a load whose setting never arrived still shows the button, so
a visitor there can attach a picture and be refused rather than see it greyed
out.
|
What Sonela can see — and what it cannot
Sonela is a data processor: your content transits our service to one AI provider —
yours under your key and DPA, or ours under our agreement and model choice.
| Data |
What actually happens |
| The question, and the rows that answer it |
In transit Reach Sonela and one AI
provider — yours under your DPA, or ours. Held in memory for one request,
never written to our disks.
|
| Stored questions, answers or SQL |
Never A metering row has counts, latency,
tokens, provider and model — and no column that can hold a question, an answer
or a query. It records which of your tenants asked, for billing.
|
| Written material you upload |
Stored Procedures and notes you give
the assistant are kept in your workspace and sent to that same provider with
the question. Workspace-scoped; deleting them removes them.
|
| Transcripts and cached answers |
Never Transcripts live in the visitor's
browser; an answer grounded in query results is uncacheable.
|
| A screenshot someone sends |
In transit Goes with that one message
to the same provider. Never logged, never written to our disks, never cached,
and it does not join the conversation history.
|
| What the page's data reader holds |
Never sent Held in the browser's memory
for the life of the page. The request that carries a question has no field it
could travel in.
|
| How your data is reached |
Your side The gateway you run reaches
it from inside your network, with a value configured and held there.
Or ours On a direct connection we hold
your database sign-in details, encrypted, and reach it ourselves.
|
| Your AI provider key |
Encrypted Yours: AES-256-GCM,
row-bound, never returned.
Never On our key: none of yours held.
|
| Write access to your data |
Never Three layers refuse it, and the
last of them discards the transaction rather than committing it.
|
| Your data used to train a model |
Never We train nothing, and your rows
reach one provider and no further.
|
What is written down instead: two append-only feeds in your dashboard that nothing
can edit or delete. One records every refusal at your workspace's doors — a forged
signature, a blocked origin, a query outside the allowlist — as a kind and a time,
never the text: a feed that stored payloads would itself be the leak it exists to
catch. The other records every switch an admin flips, with who flipped it and when.
On request — not shipped today
Need rows that never transit Sonela? Ask about the local data plane — the answer
loop, model calls included, runs inside your network.
Ask us for the threat model.
We will send the real one: what we assume, what we deliberately do not defend
against, and where the boundaries sit. We hold no security certifications today and
would rather say so — what we offer instead is specificity you can check against
the code.