codingBy HowDoIUseAI Team

Your AI second brain worked. Now your whole team wants in, and that breaks everything

A personal AI knowledge base doesn't scale to a team without permissions. Here's how to build a team brain that keeps everyone's data separate.

A personal AI second brain is easy to justify. One person, one inbox, one set of Slack channels, one Notion workspace — pipe it all into a vector database, wire up an agent, and suddenly you can ask "what did we decide about pricing in March?" and get a real answer. It feels like magic the first time it works.

Then someone on your team asks for the same thing. And the instinct is to just... give them access to the same brain. Bigger database, more documents, same architecture. That instinct is wrong, and it's wrong in a way that can get you fired if you're not careful.

The problem isn't scale. It's that a personal knowledge base has exactly one implicit permission model: you can see everything, because it's all yours. The moment a second person touches that system, you need an actual permission model — one that decides, on every single query, exactly which rows of data that specific person is allowed to see. Skip that step and you've built a tool that will happily tell a new hire what the CFO said about layoffs in a private Slack thread.

Why can't you just make the personal brain bigger?

Because "bigger" implies the same trust boundary applies to everyone, and it doesn't. Your personal second brain trusts you completely because there's no one else in the loop. A team brain has to assume the opposite by default: nobody sees anything until the system can prove they're allowed to.

This is a real architectural fork, not a config tweak. Most attempts to retrofit permissions onto an existing RAG setup end up doing permission checks after retrieval — pull the top-K most relevant chunks first, then filter out what the user shouldn't see. That approach has a fundamental flaw: the trust boundary moves, unauthorized candidates are retrieved before authorization is enforced, and whether the system returns content or only ids and metadata, enforcement happens too late. Sensitive content has already been touched by the retrieval layer and possibly even reasoned about by the model before anyone checked if that was allowed.

The fix is to push permission enforcement down into the database itself, so it happens during retrieval, not after. This turns authorization into a ranking constraint instead of a post-filtering step. The database simply never returns a row the requester isn't cleared to see — the model never even gets the chance to leak it.

How does row-level security actually make this work?

Row Level Security (RLS) is a native Postgres feature, and it's the backbone of any serious team knowledge base. The concept is refreshingly simple: policies in Row Level Security are used to restrict access to rows in a table — think of them like adding a WHERE clause to every query. You don't write permission-checking code scattered across your application. You write a policy once, attach it to the table, and Postgres enforces it every single time — each policy is attached to a table, and the policy is executed every time a table is accessed.

That last point matters more than it sounds. Because RLS is a Postgres primitive, it also protects your data when it is reached through third-party tooling, which is what makes it "defense in depth." It doesn't matter if the request comes from your chat agent, a script someone wrote at 2am, or a BI tool nobody remembers connecting — the database enforces the rule regardless of the path the query took to get there.

If you're building on Supabase, which pairs Postgres with pgvector out of the box, their RAG with Permissions guide walks through exactly this pattern for AI knowledge bases. The core mechanism: auth.uid() references the JWT's sub claim, which is automatically set at the beginning of each request to the REST API, and all subsequent queries inherit the permission of that user. Every retrieval call carries the identity of the person asking, and the database uses that identity to decide what comes back.

A few things worth knowing before you flip this on:

  1. RLS is deny-by-default. The moment you enable it on a table, every query returns nothing until you write at least one policy. That's intentional — it fails safe instead of failing open.
  2. Grants and policies are two separate checks. Postgres runs two checks before a client touches a table — grants decide whether a role can run an operation on the table at all, and policies decide which rows that operation applies to. Miss the grant and you'll get a confusing error before your policy logic ever runs.
  3. This isn't just for reads. You can create more RLS policies for inserts, updates, and deletes in order to apply the same permission logic for those other operations — useful if different roles on your team can add documents but not delete them.

Postgres isn't the only option here, either. If you're on a dedicated vector database, most of the major players have their own version of this: Weaviate, Pinecone, and Qdrant all support namespace-based isolation and per-collection RBAC, and Milvus added row-level RBAC via bitmap indexing. The specifics differ, but the principle is identical — permission enforcement belongs in the retrieval layer, not bolted on afterward.

How do you label every row without slowing everything down?

Here's where a lot of teams get stuck: RLS only works if every row actually has metadata describing who's allowed to see it. That means every Slack message, every doc, every code snippet flowing into your knowledge base needs to be tagged at ingestion time — which team it belongs to, which project, which sensitivity level, who owns it.

This labeling step is not optional busywork. It's the entire foundation the security model rests on. If a document comes in unlabeled, the safest default is to treat it as maximally restricted until someone classifies it — never assume public access by default.

The practical workflow looks like this:

  • Tag at the source. When your ingestion pipeline pulls from Slack, GitHub, or Notion, capture the native permission context right there — which channel, which repo, which workspace — and store it as metadata alongside the embedding.
  • Normalize into roles. Translate those source-specific permissions into a consistent role or attribute system your database understands (e.g., team:ops, role:manager, project:client-x).
  • Enforce with policies, not application code. Write the RLS policy once against those attributes rather than checking permissions in your agent's code — otherwise every new tool you build has to reimplement the same logic, and eventually someone forgets.

This is essentially Role-Based Access Control applied to a knowledge base instead of a traditional app. Role-Based Access Control is a security model where access to resources is granted based on a user's role within an organization — roles define permissions, and users inherit those permissions, ensuring secure and efficient management of access rights. The advantage over per-user rules is obvious once your team grows past a handful of people: you manage five roles instead of fifty individual permission sets.

What does the agent side of this look like?

Once the database enforces permissions, the agent layer gets simpler, not harder — it doesn't need to know anything about who's allowed to see what. It just needs to authenticate the requester and pass that identity through on every call. This is exactly the kind of problem the Model Context Protocol was built to standardize.

MCP defines a clean contract between your AI application and the tools/data it needs to reach. MCP is an open-source standard for connecting AI applications to external systems — using MCP, AI applications can connect to data sources like local files and databases, tools like search engines and calculators, and workflows like specialized prompts. The official documentation and getting-started guide live at modelcontextprotocol.io/docs, and the GitHub spec repository has the full protocol details if you're implementing a server from scratch.

For a team brain, your MCP server typically exposes a small, deliberate set of tools rather than one giant "search everything" function:

  • An identity tool — confirms who's actually asking, tied to an authenticated key rather than a free-text claim
  • A document search tool — queries the RLS-protected table using that identity
  • A code search tool — same idea, but tuned for repo structure since code chunks behave differently than prose
  • A fetch tool — pulls a full document once search has narrowed things down, still gated by the same permission checks

Every one of these tools passes the caller's authenticated identity down to the database on every single request. The MCP server itself holds no special knowledge of who can see what — it just plumbs the identity through, and Postgres (or whatever database you're using) makes the actual decision. If tools access private data or perform actions for a user, protect the server with the authorization flow defined by the MCP specification.

This separation is deliberate and important: your agent logic and your security logic never touch. Change the permission model and you don't have to touch a single line of agent code. Add a new agent and you don't have to reimplement security — it's already enforced at the layer beneath.

Which mistakes actually cause data leaks?

A few patterns show up again and again in teams building this for the first time:

Filtering after retrieval instead of during it. As covered above, pulling the top matches and then checking permissions is backwards. At query time, the retrieval query should include a filter that restricts results to chunks the requesting user has permission to see — the vector store should never return an unauthorized document in the first place. If your architecture can't do that, the model has already "seen" the leak even if it never repeats it back — and eventually it will repeat it back.

Trusting client-supplied identity. Never let the calling application tell the database who the user is. The identity has to come from a verified token — a JWT, a signed API key, something the database itself can validate — not a field the client sets on a request.

Treating labels as a one-time job. Permissions change. People change teams, projects get archived, contractors leave. If your labeling pipeline only runs at ingestion and never revisits old rows, your team brain will slowly drift out of sync with reality. Build a re-labeling job, even a crude one, that runs on a schedule.

Skipping the audit trail. When something does go wrong — and eventually something will — you want a log of exactly which identity queried which rows and when. This isn't optional for anything handling business-sensitive data.

What should you actually build first?

Don't start by ripping apart your personal second brain. Start by standing up a single table with proper row-level metadata and RLS policies, wired to one MCP server with two tools: identity check and search. Get that working for two or three people in two clearly different roles — someone in ops, someone in engineering — and verify, concretely, that asking the same question returns different answers depending on who's asking.

Only after that foundation holds should you pour in the rest of your team's chat history, docs, and repos. The database is doing the hard work here, not the agent — which means the smartest agent in the world can't save you from a permission model you didn't design carefully.

The personal second brain was about proving AI could remember things for you. The team brain is about proving it can be trusted to forget the things it's not supposed to share. That's a much higher bar — and it's the one that actually matters once more than one person is asking it questions.