<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ANDREI MIRONOV</title>
    <description>The latest articles on DEV Community by ANDREI MIRONOV (@datamcp).</description>
    <link>https://dev.to/datamcp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957957%2Fe9c670d7-a6a6-47e6-8af8-f46b3119bf3b.jpg</url>
      <title>DEV Community: ANDREI MIRONOV</title>
      <link>https://dev.to/datamcp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/datamcp"/>
    <language>en</language>
    <item>
      <title>How to set up PostgreSQL permissions for AI coding tools (Cursor, Claude, Copilot)</title>
      <dc:creator>ANDREI MIRONOV</dc:creator>
      <pubDate>Fri, 29 May 2026 08:17:15 +0000</pubDate>
      <link>https://dev.to/datamcp/how-to-set-up-postgresql-permissions-for-ai-coding-tools-cursor-claude-copilot-28kj</link>
      <guid>https://dev.to/datamcp/how-to-set-up-postgresql-permissions-for-ai-coding-tools-cursor-claude-copilot-28kj</guid>
      <description>&lt;p&gt;Most developers I talk to connect Cursor or Claude directly to their databases using a full admin connection string. Wether it's a local Docker database, or cloud-based QA or even production database, since it's not hidden in a subnet and publicly available. Many of developers have open-to-world QA databases, replicating prod data, where multiple developers work.&lt;/p&gt;

&lt;p&gt;It works. But it's the equivalent of giving someone your house keys because they need to water your plants.&lt;/p&gt;

&lt;p&gt;This post covers how to set up PostgreSQL permissions specifically for AI tools - what to create, what to restrict, and how to actually enforce it at the query level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a direct connection string
&lt;/h2&gt;

&lt;p&gt;When you drop a raw postgresql://user:password@host:5432/db into Cursor, you're giving it whatever privileges that user has. If that's your admin user - and it usually is - the AI can:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT anything&lt;br&gt;
UPDATE anything&lt;br&gt;
DELETE anything&lt;br&gt;
DROP tables&lt;br&gt;
Run TRUNCATE on production data&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Not that Cursor would do this on purpose. But AI tools generate SQL based on context, and mistakes happen. One misunderstood prompt, one "fix this data" instruction gone wrong - and you're restoring from backup. If you have one. If it's restorable.&lt;/p&gt;

&lt;p&gt;The fix is two things: a restricted PostgreSQL role, and a permission layer that validates queries before they reach the database.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Create a dedicated PostgreSQL role for AI access
&lt;/h2&gt;

&lt;p&gt;Never use your admin user for AI tools. Create a separate role with only the privileges the AI actually needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create the role&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'your-strong-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Grant connection to the database&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;your_database&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Grant usage on the schema&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Grant SELECT on all existing tables&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Grant SELECT on future tables too&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
  &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a read-write setup where the AI needs to insert or update (for example, a coding assistant that runs migrations):&lt;/p&gt;

&lt;p&gt;CREATE ROLE ai_readwrite WITH LOGIN PASSWORD 'your-strong-password';&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;your_database&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readwrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readwrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readwrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;
  &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readwrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Explicitly block DELETE and destructive operations&lt;/span&gt;
&lt;span class="c1"&gt;-- (just don't grant them - absence of GRANT = denied)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key point: in PostgreSQL, not granting a privilege is enough to block it. You don't need to explicitly REVOKE anything you never granted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Block access to sensitive tables
&lt;/h2&gt;

&lt;p&gt;Even with a read-only role, you probably don't want the AI seeing everything. Billing data, PII, internal audit tables - these should be off limits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Revoke access to specific tables after the blanket GRANT&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;billing_events&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;api_keys&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Or lock down an entire schema&lt;/span&gt;
&lt;span class="k"&gt;REVOKE&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern: grant broadly, then revoke specifically. This is easier to maintain than trying to grant table by table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Restrict to specific schemas
&lt;/h2&gt;

&lt;p&gt;If your database has multiple schemas and the AI only needs access to one of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Only grant access to the 'app' schema, not 'internal' or 'audit'&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;ai_readonly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Don't grant anything on other schemas&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cleanest approach for teams where different schemas have different sensitivity levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Add a permission layer on top of PostgreSQL
&lt;/h2&gt;

&lt;p&gt;PostgreSQL role-level permissions are necessary, but not sufficient on their own. They don't:&lt;/p&gt;

&lt;p&gt;Log what queries the AI actually ran&lt;br&gt;
Block TRUNCATE (which requires only ownership, not a special privilege)&lt;br&gt;
Prevent the AI from querying system catalogs (pg_catalog, information_schema)&lt;br&gt;
Give you per-tool control (different permissions for Cursor vs a contractor's Claude setup)&lt;/p&gt;

&lt;p&gt;This is where a tool like &lt;a href="https://datamcp.app" rel="noopener noreferrer"&gt;DataMCP&lt;/a&gt; comes in. It sits between your AI tool and PostgreSQL, adds a second permission layer, and validates every query before execution.&lt;br&gt;
The setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cursor / Claude / VS Code
        |
        | MCP protocol
        v
   DataMCP (permission check + query validation)
        |
        | only validated queries pass through
        v
   PostgreSQL (your ai_readonly role)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, effective permissions = PostgreSQL role permissions AND DataMCP permission rules. Both have to allow a query for it to execute.&lt;br&gt;
In DataMCP you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set a permission preset per MCP link (READ_ONLY, READ_WRITE, CUSTOM)&lt;/li&gt;
&lt;li&gt;Block specific tables at the application layer without touching PostgreSQL roles&lt;/li&gt;
&lt;li&gt;See every query that ran, when, and what it returned
Revoke a specific AI tool's access instantly without changing your database&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 5: Test that restrictions actually work
&lt;/h2&gt;

&lt;p&gt;Don't assume the permissions are working - verify them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;-- Connect as the AI user and test
psql postgresql://ai_readonly:password@host:5432/db

-- This should work
&lt;/span&gt;&lt;span class="gp"&gt;SELECT id, email FROM users LIMIT 5;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
-- This should fail with "permission denied"
&lt;/span&gt;&lt;span class="gp"&gt;DELETE FROM users WHERE id = 1;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
-- This should fail
&lt;/span&gt;&lt;span class="gp"&gt;DROP TABLE users;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
-- This should fail if you revoked it
&lt;/span&gt;&lt;span class="gp"&gt;SELECT * FROM billing_events;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run through the operations you blocked and confirm they return permission errors, not results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this setup gives you
&lt;/h2&gt;

&lt;p&gt;With a dedicated PostgreSQL role plus a permission layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AI can only do what you explicitly allowed&lt;/li&gt;
&lt;li&gt;Every query is logged with execution time and row count&lt;/li&gt;
&lt;li&gt;You can give different tools different access levels&lt;/li&gt;
&lt;li&gt;You can revoke access for a specific tool without touching other integrations
= TRUNCATE and DROP are blocked even if the PostgreSQL role somehow got too many privileges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PostgreSQL role is your first line of defense. The permission layer is your second. Neither alone is enough - you want both.&lt;/p&gt;




&lt;p&gt;If you're using Cursor or Claude with a PostgreSQL database and want to skip the manual setup, &lt;a href="https://datamcp.app" rel="noopener noreferrer"&gt;DataMCP&lt;/a&gt; handles the permission layer out of the box. Free tier covers one database connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need both a restricted PostgreSQL role AND a separate permission layer?&lt;/strong&gt;&lt;br&gt;
Yes. PostgreSQL roles control what the database user can do. A permission layer like DataMCP adds query-level validation, audit logging, and per-tool access control on top. Use both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use row-level security (RLS) instead?&lt;/strong&gt;&lt;br&gt;
RLS is great for multi-tenant apps but adds complexity for AI tools. The AI needs to understand the RLS policies to write correct queries. A simpler approach: restricted role + table-level REVOKE + a permission layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I'm using Supabase or Neon?&lt;/strong&gt;&lt;br&gt;
The same SQL commands work. Supabase and Neon both expose standard PostgreSQL role management. Create the restricted role through the SQL editor in their dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I connect the restricted role to Cursor?&lt;/strong&gt;&lt;br&gt;
Add it to &lt;code&gt;.cursor/mcp.json&lt;/code&gt; - either directly as a connection string (basic, no permission layer) or via an MCP gateway like DataMCP (recommended). The MCP approach gives you the audit trail and query validation on top of the PostgreSQL role restrictions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work for Claude Desktop?&lt;/strong&gt;&lt;br&gt;
Yes. Claude Desktop supports MCP via mcp-remote. The same DataMCP MCP URL works for Cursor, Claude Desktop, VS Code, and any other MCP-compatible tool.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>cursor</category>
      <category>ai</category>
      <category>security</category>
    </item>
  </channel>
</rss>
