Brod

Use case

A customer lookup tool for your support team

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.

What the tool does

A thin, read-only window onto data you already have.

  • One search box

    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 copied

    Nothing is copied into a new database. The app is a thin, read-only window onto data you already have.

  • The risky part is the key

    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.

The parts of Brod it uses

NeedHow it works on Brod
The CRM keyA 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 CRMThe 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 itAccess mode people, with the support group granted a role. Everyone else gets "App not found".
Who searched whatThe signed-in person arrives in x-brod-user-email, so the app can keep its own record of searches in env.DB.

The prompt

The conversation at the top of the page is the prompt. The agent stores the key with set_secret.

  • Named in upper snake case. Secret names are upper snake case, like CRM_API_KEY.
  • Set by builders and admins. Only builders and admins can set secrets.
  • Recorded. Each set and removal is recorded in the workspace's activity.

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.

// _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.

Keeping it to support

A new app is private to its owner and the workspace admins.

  • Opening it to support. Your agent can ask to open it to the support group, and a person confirms the link it returns.
  • Someone moves out of support. Take them out of the group.
  • Someone leaves the company. Remove them from the workspace and their access to every app ends on their next request.
  • A manager outside support needs it for a day. Grant that one person a role instead of switching the app to workspace.
  • Narrowing is immediate. Narrowing access applies immediately; widening it always waits for a person.

Who can see the key

  • Not readable back. Nobody can read the key back through Brod's API or MCP, including the agent that stored it.
  • Bound to the app. It is encrypted with the app's id bound in, so a value copied to another app does not decrypt.

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.

Questions

Does the staging version use a different key?

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.

How do I rotate the CRM key?

Ask the agent to call set_secret with the new value. The server code reads it from its next request; no redeploy is needed.

Can the app write to the CRM too?

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.

Can it be written in Python?

No. Server code on Brod is JavaScript or WebAssembly. A coding agent can write the handler above in JavaScript from a description.

Your AI already built the app. Put it in your team's hands.