The server code
The outside host, in brod.json (use your CRM's real API host):
{
"network": ["api.yourcrm.example"]
}
And the search handler in _worker.js.
Use case
Support agents need a customer's plan, open tickets and last order in one place, without a seat in every system. Your coding agent can build the search page. Brod keeps the CRM key out of the code and the page away from everyone outside support.
Build a customer lookup page for support and deploy it to Brod as customer-lookup. The server code calls our CRM's search API with the key in env.CRM_API_KEY; never send the key to the browser. Return only name, email, plan, open tickets and last order. Declare the CRM's API host in brod.json network. Log each search with the person's email in env.DB. Give the support group access. Here is the key: ...
deploy_app · set_secret · set_access
Deployed: https://customer-lookup.acme.brod.sh (version 1). CRM_API_KEY is stored encrypted and cannot be shown again. Giving the support group access needs your confirmation: open this link.
A thin, read-only window onto data you already have.
A support agent types a name, an email or an order number. The app asks the CRM's API, and shows the matching customers with the few fields support actually uses.
Nothing is copied into a new database. The app is a thin, read-only window onto data you already have.
The risky part is not the page. It is the CRM API key, which can usually read every customer you have. An AI-built tool tends to put that key in the front-end code or a committed .env file. On Brod it goes somewhere else.
| Need | How it works on Brod |
|---|---|
| The CRM key | A secret, stored encrypted and read by the server code as env.CRM_API_KEY. Values cannot be read back through the API or MCP, and get_files never includes them. |
| Calling the CRM | The app declares the hosts its code calls in brod.json. The operator can have Brod log or refuse calls to anything else, and the app's Network tab lists undeclared hosts it reached. |
| Who can open it | Access mode people, with the support group granted a role. Everyone else gets "App not found". |
| Who searched what | The signed-in person arrives in x-brod-user-email, so the app can keep its own record of searches in env.DB. |
The conversation at the top of the page is the prompt. The agent stores the key with set_secret.
CRM_API_KEY.The outside host, in brod.json (use your CRM's real API host):
{
"network": ["api.yourcrm.example"]
}
And the search handler in _worker.js.
// _worker.js
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname !== '/api/search') return new Response('Not found', { status: 404 });
const email = request.headers.get('x-brod-user-email');
const q = (url.searchParams.get('q') || '').trim();
if (q.length < 3) return Response.json({ customers: [] });
const res = await fetch(
'https://api.yourcrm.example/v1/contacts?search=' + encodeURIComponent(q),
{ headers: { Authorization: 'Bearer ' + env.CRM_API_KEY } });
if (!res.ok) {
console.log('crm error', res.status); // shows in get_logs, without customer data
return new Response('CRM unavailable', { status: 502 });
}
const { results } = await res.json();
await env.DB.run('CREATE TABLE IF NOT EXISTS searches (by TEXT, q TEXT, at INTEGER)');
await env.DB.run('INSERT INTO searches VALUES (?, ?, ?)', [email, q, Date.now()]);
// Send the browser only what support needs
return Response.json({
customers: results.map(c => ({
name: c.name, email: c.email, plan: c.plan,
openTickets: c.open_tickets, lastOrder: c.last_order,
})),
});
},
};
Two habits in that code are worth keeping. The browser gets a short list of fields, not the CRM's full record. And the log line has the status code, not the customer: server logs keep console output for 7 days, and builders and admins in the workspace can read them.
A new app is private to its owner and the workspace admins.
workspace.Whoever runs the Brod server can still read every app's secrets. On brod.sh that is the Brod team. If that does not fit your policy, Brod is open source and you can run it yourself.
No. Staging uses the same secrets, database and files as the live app. If you want tests to hit a sandbox CRM account, the code has to choose that itself.
Ask the agent to call set_secret with the new value. The server code reads it from its next request; no redeploy is needed.
Yes, if your code calls the write endpoints. Check x-brod-role on the server before any write, because Brod tells the app the role but does not enforce it inside the app.
No. Server code on Brod is JavaScript or WebAssembly. A coding agent can write the handler above in JavaScript from a description.