Coming from Vanna

Vanna's repository was archived on 29 March 2026 and is read-only. If you built on it, this page is about one specific thing it never did, and how to add that thing to whatever you migrate to — including keeping your Vanna code as-is.

What Vanna did with identity, and what it didn't

Vanna 2.0 resolves a User with group_memberships, gates tools on those groups, and applies row-level security when the SQL runs:

Execute SQL tool (user-aware) → Apply row-level security → Filtered results

That protects the data. It does not protect the answer. The model still sees the whole schema, still writes valid SQL against a table this user can't read, RLS strips every row, and the user is told "no records found" — a wrong answer delivered with confidence. Nothing in that chain can tell the difference between "there is no data" and "you are not allowed to see it."

schemagate works one step earlier. It decides which tables the model is shown, per caller, before any SQL exists. A restricted table is not de-ranked; it is absent from the prompt.

The mapping

Vanna schemagate
vn.train(ddl=...) (1.x) / tools reading a configured DB (2.x) cat = Catalog().bootstrap("postgresql://…") — reflects the schema once
vn.train(documentation=...) cat.hint("orders", "…") — a human note that outranks everything
User(id=…, group_memberships=[…]) Principal("okta:jdoe", roles={…})
a tool's access_groups cat.restrict("hr_compensation", ["payroll"]) — on the table, not the tool
vn.ask(question) / chat_sse sel = cat.select(question, principal=p); put sel.prompt_fragment() in your SQL prompt
vn.generate_sql(...) not schemagate's job — keep whatever model call you have

schemagate is not an agent, a chat server, or a SQL generator. It is the schema selection step. Keep your Vanna agent, LangChain chain, or hand-rolled loop; replace the part that decides what DDL goes in the prompt.

Minimal example

from schemagate import Catalog, Principal

cat = Catalog().bootstrap("postgresql://localhost/app")
cat.restrict("hr_compensation", ["payroll"])          # once, at startup

def build_prompt(question, user):                      # per request
    p = Principal(f"okta:{user.id}", roles=set(user.group_memberships))
    sel = cat.select(question, top_k=6, principal=p)
    return f"Schema:\n{sel.prompt_fragment()}\n\nQuestion: {question}"

Your User object drops straight in: id becomes the principal's subject, group_memberships become its roles. Whatever generated SQL before still does — it just never sees hr_compensation unless the caller holds payroll.

If you're using an agent framework

Cursor, Windsurf, or any MCP client: run python -m schemagate.mcp_server and the agent calls select_schema(question, principal, roles) as a tool. LangChain: SchemagateRetriever is a BaseRetriever. Both are in the README.

What you lose, honestly

Vanna trained on question/SQL pairs and learned from feedback. schemagate does not learn; it reflects and ranks. If your accuracy came from a large question–SQL memory, keep that memory and use schemagate only for the identity gate. If it came from schema documentation, cat.hint() and cat.describe() do the same job with less machinery.

GitHubTry the demo