Auth, API keys, rate limits, and the safe generative UI model.
Breeze treats the backend as a private service and checks every access at the layer that owns it. This page documents the guarantees, and the defaults you should harden before it faces anything but localhost.
The defaults are tuned for one person on one machine
They are not wrong, but they assume a trusted network. The production hardening section is the part to read before you expose this.
NextAuth with the Credentials provider and JWT sessions.
bcryptjs at cost factor 12. Signup requires a
name, a valid email, and a password of at least 8 characters.NEXTAUTH_SECRET. Nothing
is stored in a session database.getServerSession(authOptions) and returns
401 without a session.Conversations are scoped to the signed-in user, and every CRUD route enforces it.
| Situation | Response |
|---|---|
| No session | 401 |
| Another user's conversation | 403 |
| Missing conversation | 404 |
Zod validates PATCH bodies with a strict schema that rejects unknown fields,
and message deletion validates fromId as a Mongo ObjectId.
The FastAPI backend is never exposed to the browser. Requests go through the
Next.js API routes, which add the X-API-Key header. The backend compares it
against API_KEY with a constant-time comparison and returns 401 on any
mismatch.
| Endpoint | Protected |
|---|---|
/completion | Yes |
/summarize | Yes |
/ | No, by design |
/health | No, by design |
Per client IP, via slowapi.
| Endpoint | Limit |
|---|---|
/completion | 10/minute |
/summarize | 20/minute |
The model emits data, never JSX
Nothing the model produces is ever evaluated.
lib/genui/schema.ts.type renders as collapsed JSON.Full detail in the Generative UI guide.
Search results and fetched pages are attacker-controlled input: a page can carry text written specifically to be read by a model. Two surfaces, handled separately.
Every retrieved byte passes one choke point, backend/evidence.py, so a defence
added once covers Tavily, the keyless fallback and fetch_url alike.
Containment is structural first. Web text is only ever carried in a user turn
-- never a system message, never a tool message -- fenced, numbered and
length-capped, with a standing system-prompt rule that text inside the fence is
data and never instructions.
On top of that, the sanitiser strips what would let text stop being text:
| Stripped | Why |
|---|---|
<|im_start|> and other control tokens | Would open a forged turn inside the prompt |
<think> / </think> | Would misroute answer text into the reasoning panel |
```breeze-ui fences | A page could otherwise inject a widget into an answer |
| The evidence fence itself | Content must not be able to close its own quoting |
| Zero-width and bidi characters | Hide text from a human reviewing the page or log |
The widget fence is the one specific to Breeze
A breeze-ui fence renders. Without neutralising it, any page the model reads
could publish a chart, table or card into an assistant reply.
fetch_url is the only place the backend requests an address a model chose, so
backend/webfetch.py assumes every URL is hostile.
file://, and no
port 6379 or 11434 -- the Redis or Ollama instance next to the process.http://expected.com@attacker.com cannot pass a human skim.169.254.169.254 is the cloud metadata endpoint), CGNAT,
multicast and reserved space, in IPv4 and IPv6 including IPv4-mapped. Every
answer, so a name resolving to both a public address and 127.0.0.1 is refused.302 -> http://127.0.0.1:11434 is the standard way past a check that
only inspects the URL it was handed.Content-Length is
attacker-supplied), content-type allowlist.HTML is reduced to text by a stdlib parser that drops script/style, comments,
and display:none / aria-hidden elements -- the standard hiding places for
instructions invisible to a person checking the page.
Keep these out of version control. The repo's .gitignore already excludes
.env and .env.local.
| Secret | Protects |
|---|---|
NEXTAUTH_SECRET | Signs your JWT session tokens. |
OLLAMA_API_KEY / API_KEY | The shared frontend-backend key. |
TAVILY_API_KEY | Your web search credential. |
LANGFUSE_* | Access to your traces. |
/completion should be reachable from the Next.js process only, not from the
public internet. The API key is a second line of defence, not the first.
Rate limits are keyed by IP. Behind a load balancer, every request looks like it came from the proxy, so the limit applies to the proxy as a whole rather than per user.
There is no headers() block in next.config.ts -- no CSP, no HSTS. Add
one, or terminate on a platform that sets them for you.
If you enable tracing, your prompts and completions go to whatever
LANGFUSE_BASE_URL names. Confirm it is yours. See
Langfuse.