AI Analytics Row-Level Security: Let Users Ask Questions Without Leaking Data A developer outlines a security architecture for AI analytics that enforces row-level permissions, preventing natural language queries from leaking cross-tenant data. The design uses an analytics gateway with a policy engine and semantic layer to scope queries by user identity, ensuring the AI model acts as an untrusted planner rather than the enforcement layer. The dangerous part of AI analytics is not that a model may write a bad chart title. It is that one friendly question can turn into a warehouse query your user was never supposed to run. That risk is growing because builders are adding natural language analytics to products, dashboards, internal tools, support consoles, and agent workflows. Users want to ask, “Which accounts are slipping this month?” and get an answer. That is useful, and it is a permissions trap. If your AI analyst connects through one powerful service account, every customer question may inherit the same access. Your app may have perfect tenant checks in the UI, while the AI path quietly bypasses them. This guide shows how to design AI analytics row-level security so customers can ask useful questions without leaking rows, metrics, or private business context. Recent AI platform activity points in the same direction: builders are moving from “chat with documents” to “ask questions about live business data.” Developer pain points are consistent: safe natural language questions, tenant-scoped queries, auditable user identity, consistent metric definitions, and charts that do not expose raw tables. The search gap is clear. Many articles compare embedded analytics tools. Others explain database row-level security in isolation. Fewer walk through the product architecture for a customer-facing AI analyst that must handle tenant scope, natural language, semantic metrics, safe SQL, and audit evidence together. Traditional analytics has a simple identity chain: Human user → app session → analytics permission → database query The database or BI layer knows who is asking. The app can apply tenant filters, role checks, and column restrictions. AI analytics often breaks that chain: Human user → app session → AI service → service account → database query Now the warehouse sees one identity: the AI service account. That account usually needs broad read access so it can answer many kinds of questions. If you do nothing else, a basic natural language question can become a cross-tenant data leak. The model is not necessarily malicious. It may simply do what models do: search for the data that seems relevant. If the path is open, it will use it. A safe design treats the AI analyst as an untrusted planner, not as the enforcement layer. Row-level security means users only see the records they are allowed to see, even when they ask broad questions. For example: With AI analytics, this must hold across every path: If the user asks, “Show all churn risks,” the system should not trust the model to remember where tenant id = ... . The platform should enforce that scope below or beside the model. Use a layered design: User question ↓ Auth context ↓ AI planner ↓ Analytics gateway ↓ Policy engine ↓ Semantic layer ↓ Query compiler ↓ Database / warehouse ↓ Scoped result + evidence ↓ Answer or chart The important point: the AI planner proposes intent. It does not get raw database freedom. The analytics gateway should own five jobs: This creates a boundary the model cannot prompt its way around. Natural language to SQL is tempting. It demos well. It also fails in ways that are easy to miss. A model may: A better pattern is intent to semantic query . The model extracts the user’s intent, then your system maps it to approved metrics and dimensions. { "metric": "active accounts", "dimensions": "plan", "region" , "filters": { "field": "period", "operator": "last 30 days" } , "visualization": "bar chart" } Then your query compiler creates SQL using server-side policy. select plan, region, count as active accounts from account metrics where tenant id = :tenant id and deleted at is null and activity date = current date - interval '30 days' and region = any :allowed regions group by plan, region order by active accounts desc; The model can ask for “active accounts by region.” It cannot remove tenant id , invent a metric, or select payroll data. Every AI analytics request should begin with a server-created access context. Do not let the browser or the model provide this object. type AnalyticsAccessContext = { userId: string; tenantId: string; role: "owner" | "admin" | "analyst" | "support" | "viewer"; allowedDatasets: string ; allowedMetrics: string ; rowFilters: { tenant id: string; regions?: string }; deniedColumns: string ; requestId: string; }; This object becomes the policy input for every later step. The user may ask a broad question. The model may suggest a query. But the access context decides what data exists for this session. A metric catalog keeps the model from redefining your business. Without a catalog, “revenue” may mean booked revenue in one answer, paid invoices in another, and forecasted expansion in a third. That destroys trust. A simple metric definition can start like this: js const metrics = { churn risk accounts: { id: "churn risk accounts", label: "Churn-risk accounts", sqlExpression: "count distinct account id ", allowedRoles: "owner", "admin", "analyst" , defaultFilters: { is test account: false, deleted: false }, safeDimensions: "plan", "region", "owner team" , freshnessTargetMinutes: 60 } }; The model can explain this metric to the user, but it should not create the definition on the fly. There are three common enforcement patterns. This uses the database’s row-level security features. The query runs with a user or tenant context, and the database enforces the rule. Example in Postgres: alter table account metrics enable row level security; create policy tenant isolation policy on account metrics using tenant id = current setting 'app.tenant id' ::uuid ; Then set the tenant before the query: select set config 'app.tenant id', :tenant id, true ; This is strong because the rule lives near the data. It is harder for app code to forget. The gateway injects tenant and role filters into every compiled query. This is practical when you query warehouses or multiple data stores where native RLS is uneven. The key rule: filters must be added by trusted code, not by the model. The user only sees a subset of datasets and dimensions. If a support user cannot access revenue, the model never receives revenue tables, revenue metrics, or revenue examples in its prompt. This reduces both security risk and model confusion. Most small teams should combine patterns 2 and 3 first. Add database-native RLS where your stack supports it cleanly. Row-level security answers, “Which records can this user see?” AI analytics also needs column-level controls for emails, invoice notes, API keys, compensation, internal scores, support transcripts, and other sensitive fields. The model does not need most of this to answer common analytics questions. Create a column policy: js const columnPolicy = { viewer: "account name", "plan", "usage count", "created month" , analyst: "account name", "plan", "usage count", "mrr band", "region" , support: "account name", "plan", "health status", "open ticket count" , owner: " " }; Prefer bands and aggregates over raw sensitive values. “MRR band: $1k-$5k” is often enough for AI analytics. You do not need to expose exact invoices for every question. Natural language makes expensive analytics easier to trigger. A user can ask: Compare every customer cohort by feature usage, ticket sentiment, renewal risk, onboarding source, and plan since launch. That may sound reasonable. It may also scan half your warehouse. Add budgets for rows returned, query runtime, joins, date range, follow-up depth, chart series, returned context, and repeated-question caching. Your query guard should reject plans with too many rows, too many joins, blocked columns, or a date range the user’s plan does not allow. Return a helpful refusal: I can answer that if we narrow the date range or choose fewer breakdowns. Try “last 90 days by plan and region.” This protects cost and improves UX. Follow-up questions are a hidden leak path. User: “Show churn risk for my region.” AI: “Here are the Midwest accounts at risk.” User: “Now compare that with everyone else.” The phrase “everyone else” is dangerous. It could mean every region in the tenant, every tenant, or all customers in the warehouse. Follow-ups must inherit the original access context and policy, not raw result rows unless needed. Store conversation state like this: { "conversation id": "conv 123", "tenant id": "tenant 456", "user id": "user 789", "active scope": { "regions": "midwest" , "datasets": "account metrics" , "metrics": "churn risk accounts" }, "last query id": "qry abc" } When the user asks a follow-up, the gateway resolves ambiguous words against policy, not against the model’s imagination. AI analytics needs an answer receipt. At minimum, log the user, tenant, original question, normalized intent, selected metrics, policy decision, query hash, row count, returned columns, freshness, model, and refusal reason if any. Example: { "request id": "req 01", "tenant id": "tenant 456", "user id": "user 789", "question": "Which accounts are slipping this month?", "intent": "churn risk accounts by owner", "policy": "allow", "filters applied": "tenant id", "allowed regions", "not deleted" , "columns returned": "account name", "owner team", "risk band" , "row count": 42, "freshness minutes": 18, "query hash": "sha256:..." } Bad: “Always remember to filter by tenant id.” Good: the query compiler always injects tenant scope from server-side auth context. Prompts are guidance. Policy is enforcement. Do not show the model every table name and column. Hidden tables can leak meaning even if rows are blocked. A table name can be sensitive by itself. If the user asks for a trend, return a trend. Do not send 10,000 rows to the model so it can summarize them. Cache keys must include tenant, role, metric version, and policy version. tenant 456:role analyst:metric v4:policy v12:churn-risk-last-30-days Charts can leak. A chart with one bar can reveal a single customer’s revenue. Add minimum group sizes and suppression rules. You do not need a huge platform on day one. Start here: For example, ship usage trends, active accounts, and support ticket volume first. Avoid high-risk areas like billing disputes, payroll, medical records, security logs, raw messages, and unrestricted SQL exports until your boundaries are proven. Add these to CI or a staging eval suite: | Test | Expected result | |---|---| | User asks for another tenant’s revenue | Refuse or return only scoped data | | Viewer asks for exact MRR | Return allowed aggregate or deny | | User asks for hidden table names | Refuse without confirming table existence | | Follow-up says “show everyone” | Keep tenant and role scope | | Query requests too many rows | Ask user to narrow the question | | Chart group has one account | Suppress or bucket the value | | Model suggests raw SQL | Compile through semantic layer only | | Cached answer exists for admin | Do not show it to viewer | The goal is not to prove the model is obedient. It is to prove the boundary holds when the model is creative. Before giving customers an AI analyst, make sure you can answer yes to these: If not, your AI analytics feature may be a data leak with a chat box. AI analytics row-level security is the practice of enforcing user, tenant, and role-based row filters when an AI system answers questions about data. The model may interpret the question, but trusted application or database code decides which records the user can access. No. A prompt can remind the model to use tenant filters, but it is not a security boundary. Tenant scope should be enforced by the database, analytics gateway, semantic layer, or query compiler. It can generate draft SQL for internal review, but customer-facing systems should avoid executing unchecked SQL from a model. A safer pattern is to convert user intent into approved metrics, dimensions, and filters, then compile SQL with server-side policy. Store conversation scope separately from model text. Follow-ups should inherit the same tenant, role, metric, and dataset limits. Ambiguous phrases like “everyone else” should be resolved by policy, not by model guesswork. Log the user, tenant, original question, normalized intent, selected metrics, applied filters, returned columns, row count, freshness, query hash, model version, and policy decision. This creates evidence for debugging and customer trust. Start with a few approved metrics, a small semantic catalog, strict tenant filters, column blocks, query limits, and answer receipts. Avoid raw SQL execution and sensitive datasets until your policy and tests are reliable.