If your service status and other operational data live in PostgreSQL or Supabase, you will want a customer-facing AI agent to show it, next to answers from your knowledge base. Connecting an LLM agent straight to your database tables is the risky way to do that: chat text from anyone on the internet becomes a prompt-injection route, and the agent can leak data it should never have seen. The safer pattern combines pgvector hybrid search for the knowledge base with a read-only API in front of the database and the narrowest credentials you can give it.

LayBuild does not connect directly to your Postgres or Supabase database credentials, and an earlier version of this post implied it could. What LayBuild does is call an HTTP endpoint you own, an "API tool", and render the structured result alongside the AI's response. So the secure way to bring database data into a support chat is to put a small read-only service in front of the database and give LayBuild a token for that service, never a connection string. Below is how to build that service, and what the tool mechanism can't do today.

How API tools work in LayBuild

An admin configures a tool with a base URL, a path, an HTTP method, custom headers, optional parameters with default values, a timeout (1 to 60 seconds, 10 by default) and one of four auth types: none, bearer token, basic auth, or an API key in a header you name. Then they attach the tool to an AI agent.

At runtime the flow is fixed:

text
customer message
      |
      v
retrieve from knowledge base --> generate answer (LLM)
                                        |
                                        v
                 call every attached tool in parallel, with default parameters only
                                        |
                                        v
                 first successful (2xx) response is attached to the reply
                                        |
                                        v
       chat window shows the answer, with the tool's JSON rendered underneath

That design has consequences you need to plan around:

  • This is not LLM function calling. The model doesn't decide whether to call a tool and can't fill in arguments. Every attached tool is called on every AI reply, with the default values you saved.
  • No customer context is sent. The request carries no question text, no customer ID and no conversation ID, and LayBuild sends no request body. A tool can't look up "my order" or "my account".
  • The model never sees the result. Tool data is attached to the message after the answer is generated, so the AI's text won't mention or reason about it.
  • The customer sees what you return. The response JSON is rendered under the reply. Treat the endpoint's output as public.
  • In production, tool URLs must point at a public host. Private IP ranges and localhost are rejected.
  • LayBuild logs each call with the request URL and the first 2,000 characters of the response.

Plans include 5 (Starter), 25 (Pro) or 100 (Premium) API tools.

What this is good for

Given those rules, the right data is current, non-personal and the same for every customer: open incidents and service status, today's delivery cut-off, maintenance windows, stock levels for a fixed product list, a price list your database is the source of truth for.

Customer-specific lookups (order status, balances, subscription state) aren't possible through LayBuild tools today. Route those to a human with handoff, or show them in your own signed-in UI.

Step one: a view and a read-only role

Don't point the endpoint at your tables with your application's credentials. Give it a view that contains only the columns you'd show a customer, and a role that can read that view and nothing else:

sql
create schema if not exists support_api;

-- Only the columns a customer may see. The view runs with its owner's
-- privileges, so the reader role needs no grant on public.incidents.
create view support_api.open_incidents as
  select title, status, started_at
  from public.incidents
  where resolved_at is null;

create role support_reader login password 'use-a-long-random-password';
grant usage on schema support_api to support_reader;
grant select on support_api.open_incidents to support_reader;

-- Belt and braces: even a bug in the endpoint can't write or run long.
alter role support_reader set default_transaction_read_only = on;
alter role support_reader set statement_timeout = '2s';

This works the same in Supabase's SQL editor. Two Supabase-specific notes: don't add support_api to the schemas exposed through Supabase's auto-generated API, since the endpoint below should be the only way in; and when you build the connection string for support_reader, check Supabase's connection docs for the username format your connection pooler expects.

Step two: the endpoint

A Bun handler with a bearer token, a fixed parameterized query, a row limit and a short cache. The cache matters because LayBuild calls the tool on every AI reply.

ts
import { SQL } from 'bun';
import { timingSafeEqual } from 'node:crypto';

const databaseUrl = process.env.SUPPORT_READER_URL;
const token = process.env.SUPPORT_TOOL_TOKEN;
if (!databaseUrl || !token) throw new Error('SUPPORT_READER_URL and SUPPORT_TOOL_TOKEN are required');

const db = new SQL(databaseUrl);
const MAX_ROWS = 10;
const CACHE_MS = 30_000;
let cached: { body: unknown; expiresAt: number } | null = null;

function isAuthorized(req: Request): boolean {
  const given = Buffer.from(req.headers.get('authorization') ?? '');
  const expected = Buffer.from(`Bearer ${token}`);
  // Constant-time compare so the token can't be guessed byte by byte from timing.
  return given.length === expected.length && timingSafeEqual(given, expected);
}

async function loadIncidents(): Promise<unknown> {
  const rows: Array<{ title: string; status: string; started_at: Date }> = await db`
    select title, status, started_at
    from support_api.open_incidents
    order by started_at desc
    limit ${MAX_ROWS}`;
  if (rows.length === 0) {
    return { type: 'text', title: 'All systems normal', description: 'No open incidents.' };
  }
  return {
    type: 'key_value',
    title: 'Open incidents',
    fields: rows.map((r) => ({
      label: r.title,
      value: `${r.status} since ${new Date(r.started_at).toUTCString()}`,
    })),
  };
}

Bun.serve({
  port: 8787,
  routes: {
    '/support/incidents': {
      GET: async (req) => {
        if (!isAuthorized(req)) return new Response('Unauthorized', { status: 401 });
        if (!cached || cached.expiresAt < Date.now()) {
          cached = { body: await loadIncidents(), expiresAt: Date.now() + CACHE_MS };
        }
        return Response.json(cached.body);
      },
    },
  },
});

The response uses one of the shapes LayBuild's chat window knows how to render: key_value (a titled list of label and value pairs) and text here; table, list, link_card and product_card also exist. Anything else is shown as raw JSON, which works but looks like a debug panel.

Put it behind TLS on a public hostname. If you'd rather not run a server, a Supabase Edge Function can do the same job; Supabase's docs note that because Edge Functions run server-side, "it's safe to connect directly to your database" (Supabase docs, checked September 2026). Keep the same rules: read-only role, fixed query, row limit, bearer token.

Step three: attach it in LayBuild

In the dashboard, an admin creates the tool with the endpoint's base URL, path /support/incidents, method GET and auth type Bearer with your token, then attaches it to the AI agent. Use the tool's test action to check the response renders before customers see it.

The token is stored as tool configuration in LayBuild's database. Make it worthless anywhere else: generate it for this endpoint only, never reuse your database password or an application API key, and rotate it if an admin with tool access leaves.

Why we don't let a model near SQL

A common alternative is to give the model a database tool and let it write queries. We don't do that and don't recommend it. A support chat takes untrusted text from anyone on the internet, and a model that turns that text into SQL is one prompt-injection away from reading tables you never meant to expose. Even with function calling, the pattern that holds up is the one above: fixed, parameterized endpoints with the narrowest possible credentials. LayBuild's tools are stricter than that, since they don't take arguments at all, which is limiting but leaves little for an attacker to steer.

Frequently asked questions

How do you securely connect PostgreSQL and Supabase to an AI support agent?

Put a small read-only service or Supabase Edge Function in front of the database, with a role that can read one view and nothing else. LayBuild never receives database credentials or connection strings. It calls your fixed HTTP endpoint with the default parameters you saved (the model passes no arguments), and the endpoint returns a few columns under a row limit (10 in the example above).

Can an AI customer support agent query PostgreSQL or Supabase directly?

It can, but it shouldn't. Chat input can steer the SQL the agent generates through prompt injection. Put an API or a Supabase Edge Function between the AI agent and the database, with a read-only role, parameterized queries and a bearer token scoped to that one endpoint.

Why is direct SQL generation risky for customer-facing AI support agents?

When an LLM writes SQL from untrusted chat input, a user can use prompt injection to make it read internal tables, get around row-level permissions, or run queries heavy enough to act as a denial of service. Pre-defined API tools take SQL generation out of the path entirely.

How does LayBuild use PostgreSQL and pgvector for AI support?

LayBuild stores knowledge chunks and their embeddings in PostgreSQL with the pgvector extension. Each question runs a pgvector cosine search and Postgres full-text search in parallel, along with Q&A-pair and knowledge-graph lookups, and the results are merged with Reciprocal Rank Fusion. A BM25 leg exists but is off by default.

How do you prevent sensitive database leakage to customer support AI?

Apply least privilege: give the endpoint a role that can only read a view of the columns you would show a customer, set a row limit (10 in the example above) and a statement timeout, and return only data you would publish. The tool's JSON is rendered to the customer as-is, so treat it as public. LayBuild's PII redaction covers the AI's reply text, not tool responses.