{"slug": "read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes", "title": "Read-Only by Design: Letting AI Explore Your Database Without the Risk of Writes", "summary": "A developer outlines a defense-in-depth approach to giving AI assistants read-only access to production databases, using database permissions, read replicas, and query parsers to make writes structurally impossible. The post emphasizes that prompt instructions are insufficient and provides PostgreSQL and MySQL examples for enforcing read-only access at the database level.", "body_md": "There's a moment every developer hits the first time they connect an AI assistant to a real database: it works beautifully, the model writes a clean `SELECT`\n\n, you get your answer in seconds — and then a small, cold thought arrives. *What if it had written DELETE instead?*\n\nThat worry is healthy. An AI agent that can query your production database is also, by default, an AI agent that can `UPDATE`\n\n, `DROP`\n\n, and `TRUNCATE`\n\nit. Large language models are probabilistic. They hallucinate. They misread a vague prompt like \"clean up the test users\" as an instruction to actually delete rows. You don't want the only thing standing between a confused model and your `orders`\n\ntable to be good intentions.\n\nThe fix isn't to keep AI away from your data. It's to make write operations *structurally impossible* — read-only by design, enforced at layers the model can't talk its way past. This post walks through how to do that properly, from the database grant all the way up to query-level guardrails.\n\nThe tempting shortcut is to add \"only run SELECT queries, never modify data\" to your system prompt and call it a day. Don't rely on this. Prompt instructions are suggestions, not enforcement. A cleverly worded user request, an injected instruction hidden in some data the model reads, or a plain misunderstanding can all lead the model to generate a destructive statement anyway.\n\nReal read-only access is enforced *below* the model — in places where no amount of clever text can override it. Think of it as defense in depth, with at least three independent layers:\n\n| Layer | What it stops | Enforced by |\n|---|---|---|\n| Database permissions | Any write reaching the engine | SQL `GRANT` /`REVOKE`\n|\n| Connection / replica | Writes even being routed to a writable node | Read replica, read-only transaction |\n| Query parser / broker | Non-SELECT statements before they run | SQL parsing, allowlists |\n\nAny one of these is decent. All three together mean a write has to defeat your database engine, your routing, *and* your parser simultaneously — which is a very different threat model than \"the model promised.\"\n\nStart at the bottom. Create a database role whose entire vocabulary is `SELECT`\n\n. This is the single most important step, because it's enforced by the database engine itself and applies no matter what SQL arrives.\n\nIn PostgreSQL:\n\n```\n-- Create a login role with no inherited privileges\nCREATE ROLE ai_readonly WITH LOGIN PASSWORD 'use-a-secret-manager';\n\n-- Let it see the schema, but nothing more\nGRANT CONNECT ON DATABASE app_production TO ai_readonly;\nGRANT USAGE ON SCHEMA public TO ai_readonly;\n\n-- Read-only on existing tables\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_readonly;\n\n-- And on tables created later\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n  GRANT SELECT ON TABLES TO ai_readonly;\n```\n\nNow prove it. Connected as `ai_readonly`\n\n, a write simply bounces:\n\n```\nDELETE FROM orders WHERE created_at < '2025-01-01';\n-- ERROR: permission denied for table orders\n```\n\nThat error is the whole point. The model can generate the most confident `DELETE`\n\nin the world and Postgres will refuse it. The equivalent in MySQL is `GRANT SELECT ON app_production.* TO 'ai_readonly'@'%';`\n\n— same idea, same guarantee.\n\nA subtle but important detail: grant `SELECT`\n\non *specific* tables or schemas rather than handing over a blanket \"read everything\" role. Your AI assistant probably doesn't need to read `password_resets`\n\nor `internal_audit_log`\n\n. Scope the grant to the tables that answer real questions.\n\nPermissions stop writes, but you can also stop writes from ever reaching a writable machine. If you run a read replica — standard on managed Postgres and MySQL — send all AI traffic there.\n\n```\n# Analytics / AI connection string points at the replica\nDATABASE_URL=postgres://ai_readonly@replica.db.internal:5432/app_production\n```\n\nThis buys you two things. First, a replica is physically read-only; even a superuser can't write to it. Second, you isolate the load. An AI assistant exploring data with a few accidental full-table scans won't compete with your production write path. If you're on SQL Server Always On, the `ApplicationIntent=ReadOnly`\n\nconnection property routes the session to a secondary and refuses to promote it to the primary — a nice belt-and-suspenders check.\n\nFor a single-node database with no replica, you can still force each session into a read-only transaction:\n\n```\n-- Postgres: this session cannot write, full stop\nSET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;\n\nINSERT INTO events (name) VALUES ('test');\n-- ERROR: cannot execute INSERT in a read-only transaction\n```\n\nThe top layer is where the Model Context Protocol (MCP) and similar \"broker\" architectures shine. Instead of the AI holding a database connection directly, it talks to an intermediary that holds the credentials, inspects every query, and executes only what's allowed.\n\nA good broker parses the SQL — not with a fragile regex, but with a real SQL grammar — and rejects anything that isn't a plain `SELECT`\n\n. That catches the sneaky cases a keyword blocklist misses:\n\n| Query | Naive keyword check | Parser-based check |\n|---|---|---|\n`SELECT * FROM users` |\nallow | allow |\n`DELETE FROM users` |\nblock | block |\n`SELECT * FROM users; DROP TABLE users` |\nmay allow (starts with SELECT) | block (two statements) |\n`WITH x AS (DELETE FROM users RETURNING *) SELECT * FROM x` |\nmay allow | block (writable CTE) |\n\nThose last two are exactly the tricks that get past hand-rolled string checks. A broker that parses the statement, confirms it's a single read, caps the row count, and logs the whole thing gives you enforcement the model can't argue with. This is the model that managed MCP servers use — [Draxlr's MCP server](https://docs.draxlr.com/docs/mcp-server), for instance, exposes a database over OAuth as SELECT-only, so an AI client can list schemas and run queries but never issue a write. The broker holds the connection; the AI never sees the credentials.\n\nThe bigger win of the broker pattern is that read-only stops being one setting you hope everyone remembers and becomes a property of the gateway every AI client shares.\n\n**Relying on the prompt.** Worth repeating because it's the most common error: a system prompt is not a security boundary. Enforce read-only at the database and connection layers first, always.\n\n**Forgetting DEFAULT PRIVILEGES.** Grant\n\n`SELECT ON ALL TABLES`\n\ntoday and a table created next week won't be readable — or worse, your migration grants it broader access. The `ALTER DEFAULT PRIVILEGES`\n\nline above handles future tables cleanly.**Read-only isn't the same as private.** A read-only role can still read *everything* it's granted, including PII and secrets. \"Can't write\" says nothing about \"should see.\" Scope table grants, and mask sensitive columns (email, tokens, card numbers) before results leave the broker.\n\n**Ignoring resource exhaustion.** A model can't corrupt your data with a `SELECT`\n\n, but `SELECT * FROM events`\n\non a billion-row table can still take your database down. Cap returned rows, set a `statement_timeout`\n\n, and prefer a replica so read load stays off the primary.\n\n**No audit trail.** If you can't answer \"what did the AI query last Tuesday,\" you have a blind spot. Log every query the broker runs, with the identity behind it. This is also what turns an incident review from guesswork into a five-minute grep.\n\nGiving an AI assistant access to your database doesn't have to be a leap of faith. Make writes structurally impossible instead of merely discouraged:\n\n`SELECT`\n\n-only grants, scoped to the tables that matter — enforced by the database engine.Do those, and you get the upside — an AI that explores your data, answers questions, and drafts queries in seconds — without the 2 a.m. worry that it might rewrite history instead of reading it.\n\nHow do you hand database access to AI tools on your team — a read replica, a scoped role, a broker, or something else? I'd love to hear what's working (and what's bitten you) in the comments.\n\n*Sources: Model Context Protocol for Databases (AI2SQL), Safely connecting AI tools to your database (Daymark), Protecting production SQL from agentic query risks (Rietta), safedb-mcp (GitHub), Configure read-only access on an availability replica (Microsoft Learn).*", "url": "https://wpnews.pro/news/read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes", "canonical_source": "https://dev.to/vivekdraxlr/read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes-2pmm", "published_at": "2026-08-20 09:24:40+00:00", "updated_at": "2026-08-20 09:44:04.057414+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "developer-tools"], "entities": ["PostgreSQL", "MySQL"], "alternates": {"html": "https://wpnews.pro/news/read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes", "markdown": "https://wpnews.pro/news/read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes.md", "text": "https://wpnews.pro/news/read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes.txt", "jsonld": "https://wpnews.pro/news/read-only-by-design-letting-ai-explore-your-database-without-the-risk-of-writes.jsonld"}}