Station 01 — the wire
A visitor asks, and the browser watches it happen.
The widget is one script tag on the tenant’s own site, rendered inside a shadow root so the host page’s stylesheet cannot reach in and its own cannot leak out. It mints an anonymous session judged on the page’s origin — no email, no password, no sign-up wall in front of a question.
POST /widget/turns/stream
- event: status{ "state": "connecting" }
- event: status{ "state": "working" }
- event: retrieval{ "top": "DOC-015#1", "score": 2.277, "margin": 0.127 }
- event: gate{ "placement": "answer", "ruling": "answer" }
- event: token"The importer looks for the required headers"
- event: token" email, name, and company."
- event: done{ "outcome": "answered", "latency_ms": 3490 }

Station 02 — the gate
Three outcomes, and it commits to one.
The gate decides on free signals first — the top retrieval score, the margin to the next chunk, whether the question is sensitive — and only spends a second model call on the genuinely uncertain few. Here is the same instrument on a question it refused.
The question
“How long is the refund window on an annual plan?”
- top score
- 1.623
- margin
- 0.005
- sensitive
- 0.000753
The ruling
Retrieval was not weak — it scored 1.62. But the margin to the next chunk was 0.005: nothing in the corpus distinguished itself. And the question moves money. It declined rather than reached.
Prose the model writes outside a tool call is never posted to a customer. That is the guarantee the whole layer is built around: the only path to a customer’s inbox is a tool call carrying retrieved content, so the system cannot claim an answer it did not give.
93.6%
595 of 600 cases
Correct disposition — answered, clarified or escalated as it should have been.
eval/harness_results.md
0
across the adversarial suite
Successful prompt injections.
injection/ · nivara-ai
94.2%
against a 92.9% dense baseline
Retrieval recall@1, after the hybrid path earned its place.
eval/retrieval_ablation.md
Station 03 — the hand-over
A person picks it up where it stopped.
An escalation arrives as an ordinary ticket carrying an internal note with what the model already found, so the agent starts where it stopped rather than from the top. The queue is filtered, cursor-paginated and live — and deliberately does not rearrange itself under a reader mid-scroll.


Station 04 — the score
And a number the system cannot tune.
Deflection is read from the API’s own analytics, over a window whose start is a committed constant, using the API’s definition verbatim. The AI layer never holds the credential that reads it, so the thing being measured cannot quote its own score. Where the live rate and the offline one disagree, the gap is published beside them rather than subtracted.

Isolation
A forgotten filter cannot leak another tenant’s rows.
Isolation is a property of the database rather than a discipline in application code. Every tenant-scoped table has Postgres row-level security enabled and forced, with a policy predicated on a transaction-local setting, so the rows simply are not returned.
01 of 1
Row-level security, forced
Every tenant-scoped table has RLS enabled and forced. The application connects as a role that is neither superuser nor table owner, so it cannot bypass a policy even by mistake.
The tenant is never a client value
It is read from the auth token or the channel context and armed transaction-locally. Nothing a caller sends can widen what a request can see.
404, not 403
A row another tenant owns returns not-found rather than forbidden, because a 403 confirms the record exists and turns the API into a probe.
Two credentials, not one
The token that answers tickets and the token that reads analytics are separate grants, so the credential on the request path cannot read the numbers its own work is scored by.
Injection resistance by contract
Sensitive tools expose no granted branch, so a prompt-injection has nothing to steer the system toward. Zero successful injections across the adversarial suite.
An audit trail with attribution
Append-only, with the actor enforced by database trigger rather than supplied by the code path being audited. An absent actor raises rather than defaulting.
Install
One script tag, and it answers on your own site.
The widget mounts inside a shadow root, so the host page’s stylesheet cannot reach in and its own cannot leak out. Sessions are judged on the requesting origin against that tenant’s allowlist. This is the real file the demo host loads — view its source and you will find these three lines.
index.html
<script
src="https://nivara-web-nextjs.vercel.app/widget/widget.js"
data-tenant-id="YOUR_TENANT_ID"
defer
></script>Widget
shadow rootAn anonymous session from a tenant's own site, origin-checked against that tenant's allowlist.
Slack
Events APIA message in a connected channel opens a ticket, and the reply comes back to the thread it started in.
REST + OpenAPI
generated typesThe whole surface is documented and the front end's client types are generated from it, with a scheduled job that fails when the two drift.
WebSocket
rooms with replayPer-room sequence numbers and a bounded replay buffer, so a reconnecting client resumes rather than guesses.
What it costs to run
Nothing, so far.
There is no price list here because nothing is for sale. What there is instead is the actual bill: every rung of the model chain is a free tier, so the modelled cost below is what these answers would cost at each provider’s published list price, quoted beside a real spend of zero.
0.00 USD
actual spend to date
Every provider rung, the vector store and all three hosts are on free tiers.
eval/scoreboard.md
0.00026 USD
modelled, at list price
The answered Turn above, priced at the provider's published rate for its real token count.
src/nivara_ai/turn/cost.py
26–39%
across eight routed categories
Cheaper again with the model router on, measured at no accuracy regression.
eval/router_ablation.md
The free instances sleep after fifteen idle minutes, which is the honest cost of this particular bill: a genuine cold start can take up to a minute. A scheduled ping keeps the API warm; the AI service is left to sleep.
The architecture
Four front ends, one API. What separates them is what they hold.
Not what they render. Every surface resolves its tenant server-side from the credential on the request, and the tenant is never a value the client passes. Isolation is enforced in Postgres with row-level security, so a forgotten filter in application code cannot return another tenant’s rows.
- PortalContact · password
Raise a ticket, read the replies, answer them.
- DashboardUser · agent or admin
Work the queue, reply, leave internal notes.
- WidgetVisitor · anonymous session
Ask from a tenant's own site. No account first.
- AnalyticsUser · analytics:read
Four rates, two percentiles, and what an empty cohort is not.
The source
Three repositories, and every number in them reproduces.
The evaluation harness runs from a clean clone with no provider key and no credential of the author’s — it replays committed recordings — so the figures on this page are checkable rather than claimed.
- nivara-api-nestjsNestJS · Postgres · Prisma
Row-level security on every tenant-scoped table, cursor pagination, WebSocket rooms with replay, SLA clocks, an append-only audit log.
- nivara-web-nextjsNext.js · TypeScript · Tailwind
Four front ends and one typed client, generated from the API's own OpenAPI document. The widget ships separately and mounts in a shadow root.
- nivara-aiPython · FastAPI · Qdrant
Retrieval, the agent loop behind an MCP tool surface, the gate, and an eval harness that reproduces every number with no provider key.
Questions
The things worth asking.
Is this a real product I can buy?
Are these figures real, or illustrative?
What stops the model from claiming it answered something it did not?
What happens to a question about money?
Why is the first request slow?
Can I run it myself?
docker compose up in the API repository migrates the schema, seeds two tenants and comes up with no credentials and no keys. Optional integrations stay dormant when unconfigured rather than fatal, which is what makes a key-free start possible.Ask it something it cannot answer.
The interesting half of this system is what it refuses. Open the widget and ask about a refund — then watch the ticket arrive on the agent queue.
Demo admin, seeded. Nothing to create, nothing to pay for.



