[{"data":1,"prerenderedAt":1029},["ShallowReactive",2],{"blog-postgresql-rls-langgraph-agent-isolation":3,"blog-postgresql-rls-langgraph-agent-isolation-favor-of":1028},{"id":4,"title":5,"body":6,"cover_image":1011,"date":1012,"description":1013,"extension":1014,"meta":1015,"navigation":294,"path":1017,"seo":1018,"stem":1019,"tags":1020,"type":1026,"__hash__":1027},"blog\u002Fblog\u002Fpostgresql-rls-langgraph-agent-isolation.md","Multi-Tenant AI Agents: Enforcing PostgreSQL Row-Level Security in LangGraph",{"type":7,"value":8,"toc":998},"minimark",[9,13,20,23,138,145,151,159,162,167,174,177,185,188,216,218,225,236,243,248,251,431,448,450,454,457,461,555,559,568,917,920,922,926,929,969,971,975,994],[10,11,5],"h1",{"id":12},"multi-tenant-ai-agents-enforcing-postgresql-row-level-security-in-langgraph",[14,15,16],"p",{},[17,18,19],"em",{},"What happens when an autonomous LangGraph agent receives a prompt-injection payload or hallucinates an organization ID inside a database tool?",[14,21,22],{},"When building multi-tenant AI systems, developers often construct tool nodes that accept database queries or parameterized CRUD arguments. A typical LangGraph tool definition looks like this:",[24,25,30],"pre",{"className":26,"code":27,"language":28,"meta":29,"style":29},"language-python shiki shiki-themes github-dark-default","@tool\ndef get_patient_records(patient_id: str, organization_id: str) -> dict:\n    \"\"\"Fetch clinical summaries for a specific patient.\"\"\"\n    query = \"\"\"\n        SELECT id, patient_name, diagnosis, notes\n        FROM clinical_records\n        WHERE id = :patient_id AND organization_id = :organization_id;\n    \"\"\"\n    return db.execute(query, {\"patient_id\": patient_id, \"organization_id\": organization_id}).fetchall()\n","python","",[31,32,33,42,74,81,93,99,105,111,117],"code",{"__ignoreMap":29},[34,35,38],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"sc3cj","@tool\n",[34,43,45,49,52,56,60,63,65,68,71],{"class":36,"line":44},2,[34,46,48],{"class":47},"suJrU","def",[34,50,51],{"class":40}," get_patient_records",[34,53,55],{"class":54},"sZEs4","(patient_id: ",[34,57,59],{"class":58},"sFSAA","str",[34,61,62],{"class":54},", organization_id: ",[34,64,59],{"class":58},[34,66,67],{"class":54},") -> ",[34,69,70],{"class":58},"dict",[34,72,73],{"class":54},":\n",[34,75,77],{"class":36,"line":76},3,[34,78,80],{"class":79},"s9uIt","    \"\"\"Fetch clinical summaries for a specific patient.\"\"\"\n",[34,82,84,87,90],{"class":36,"line":83},4,[34,85,86],{"class":54},"    query ",[34,88,89],{"class":47},"=",[34,91,92],{"class":79}," \"\"\"\n",[34,94,96],{"class":36,"line":95},5,[34,97,98],{"class":79},"        SELECT id, patient_name, diagnosis, notes\n",[34,100,102],{"class":36,"line":101},6,[34,103,104],{"class":79},"        FROM clinical_records\n",[34,106,108],{"class":36,"line":107},7,[34,109,110],{"class":79},"        WHERE id = :patient_id AND organization_id = :organization_id;\n",[34,112,114],{"class":36,"line":113},8,[34,115,116],{"class":79},"    \"\"\"\n",[34,118,120,123,126,129,132,135],{"class":36,"line":119},9,[34,121,122],{"class":47},"    return",[34,124,125],{"class":54}," db.execute(query, {",[34,127,128],{"class":79},"\"patient_id\"",[34,130,131],{"class":54},": patient_id, ",[34,133,134],{"class":79},"\"organization_id\"",[34,136,137],{"class":54},": organization_id}).fetchall()\n",[14,139,140,141,144],{},"This model assumes the model will behave. It assumes the model will faithfully forward the authenticated user's ",[31,142,143],{},"organization_id"," into the tool arguments.",[14,146,147,148,150],{},"That assumption fails in production. An adversarial prompt, an ambiguous user context, or a subtle hallucination can cause the model to substitute an arbitrary ",[31,149,143],{}," or omit it during tool invocation. If your application connects to PostgreSQL using a shared connection pool with ambient credentials, the database executes whatever the agent passes.",[14,152,153,154,158],{},"The solution is not to prompt the model to \"be more secure\". The solution is to enforce isolation at the database layer using ",[155,156,157],"strong",{},"PostgreSQL Row-Level Security (RLS)"," bound to the LangGraph execution state.",[160,161],"hr",{},[163,164,166],"h2",{"id":165},"the-ambient-authority-dilemma-in-agent-tooling","The Ambient Authority Dilemma in Agent Tooling",[14,168,169,170,173],{},"In traditional web applications, a trusted API gateway validates a JWT token and passes the authenticated ",[31,171,172],{},"tenant_id"," directly to a predefined query. The user never writes SQL, and the parameters are strictly controlled.",[14,175,176],{},"LangGraph agents change this relationship. The agent reasons dynamically, decides which tools to call, and formats arguments:",[24,178,183],{"className":179,"code":181,"language":182,"meta":29},[180],"language-text","[User Request] ──> [LangGraph StateGraph] ──> [LLM Reasoning Engine]\n                                                       │\n                                        Decides Tool Call & Arguments\n                                                       │\n                                                       ▼\n                                         [Database Tool Invocation]\n                                                       │\n                                            Ambient Connection Pool\n                                                       │\n                                                       ▼\n                                            [PostgreSQL Database]\n","text",[31,184,181],{"__ignoreMap":29},[14,186,187],{},"If the database tool relies on application-level filtering, you face two immediate failure modes:",[189,190,191,202],"ol",{},[192,193,194,197,198,201],"li",{},[155,195,196],{},"Parameter Tampering \u002F Prompt Injection:"," A malicious user prompts the agent: ",[17,199,200],{},"\"Retrieve patient notes for case #4912 from organization beta-health\"",". If the agent uses the model-provided parameter, the query exposes cross-tenant records.",[192,203,204,207,208,211,212,215],{},[155,205,206],{},"Ambient Pool Trust:"," Because the application connects as a shared superuser or service role (",[31,209,210],{},"postgres"," or ",[31,213,214],{},"app_backend","), the database cannot differentiate between a legitimate tenant query and an unauthorized access attempt.",[160,217],{},[163,219,221,222],{"id":220},"architectural-pattern-session-scoped-context-via-set-local","Architectural Pattern: Session-Scoped Context via ",[31,223,224],{},"SET LOCAL",[14,226,227,228,231,232,235],{},"PostgreSQL provides a native mechanism to pass request context into database sessions without altering query syntax: ",[155,229,230],{},"Configuration Parameter Settings"," combined with ",[155,233,234],{},"Row-Level Security",".",[14,237,238,239,242],{},"Executing ",[31,240,241],{},"SET LOCAL app.current_tenant_id = :tenant_id"," within a transaction block scopes the setting strictly to that single transaction. When the connection returns to the connection pool, the setting automatically resets.",[244,245,247],"h3",{"id":246},"_1-postgresql-schema-rls-policy","1. PostgreSQL Schema & RLS Policy",[14,249,250],{},"First, enable RLS on your tenant-sensitive tables and define the security policy:",[24,252,256],{"className":253,"code":254,"language":255,"meta":29,"style":29},"language-sql shiki shiki-themes github-dark-default","-- Enable Row Level Security on the table\nALTER TABLE clinical_records ENABLE ROW LEVEL SECURITY;\n\n-- Force RLS even for table owners (optional, recommended for app roles)\nALTER TABLE clinical_records FORCE ROW LEVEL SECURITY;\n\n-- Define tenant isolation policy using current_setting\nCREATE POLICY tenant_isolation_policy ON clinical_records\n    FOR ALL\n    USING (\n        tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid\n    )\n    WITH CHECK (\n        tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid\n    );\n","sql",[31,257,258,264,290,296,301,320,324,329,346,354,363,389,395,406,425],{"__ignoreMap":29},[34,259,260],{"class":36,"line":37},[34,261,263],{"class":262},"sH3jZ","-- Enable Row Level Security on the table\n",[34,265,266,269,272,275,278,281,284,287],{"class":36,"line":44},[34,267,268],{"class":47},"ALTER",[34,270,271],{"class":47}," TABLE",[34,273,274],{"class":54}," clinical_records ",[34,276,277],{"class":47},"ENABLE",[34,279,280],{"class":47}," ROW",[34,282,283],{"class":47}," LEVEL",[34,285,286],{"class":47}," SECURITY",[34,288,289],{"class":54},";\n",[34,291,292],{"class":36,"line":76},[34,293,295],{"emptyLinePlaceholder":294},true,"\n",[34,297,298],{"class":36,"line":83},[34,299,300],{"class":262},"-- Force RLS even for table owners (optional, recommended for app roles)\n",[34,302,303,305,307,309,312,314,316,318],{"class":36,"line":95},[34,304,268],{"class":47},[34,306,271],{"class":47},[34,308,274],{"class":54},[34,310,311],{"class":47},"FORCE",[34,313,280],{"class":47},[34,315,283],{"class":47},[34,317,286],{"class":47},[34,319,289],{"class":54},[34,321,322],{"class":36,"line":101},[34,323,295],{"emptyLinePlaceholder":294},[34,325,326],{"class":36,"line":107},[34,327,328],{"class":262},"-- Define tenant isolation policy using current_setting\n",[34,330,331,334,337,340,343],{"class":36,"line":113},[34,332,333],{"class":47},"CREATE",[34,335,336],{"class":47}," POLICY",[34,338,339],{"class":54}," tenant_isolation_policy ",[34,341,342],{"class":47},"ON",[34,344,345],{"class":54}," clinical_records\n",[34,347,348,351],{"class":36,"line":119},[34,349,350],{"class":47},"    FOR",[34,352,353],{"class":54}," ALL\n",[34,355,357,360],{"class":36,"line":356},10,[34,358,359],{"class":47},"    USING",[34,361,362],{"class":54}," (\n",[34,364,366,369,371,374,377,380,383,386],{"class":36,"line":365},11,[34,367,368],{"class":54},"        tenant_id ",[34,370,89],{"class":47},[34,372,373],{"class":58}," NULLIF",[34,375,376],{"class":54},"(current_setting(",[34,378,379],{"class":79},"'app.current_tenant_id'",[34,381,382],{"class":54},", true), ",[34,384,385],{"class":79},"''",[34,387,388],{"class":54},")::uuid\n",[34,390,392],{"class":36,"line":391},12,[34,393,394],{"class":54},"    )\n",[34,396,398,401,404],{"class":36,"line":397},13,[34,399,400],{"class":47},"    WITH",[34,402,403],{"class":47}," CHECK",[34,405,362],{"class":54},[34,407,409,411,413,415,417,419,421,423],{"class":36,"line":408},14,[34,410,368],{"class":54},[34,412,89],{"class":47},[34,414,373],{"class":58},[34,416,376],{"class":54},[34,418,379],{"class":79},[34,420,382],{"class":54},[34,422,385],{"class":79},[34,424,388],{"class":54},[34,426,428],{"class":36,"line":427},15,[34,429,430],{"class":54},"    );\n",[14,432,433,434,437,438,441,442,211,445,235],{},"If ",[31,435,436],{},"app.current_tenant_id"," is empty, unset, or mismatched, PostgreSQL returns zero rows on ",[31,439,440],{},"SELECT",", and raises a violation on ",[31,443,444],{},"INSERT",[31,446,447],{},"UPDATE",[160,449],{},[163,451,453],{"id":452},"_2-binding-langgraph-state-to-database-connections","2. Binding LangGraph State to Database Connections",[14,455,456],{},"In LangGraph, execution state passes through a typed state dictionary. We store the authenticated caller's identity in the graph state, outside the reach of the LLM's prompt parameters.",[244,458,460],{"id":459},"graph-state-definition","Graph State Definition",[24,462,464],{"className":26,"code":463,"language":28,"meta":29,"style":29},"from typing import Annotated, TypedDict\nfrom langgraph.graph import StateGraph, END\nfrom langgraph.graph.message import add_messages\n\nclass AgentState(TypedDict):\n    messages: Annotated[list, add_messages]\n    tenant_id: str\n    user_id: str\n",[31,465,466,480,495,507,511,529,540,548],{"__ignoreMap":29},[34,467,468,471,474,477],{"class":36,"line":37},[34,469,470],{"class":47},"from",[34,472,473],{"class":54}," typing ",[34,475,476],{"class":47},"import",[34,478,479],{"class":54}," Annotated, TypedDict\n",[34,481,482,484,487,489,492],{"class":36,"line":44},[34,483,470],{"class":47},[34,485,486],{"class":54}," langgraph.graph ",[34,488,476],{"class":47},[34,490,491],{"class":54}," StateGraph, ",[34,493,494],{"class":58},"END\n",[34,496,497,499,502,504],{"class":36,"line":76},[34,498,470],{"class":47},[34,500,501],{"class":54}," langgraph.graph.message ",[34,503,476],{"class":47},[34,505,506],{"class":54}," add_messages\n",[34,508,509],{"class":36,"line":83},[34,510,295],{"emptyLinePlaceholder":294},[34,512,513,516,520,523,526],{"class":36,"line":95},[34,514,515],{"class":47},"class",[34,517,519],{"class":518},"sQhOw"," AgentState",[34,521,522],{"class":54},"(",[34,524,525],{"class":58},"TypedDict",[34,527,528],{"class":54},"):\n",[34,530,531,534,537],{"class":36,"line":101},[34,532,533],{"class":54},"    messages: Annotated[",[34,535,536],{"class":58},"list",[34,538,539],{"class":54},", add_messages]\n",[34,541,542,545],{"class":36,"line":107},[34,543,544],{"class":54},"    tenant_id: ",[34,546,547],{"class":58},"str\n",[34,549,550,553],{"class":36,"line":113},[34,551,552],{"class":54},"    user_id: ",[34,554,547],{"class":58},[244,556,558],{"id":557},"context-bound-tool-execution","Context-Bound Tool Execution",[14,560,561,562,564,565,567],{},"Instead of passing ",[31,563,172],{}," as an argument to the LLM, the tool extracts ",[31,566,172],{}," directly from the runtime context or an execution wrapper:",[24,569,571],{"className":26,"code":570,"language":28,"meta":29,"style":29},"import asyncpg\nfrom contextlib import asynccontextmanager\n\nclass TenantDatabase:\n    def __init__(self, pool: asyncpg.Pool):\n        self.pool = pool\n\n    @asynccontextmanager\n    async def tenant_session(self, tenant_id: str):\n        async with self.pool.acquire() as conn:\n            async with conn.transaction():\n                # Set the session context for this specific transaction\n                await conn.execute(\n                    \"SELECT set_config('app.current_tenant_id', $1, true);\",\n                    str(tenant_id)\n                )\n                yield conn\n\n# Tool implementation registered with the agent\nasync def execute_clinical_search(query: str, state: AgentState, db: TenantDatabase) -> list:\n    \"\"\"Tool that queries records under strict RLS isolation.\"\"\"\n    tenant_id = state[\"tenant_id\"]\n\n    async with db.tenant_session(tenant_id) as conn:\n        # Notice: No manual tenant_id filter required in the query!\n        # PostgreSQL RLS automatically filters rows at the engine layer.\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT id, patient_name, diagnosis, notes\n            FROM clinical_records\n            WHERE diagnosis ILIKE $1;\n            \"\"\",\n            f\"%{query}%\"\n        )\n        return [dict(row) for row in rows]\n",[31,572,573,580,592,596,605,616,629,633,638,656,676,686,691,699,707,715,721,730,735,741,764,770,787,792,806,812,818,832,838,844,850,856,864,885,891],{"__ignoreMap":29},[34,574,575,577],{"class":36,"line":37},[34,576,476],{"class":47},[34,578,579],{"class":54}," asyncpg\n",[34,581,582,584,587,589],{"class":36,"line":44},[34,583,470],{"class":47},[34,585,586],{"class":54}," contextlib ",[34,588,476],{"class":47},[34,590,591],{"class":54}," asynccontextmanager\n",[34,593,594],{"class":36,"line":76},[34,595,295],{"emptyLinePlaceholder":294},[34,597,598,600,603],{"class":36,"line":83},[34,599,515],{"class":47},[34,601,602],{"class":518}," TenantDatabase",[34,604,73],{"class":54},[34,606,607,610,613],{"class":36,"line":95},[34,608,609],{"class":47},"    def",[34,611,612],{"class":58}," __init__",[34,614,615],{"class":54},"(self, pool: asyncpg.Pool):\n",[34,617,618,621,624,626],{"class":36,"line":101},[34,619,620],{"class":58},"        self",[34,622,623],{"class":54},".pool ",[34,625,89],{"class":47},[34,627,628],{"class":54}," pool\n",[34,630,631],{"class":36,"line":107},[34,632,295],{"emptyLinePlaceholder":294},[34,634,635],{"class":36,"line":113},[34,636,637],{"class":40},"    @asynccontextmanager\n",[34,639,640,643,646,649,652,654],{"class":36,"line":119},[34,641,642],{"class":47},"    async",[34,644,645],{"class":47}," def",[34,647,648],{"class":40}," tenant_session",[34,650,651],{"class":54},"(self, tenant_id: ",[34,653,59],{"class":58},[34,655,528],{"class":54},[34,657,658,661,664,667,670,673],{"class":36,"line":356},[34,659,660],{"class":47},"        async",[34,662,663],{"class":47}," with",[34,665,666],{"class":58}," self",[34,668,669],{"class":54},".pool.acquire() ",[34,671,672],{"class":47},"as",[34,674,675],{"class":54}," conn:\n",[34,677,678,681,683],{"class":36,"line":365},[34,679,680],{"class":47},"            async",[34,682,663],{"class":47},[34,684,685],{"class":54}," conn.transaction():\n",[34,687,688],{"class":36,"line":391},[34,689,690],{"class":262},"                # Set the session context for this specific transaction\n",[34,692,693,696],{"class":36,"line":397},[34,694,695],{"class":47},"                await",[34,697,698],{"class":54}," conn.execute(\n",[34,700,701,704],{"class":36,"line":408},[34,702,703],{"class":79},"                    \"SELECT set_config('app.current_tenant_id', $1, true);\"",[34,705,706],{"class":54},",\n",[34,708,709,712],{"class":36,"line":427},[34,710,711],{"class":58},"                    str",[34,713,714],{"class":54},"(tenant_id)\n",[34,716,718],{"class":36,"line":717},16,[34,719,720],{"class":54},"                )\n",[34,722,724,727],{"class":36,"line":723},17,[34,725,726],{"class":47},"                yield",[34,728,729],{"class":54}," conn\n",[34,731,733],{"class":36,"line":732},18,[34,734,295],{"emptyLinePlaceholder":294},[34,736,738],{"class":36,"line":737},19,[34,739,740],{"class":262},"# Tool implementation registered with the agent\n",[34,742,744,747,749,752,755,757,760,762],{"class":36,"line":743},20,[34,745,746],{"class":47},"async",[34,748,645],{"class":47},[34,750,751],{"class":40}," execute_clinical_search",[34,753,754],{"class":54},"(query: ",[34,756,59],{"class":58},[34,758,759],{"class":54},", state: AgentState, db: TenantDatabase) -> ",[34,761,536],{"class":58},[34,763,73],{"class":54},[34,765,767],{"class":36,"line":766},21,[34,768,769],{"class":79},"    \"\"\"Tool that queries records under strict RLS isolation.\"\"\"\n",[34,771,773,776,778,781,784],{"class":36,"line":772},22,[34,774,775],{"class":54},"    tenant_id ",[34,777,89],{"class":47},[34,779,780],{"class":54}," state[",[34,782,783],{"class":79},"\"tenant_id\"",[34,785,786],{"class":54},"]\n",[34,788,790],{"class":36,"line":789},23,[34,791,295],{"emptyLinePlaceholder":294},[34,793,795,797,799,802,804],{"class":36,"line":794},24,[34,796,642],{"class":47},[34,798,663],{"class":47},[34,800,801],{"class":54}," db.tenant_session(tenant_id) ",[34,803,672],{"class":47},[34,805,675],{"class":54},[34,807,809],{"class":36,"line":808},25,[34,810,811],{"class":262},"        # Notice: No manual tenant_id filter required in the query!\n",[34,813,815],{"class":36,"line":814},26,[34,816,817],{"class":262},"        # PostgreSQL RLS automatically filters rows at the engine layer.\n",[34,819,821,824,826,829],{"class":36,"line":820},27,[34,822,823],{"class":54},"        rows ",[34,825,89],{"class":47},[34,827,828],{"class":47}," await",[34,830,831],{"class":54}," conn.fetch(\n",[34,833,835],{"class":36,"line":834},28,[34,836,837],{"class":79},"            \"\"\"\n",[34,839,841],{"class":36,"line":840},29,[34,842,843],{"class":79},"            SELECT id, patient_name, diagnosis, notes\n",[34,845,847],{"class":36,"line":846},30,[34,848,849],{"class":79},"            FROM clinical_records\n",[34,851,853],{"class":36,"line":852},31,[34,854,855],{"class":79},"            WHERE diagnosis ILIKE $1;\n",[34,857,859,862],{"class":36,"line":858},32,[34,860,861],{"class":79},"            \"\"\"",[34,863,706],{"class":54},[34,865,867,870,873,876,879,882],{"class":36,"line":866},33,[34,868,869],{"class":47},"            f",[34,871,872],{"class":79},"\"%",[34,874,875],{"class":47},"{",[34,877,878],{"class":54},"query",[34,880,881],{"class":47},"}",[34,883,884],{"class":79},"%\"\n",[34,886,888],{"class":36,"line":887},34,[34,889,890],{"class":54},"        )\n",[34,892,894,897,900,902,905,908,911,914],{"class":36,"line":893},35,[34,895,896],{"class":47},"        return",[34,898,899],{"class":54}," [",[34,901,70],{"class":58},[34,903,904],{"class":54},"(row) ",[34,906,907],{"class":47},"for",[34,909,910],{"class":54}," row ",[34,912,913],{"class":47},"in",[34,915,916],{"class":54}," rows]\n",[14,918,919],{},"Even if the agent hallucinates a different ID, or a user injects an arbitrary instruction, the database guarantees that the connection cannot read or modify rows belonging to another tenant.",[160,921],{},[163,923,925],{"id":924},"_3-verification-failure-testing","3. Verification & Failure Testing",[14,927,928],{},"To verify this architecture in CI, we run adversarial assertions:",[189,930,931,945,958],{},[192,932,933,934,937,938,941,942,235],{},"Cross-tenant reads: Connecting under ",[31,935,936],{},"tenant-alpha"," and querying a record from ",[31,939,940],{},"tenant-beta"," returns ",[31,943,944],{},"0 rows",[192,946,947,948,951,952,954,955,235],{},"Injection writes: Inserting a row with ",[31,949,950],{},"tenant_id = tenant-beta"," under ",[31,953,936],{}," raises ",[31,956,957],{},"new row violates row-level security policy",[192,959,960,961,964,965,968],{},"Unset context: Queries executed without ",[31,962,963],{},"set_config"," evaluate ",[31,966,967],{},"NULLIF"," to null, blocking all data access.",[160,970],{},[163,972,974],{"id":973},"_4-key-takeaways","4. Key Takeaways",[976,977,978,981,991],"ul",{},[192,979,980],{},"Never trust model-provided parameters for authorization; let LLMs pick operational filters while the application context enforces tenancy boundaries.",[192,982,983,984,987,988,990],{},"Pass ",[31,985,986],{},"true"," as the third parameter to ",[31,989,963],{},", so the setting remains local to the transaction, avoiding pool contamination.",[192,992,993],{},"Centralizing authorization in PostgreSQL RLS policies eliminates repetitive scoping checks across dozens of individual agent tools.",[995,996,997],"style",{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}",{"title":29,"searchDepth":44,"depth":44,"links":999},[1000,1001,1005,1009,1010],{"id":165,"depth":44,"text":166},{"id":220,"depth":44,"text":1002,"children":1003},"Architectural Pattern: Session-Scoped Context via SET LOCAL",[1004],{"id":246,"depth":76,"text":247},{"id":452,"depth":44,"text":453,"children":1006},[1007,1008],{"id":459,"depth":76,"text":460},{"id":557,"depth":76,"text":558},{"id":924,"depth":44,"text":925},{"id":973,"depth":44,"text":974},"\u002Fimages\u002Fcovers\u002Fpostgresql-rls-langgraph-agent-isolation.png","2026-11-09","How to prevent cross-tenant data leaks in autonomous LangGraph workflows by binding session-scoped tenant claims to PostgreSQL Row-Level Security policies.","md",{"canonical_url":1016},"https:\u002F\u002Fmihai.ltd\u002Fblog\u002Fpostgresql-rls-langgraph-agent-isolation\u002F","\u002Fblog\u002Fpostgresql-rls-langgraph-agent-isolation",{"title":5,"description":1013},"blog\u002Fpostgresql-rls-langgraph-agent-isolation",[1021,1022,1023,1024,1025],"postgresql","security","langgraph","agentic-ai","architecture","blog_post","_qg8SXeQA6tWd4cM1MKYYE602qZeYIPslLn8ojAZ9lk",null,1794289299661]