Six months ago, exactly one person on your team could query the production database from an AI tool. Today it's everyone. Someone pasted a connection string into an AI assistant, it worked beautifully, and the pattern spread. Now three engineers, a product manager, and a support lead all ask a chatbot questions that quietly turn into SELECT statements against live data.
That's genuinely useful. It's also a governance blind spot. If someone asks "why did this customer's numbers look weird last Tuesday," can you answer who — or what — ran the query that touched their records? With scattered connection strings and shared credentials, the honest answer is usually "no idea."
This article is about closing that gap: how to give a team AI-powered database access while keeping it identifiable, scoped, and auditable. The goal isn't to slow anyone down. It's to make sure that when access grows from one person to twenty, you still know what's happening.
The failure mode: shared secrets and no paper trail
The default way people connect an AI tool to a database is to hand it a connection string:
postgresql://app_user:s3cr3t@db.internal:5432/production
Do that across a team and you inherit four problems at once. Everyone shares one database identity, so every query looks identical in the logs. The secret lives in prompts, chat histories, config files, and screenshots. Rotating it means chasing down every place it was pasted. And the database has no idea whether a human or a model issued a given statement.
The database's own audit log doesn't save you here, because from its point of view there's a single user, app_user, doing everything. You've lost the two facts that matter most for governance: which person the access belongs to, and whether their tool was allowed to do what it did.
What good looks like: a broker between the AI and the database
The pattern that fixes this is putting a broker — often an MCP (Model Context Protocol) server — between AI clients and the database. Instead of each tool holding raw credentials, they connect to one governed gateway that holds the connection and enforces the rules.
Model Context Protocol is an open standard for connecting AI assistants to external systems through a consistent interface. For databases, an MCP server exposes a small set of operations (list tables, fetch schema, run a read-only query) and becomes the single place where identity, permissions, and logging live.
Routing everyone through one gateway gives you five properties that scattered connection strings can't:
| Property | What it means in practice |
|---|---|
| Per-person identity | Each teammate authenticates as themselves (usually via OAuth), so every query is attributable to a human. |
| Least privilege | The broker can be read-only by design — SELECT passes, UPDATE/DROP get rejected before they reach the DB. |
| Central revocation | Off-boarding is one toggle. No long-lived secret to hunt down across chat logs. |
| Audit trail | One chokepoint logs who ran what, against which database, and when. |
| No shadow access | The database isn't network-exposed to every laptop; one gateway is. |
Managed MCP servers like Draxlr's implement this shape — connected over OAuth, read-only, with the credentials held server-side — but the pattern matters more than any product. You can build the same thing in-house. What follows works either way.
Building the audit trail
Whatever broker you use, the non-negotiable is a log of every query with enough context to answer "who did this and why." At minimum, capture the acting identity, the SQL, the target database, a timestamp, and a correlation ID that ties a query back to the conversation that triggered it.
A simple audit table looks like this:
CREATE TABLE ai_query_audit (
id BIGSERIAL PRIMARY KEY,
actor_email TEXT NOT NULL, -- the human, via OAuth
ai_client TEXT NOT NULL, -- claude, cursor, chatgpt...
database_name TEXT NOT NULL,
sql_text TEXT NOT NULL,
row_count INTEGER,
status TEXT NOT NULL, -- 'allowed' | 'rejected'
reason TEXT, -- why rejected, if it was
correlation_id UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_actor_time ON ai_query_audit (actor_email, created_at DESC);
The broker writes one row per attempt — including the rejected ones, which are often the interesting ones. Now the earlier question has an answer:
-- Who touched customer 4821's data in the last week?
SELECT actor_email, ai_client, created_at, status, sql_text
FROM ai_query_audit
WHERE sql_text ILIKE '%customer_id = 4821%'
AND created_at >= now() - INTERVAL '7 days'
ORDER BY created_at DESC;
actor_email | ai_client | created_at | status | sql_text
---------------------+-----------+---------------------+---------+------------------------------------------
dana@acme.com | claude | 2026-08-19 14:02:11 | allowed | SELECT * FROM orders WHERE customer_id...
support@acme.com | chatgpt | 2026-08-18 09:41:55 | allowed | SELECT status, total FROM orders WHERE...
You can build governance dashboards straight off this table — top queriers, rejected-write attempts over time, which tables get hit most, unusual after-hours activity. That's the observability layer security teams actually ask for.
Scoping access per team
Not everyone needs the same reach. Least privilege for AI access should be at least as sharp as the RBAC you give employees — arguably sharper, because a model will cheerfully try anything you let it. Two levers do most of the work: which databases an identity can see, and whether it's read-only.
A policy config for a broker might look like this:
roles:
support:
databases: [production_readonly]
mode: read-only
row_filter: "region = :user_region" # row-level scoping
analytics:
databases: [production_readonly, events_warehouse]
mode: read-only
engineering:
databases: [production_readonly, staging]
mode: read-only
Notice there's no write mode anywhere. For an AI exploration workflow, that's usually correct: the model can read the world and help you understand it, but it cannot modify a single row. If a prompt ever produces DELETE FROM subscriptions, the broker rejects it and logs the attempt rather than executing it.
Row-level filters are what make this safe for customer-facing use, too. Bind a filter like tenant_id = :current_tenant at the broker and a support agent asking "show me recent orders" only ever sees their own region's data — no matter how the model phrases the SQL.
What the workflow actually feels like
Governance shouldn't be visible to the person doing the work. From their side it's still plain English:
User: How many trial accounts converted to paid last month, by plan?
The AI client fetches the schema through the broker (so it uses real column names instead of hallucinating them), writes SQL, and the broker runs it read-only under that user's identity:
SELECT s.plan,
COUNT(*) AS conversions
FROM subscriptions s
JOIN users u ON u.id = s.user_id
WHERE u.trial_started_at >= date_trunc('month', now()) - INTERVAL '1 month'
AND u.trial_started_at < date_trunc('month', now())
AND s.status = 'active'
GROUP BY s.plan
ORDER BY conversions DESC;
The user gets their answer. Meanwhile, one row lands in ai_query_audit tagged with their email, the client they used, and a correlation ID. Nobody typed a credential; nobody can later ask "wait, who ran that?" and come up empty.
Common mistakes and gotchas
Logging the query but not the identity. A log full of anonymous SQL is barely better than none. The acting human's identity is the column that turns a log into an audit trail — capture it first.
Treating a shared service account as "the AI user." If every teammate's AI traffic authenticates as one account, you've rebuilt the connection-string problem with extra steps. One identity per person (or per agent), tied to your IAM.
Long-lived tokens in config files. Permanent keys can't be revoked cleanly and tend to leak. Prefer OAuth grants that expire and can be killed centrally the moment someone leaves.
Read-only in name only. "We told people not to run writes" is not a control. Enforce it at the broker so a rejected UPDATE is a logged event, not a trust exercise.
Ignoring shadow MCP. The moment a governed path exists, make it the only path. If people can still point tools straight at the database, your audit log has holes exactly where the risky queries are.
No retention or tamper-evidence on the log. An audit trail someone can quietly edit isn't much of an audit trail. Ship logs somewhere append-only, with a retention window that matches your compliance needs.
Key takeaways
Scattered connection strings give a team speed and take away accountability. You can keep the speed. Put a broker between AI clients and the database, make each person authenticate as themselves, keep access read-only and scoped by role, and log every attempt — allowed and rejected — with the identity attached. Whether you build that broker yourself or adopt a managed MCP server, the properties are the same: attributable, revocable, least-privilege, auditable.
The test is simple. If someone asks "who queried prod at 2 a.m. and why," you should be able to answer in one SELECT.
How does your team handle this today — shared credentials, a homegrown proxy, or a managed gateway? And what do you actually log per query? I'd love to hear what's working (and what's bitten you) in the comments.
Top comments (0)