{"slug": "ai-agent-permissions-access-control-auth-and-row-level-security", "title": "AI Agent Permissions: Access Control, Auth and Row Level Security", "summary": "A developer outlined an approach for securing AI agents that act on behalf of users by pushing access control into the database with Postgres row level security rather than relying on system prompts. The method has each agent request carry the end user's verified JWT, so the backend applies that user's existing rules and the database returns only rows they are permitted to see. The writeup warns that service keys with full access and vector indexes lacking ownership metadata let agents reach other users' data, and that prompt-only restrictions can be defeated by prompt injection.", "body_md": "Most AI agent tutorials spend almost all their time on the model and the tools. Data gets one line near the end: connect the agent to your database. In practice that usually means a connection string with full access, sitting in an environment variable.\n\nFor a demo, that's fine. Once real users show up, it means the agent can read any row in any table, and the only thing deciding what it shows is the prompt.\n\nThis post covers how permissions for AI agents work, what usually goes wrong, and how to set things up so the database decides what an agent can see. I'll use Nhost for the hands-on examples, but the same approach works on plain Postgres.\n\nAI agent permissions are the rules for what an agent can read, change, and trigger in your app.\n\nMost agents in real products work on behalf of a user: a support assistant, a copilot inside a dashboard, a chat that answers questions about someone's account. For those, the agent should get the same access as the user it's helping.\n\nIf Anna can see her own invoices, the agent helping Anna can see Anna's invoices. It can't see Ben's, and it can't do anything Anna couldn't do herself in the UI.\n\nAgents that run without a user behind them, like a nightly job that summarizes support tickets, are a separate case. They get their own narrow role, and we'll come back to them later.\n\nAuthentication answers who sent a request. Authorization answers what that sender is allowed to do. With agents the line gets blurry, because the agent sends the request while a person stands behind it.\n\nIn practice, agents authenticate in one of three ways:\n\nFor an agent that lives inside your app and helps users, you want the third one.\n\nOnce requests carry the user's token, authorization needs nothing new. The backend sees a request from Anna and applies Anna's rules, exactly as it does when she clicks around the UI.\n\nThe agent never has to learn your permission model, and you don't have to describe it in the prompt.\n\nMost write-ups on AI agent security focus on the model: jailbreaks, hallucinated actions, bad tool calls. In apps with user data, the more common problem is plainer. The agent can reach data it was never supposed to see.\n\nThe agent needs to look up orders, the service key is right there, and it works on the first try. From then on every row in the database is one tool call away, and the model picks which rows to return.\n\nA common fix is a line in the system prompt like \"Only return data for the current user.\" It holds up until someone writes a message that convinces the model otherwise. Prompt injection is still an open problem, so the model can be talked out of any rule that lives only in the prompt.\n\nYou add RAG, documents get embedded and stored, and the agent searches them. The documents had owners in your main tables, but the vector index often doesn't know about them. A search for \"contract renewal terms\" can then pull a chunk from another customer's contract.\n\nEach of these goes away once the rules live next to the data and every request from the agent carries the user's identity. After that, the prompt and the model's choice of tool stop being your line of defense. The database only hands back rows the user is allowed to see.\n\nPostgres has had row level security (RLS) since version 9.5. You turn it on for a table and write a policy:\n\n```\nALTER TABLE invoices ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY invoices_owner ON invoices\n  FOR SELECT\n  USING (user_id = current_setting('app.user_id', true)::uuid);\n```\n\nFrom now on, Postgres adds this condition to every query on `invoices`, whether it comes from your app or from an agent's tool call. The table owner and superusers skip RLS by default, so the connection your API uses shouldn't be one of them.\n\nThe policy reads `app.user_id`. The second argument in `current_setting` makes a missing value come back as NULL, so a request without a user gets no rows instead of an error. Your backend should set the value only after verifying the user's JWT, and it should be scoped to the transaction:\n\n```\nBEGIN;\nSELECT set_config('app.user_id', $1, true);  -- true = this transaction only\nSELECT id, amount, status FROM invoices;\nCOMMIT;\n```\n\nThe `true` flag in `set_config` works like `SET LOCAL`. If you set the value for the whole session instead and run a pooler like PgBouncer in transaction mode, it can stay on the connection and show up in the next user's queries.\n\nThe agent has to send the user's token with every tool call. Your app usually has it already, since the user is signed in. Pass it into the agent run and have each tool attach it to its requests. The backend verifies the token, reads the user ID from it, and the policies take it from there.\n\nBackground agents are different. A job that runs at 3am might find the user's token already expired. Give jobs like that their own role with only the access they need, and keep that role away from user-facing agents.\n\n[Nhost](https://nhost.io?utm_source=devto&utm_medium=post&utm_campaign=ai-agent-permissions) gives you Postgres, auth, storage, and an API in one project. Permissions follow the same idea as RLS, but they're enforced in the API layer. You set them per role and per table in the dashboard, and the JWT handling from the previous section is already done for you.\n\nOpen the table in the dashboard, pick the `user` role, and set a row filter for select:\n\n```\n{\n  \"user_id\": { \"_eq\": \"X-Hasura-User-Id\" }\n}\n```\n\n`X-Hasura-User-Id` comes from the signed-in user's token, so this reads as \"only rows where `user_id` is the current user.\" You can also pick which columns the role gets to see.\n\nA tool in your agent is a regular function that sends the user's access token along:\n\n``` js\nasync function getInvoices(userAccessToken: string) {\n  const res = await fetch(NHOST_API_URL, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      Authorization: `Bearer ${userAccessToken}`,\n    },\n    body: JSON.stringify({ query: '{ invoices { id amount status } }' }),\n  })\n  return res.json()\n}\n```\n\nThe query asks for all invoices, and Anna's token turns that into Anna's invoices. The agent code has no permission checks in it at all.\n\nFor the vector search problem, Nhost Graphite adds embeddings and semantic search on top of your Postgres data, and search results go through the same permission rules as your tables. When the agent searches for \"my unpaid invoices,\" it searches only the current user's data.\n\nNo. Someone can still talk the agent into a weird answer or an off-topic rant.\n\nIf an attacker convinces the agent to \"list every invoice in the system,\" the query still runs as the user, and the user only gets their own rows back. The worst case is an agent that misbehaves with data the user could already see.\n\nFor actions with real-world effects, like refunds or deleting records, add a confirmation step on top. Permissions decide what the agent is allowed to touch. They don't decide whether a particular action makes sense.\n\nAn agent that acts for a user should look like that user to your backend. Once it does, you can stop writing permission rules into prompts and let the database handle access, the same way it already does for the rest of your app.\n\nIf you want to try this setup without wiring up JWTs and policies yourself, [Nhost](https://nhost.io?utm_source=devto&utm_medium=post&utm_campaign=ai-agent-permissions) gives you Postgres, auth, and permissions in one project.\n\nAnd if you handle agent permissions differently, I'd like to hear how in the comments.", "url": "https://wpnews.pro/news/ai-agent-permissions-access-control-auth-and-row-level-security", "canonical_source": "https://dev.to/max4nhost/ai-agent-permissions-access-control-auth-and-row-level-security-4dfc", "published_at": "2026-11-12 19:35:25+00:00", "updated_at": "2026-11-12 19:53:35.253752+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "mlops"], "entities": ["Nhost", "Postgres"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ai-agent-permissions-access-control-auth-and-row-level-security", "markdown": "https://wpnews.pro/news/ai-agent-permissions-access-control-auth-and-row-level-security.md", "text": "https://wpnews.pro/news/ai-agent-permissions-access-control-auth-and-row-level-security.txt", "jsonld": "https://wpnews.pro/news/ai-agent-permissions-access-control-auth-and-row-level-security.jsonld"}}