Authentication & Access Control

How SwarmV authenticates your backend and your agents, and how it decides which vectors each user can see.

API keys and user tokens

SwarmV uses two kinds of credentials, and keeping them apart is what protects you from prompt injection:

API keys

evx_live_… / evx_test_…. For your backend only. Created in the dashboard with a name, a set of permissions and an optional expiry. The key is shown once; we store only a hash.

User tokens

Short-lived (up to one hour) and bound to one user. Created by your backend with an API key and given to agents, MCP servers and client apps. The user is part of the signed token, so nothing in a prompt can change who the agent acts as.

Both SDKs read SWARMV_API_KEY (backend) or SWARMV_USER_TOKEN from the environment if you don't pass credentials explicitly.

Permissions

Give each key only what it needs. User tokens can be narrowed further when you create them, and can never receive graph:admin or vector:impersonate.

vector:readSearch vectors.
vector:writeInsert and delete vectors.
graph:readList relations and check who can access what (backend keys only; never included in user tokens).
graph:adminChange who can access what (grant and revoke relations).
vector:impersonateLet a backend key search or write as any user by passing auth_user_id. Grant it only to trusted services.

Accounts and organizations

Every key, relation and vector belongs to a tenant, and tenants never see each other's data. If you sign in as an individual, your account is the tenant. If you work inside an organization (Enterprise teams), the organization is the tenant: everyone in it shares the same keys and data, and only organization admins can create, rotate or revoke keys.

Giving an agent a user token

1. Add an endpoint to your backend that returns a token for the signed-in user:

TypeScript (backend)
// Your backend (e.g. a Next.js route). Runs with your API key, never in the browser or agent.
import { SwarmV } from "@swarmv/sdk";

const swarmv = new SwarmV(); // SWARMV_API_KEY from the environment

export async function POST(req: Request) {
  const user = await getSignedInUser(req);          // your own auth
  if (!user) return new Response("Unauthorized", { status: 401 });

  const token = await swarmv.generateUserToken(`user:${user.id}`, {
    expiresIn: 900,                                  // seconds, max 3600
    scopes: ["vector:read", "vector:write"],         // optional: narrower than the key
  });
  return Response.json({ token });
}
Python (backend)
from swarmv import SwarmV

swarmv = SwarmV()  # SWARMV_API_KEY from the environment

def swarmv_token_for(user_id: str) -> str:
    return swarmv.generate_user_token(f"user:{user_id}", 900, scopes=["vector:read"])

2. Have the agent fetch its token from that endpoint. User ids always start with user:.

TypeScript (agent)
import { SwarmV } from "@swarmv/sdk";

// In the agent / client: no API key. getToken() is called again automatically
// when the token expires, so long-running agents keep working.
const memory = new SwarmV({
  getToken: async () => {
    const res = await fetch("https://your-backend.example.com/swarmv-token", { method: "POST", headers: { Authorization: `Bearer ${session}` } });
    return (await res.json()).token;
  },
});

// Only returns vectors in containers this user can view.
const results = await memory.search(queryEmbedding, { topK: 5 });

// Writes need a container the user can edit.
await memory.insert({ id: "4021", vector: noteEmbedding, metadata: { container_id: "container:apollo" } });
await memory.delete("4021", { containerId: "container:apollo" });
Python (agent)
from swarmv import SwarmV

# token_provider is called on first use and whenever the token expires (sync or async).
memory = SwarmV(token_provider=lambda: fetch_token_from_my_backend())

results = memory.search(query_embedding, top_k=5)
memory.delete("4021", container_id="container:apollo")

Managing access at scale

Grant access to groups, not individual users: give group:engineering viewer on a container once, then manage who is in the group. Thousands of users then need only a few hundred relationships.

On Pro and Enterprise you can change many relationships at once: batches of up to 500 changes (all-or-nothing), and sync, which makes a group's members (or a container's viewers) exactly the list you send and applies only the difference. A sync that would remove more than 20% of the current members is refused unless you confirm it with allowLargeRemoval, so an empty or wrong list can't wipe access by accident. Every request carries an idempotency key, so retries never apply twice. In the dashboard, Access Control → Import / Export does the same with a CSV file (subject,relation,object[,op]), with a preview before anything changes.

TypeScript
// Pro and Enterprise. Backend key with graph:admin.
// Many changes at once: sent in batches of 500, each all-or-nothing, safe to retry.
await memory.graph.batch([
  { subject: "user:ana", relation: "member", object: "group:engineering" },
  { subject: "user:bob", relation: "member", object: "group:engineering" },
  { op: "remove", subject: "user:carl", relation: "member", object: "group:engineering" },
]);

// Mirror a list you keep elsewhere (HR system, identity provider), e.g. nightly:
// afterwards group:engineering's members are exactly these.
const preview = await memory.graph.sync(
  { object: "group:engineering", relation: "member", subjects: employeesInEngineering },
  { dryRun: true },
); // { current, would_add, would_remove }
await memory.graph.sync({ object: "group:engineering", relation: "member", subjects: employeesInEngineering });

// Backup or audit: every relationship, page by page.
for await (const edge of memory.graph.exportRelations()) console.log(edge);
Python
swarmv.batch_relations([
    {"subject": "user:ana", "relation": "member", "object": "group:engineering"},
    {"op": "remove", "subject": "user:carl", "relation": "member", "object": "group:engineering"},
])

swarmv.sync_relations("group:engineering", "member", employees_in_engineering, dry_run=True)
swarmv.sync_relations("group:engineering", "member", employees_in_engineering)

for edge in swarmv.export_relations():
    print(edge)

Permissions follow within about a minute everywhere. Basic changes one relationship at a time; export works on every plan.

Rotation, revocation and expiry

  • Revoke a key from the dashboard. The key and every user token it created stop working within about a minute, worldwide.
  • Rotate a key to get a new secret under the same key. Choose how many hours the old secret keeps working while you redeploy (0 if it may have leaked). Tokens created before the rotation stop working immediately either way.
  • Expiry: keys can expire after 7, 30 or 90 days. When a key expires, so do the tokens it created.
  • New keys and changes take up to about a minute to reach every location. If a brand-new key gets a 401, retry shortly.

Eventual consistency

SwarmV serves reads from Cloudflare's global network while writes are processed through queues, so there is a short window before changes are visible everywhere:

  • Vectors are usually searchable within a few seconds of being accepted (writes return 202 Accepted).
  • Access changes (new relations, removed relations) usually apply within a few seconds, and everywhere within about a minute.
  • Keys: creation, revocation and rotation reach every location within about a minute.

Design for this window: don't expect a vector written in Tokyo to be searchable in Frankfurt 10ms later.

Relationship-based access control (ReBAC)

Every vector lives in a container, set with metadata.container_id. Who can see or change a container is decided by a graph of relations, in the style of Google's Zanzibar: users belong to groups, groups get access to folders or containers, and folders contain containers. Access follows the graph, so you can model deeply nested teams without copying permissions onto every vector.

memberuser:x or group:x → group:y: inherits everything the group can access.
viewer→ folder:x or container:x: can search it.
editor→ folder:x or container:x: can search, insert and delete (implies viewer).
parentfolder:x → container:y: access to the folder applies to the container.
TypeScript (backend)
// Backend, with a key that has graph:admin
await swarmv.graph.createRelation("user:dave", "member", "group:engineering");
await swarmv.graph.createRelation("group:engineering", "viewer", "folder:handbook");
await swarmv.graph.createRelation("folder:handbook", "parent", "container:apollo");

// Dave can now view container:apollo (through his group and the folder).
// Removing any edge removes the access again:
await swarmv.graph.deleteRelation("user:dave", "member", "group:engineering");

// With graph:read (or graph:admin): inspect the graph
const { edges, total } = await swarmv.graph.listRelations({ subject: "user:dave" });
const check = await swarmv.graph.checkAccess("user:dave", "container:apollo", "viewer");
// { allowed: true, granted: "viewer", path: [{ node: "user:dave" },
//   { relation: "member", node: "group:engineering" }, { relation: "viewer", node: "folder:handbook" },
//   { relation: "parent", node: "container:apollo" }] }
Python (backend)
swarmv.create_relation("user:dave", "member", "group:engineering")
swarmv.create_relation("group:engineering", "editor", "container:apollo")
swarmv.delete_relation("user:dave", "member", "group:engineering")

# With graph:read (or graph:admin)
page = swarmv.list_relations(object="container:apollo")       # {"edges": [...], "total": ...}
swarmv.check_access("user:dave", "container:apollo", "editor") # {"allowed": ..., "path": [...]}

A user token's searches only return vectors from containers that user can view; writes and deletes need editor access. A document's container is fixed when it is first written, so an id can't be reused to overwrite a document in another container.

Relation changes reach search permissions at the edge within a few seconds. checkAccess reads the graph directly, so it reflects a change immediately. You can also manage relations, trace access and read the audit log in the dashboard's Auth Graph tab.

Plan limits

Access control and user tokens are included on every plan. Plans differ in how many relationships an account can hold and how long the audit log is kept.

BasicProEnterprise
Relationships per account10,0001,000,000Unlimited
Audit log kept for7 days90 days1 year, CSV export
Single sign-on––SAML, set up with our team

A relationship is one edge in the graph, such as user:dave member group:engineering. Adding one when the account is full returns 403 with "code": "relationship_limit"; re-adding an existing relationship and removing relationships always work. Model teams with groups rather than granting every user to every container, and the count stays small. Usage is shown in the Auth Graph tab. If a subscription ends, existing relationships keep working, but new ones are refused while the account is over the Basic limit.

The audit log records key changes (create, rotate, revoke) and relationship changes, with who made them. Entries older than the plan's window are deleted automatically.

How it runs at the edge

1. Container bitmaps

Each container keeps a compressed RoaringBitmap of the vectors inside it, so adding a vector updates one small record.

2. Precomputed access lists

When relations change, SwarmV walks the graph with a recursive SQL query and stores each affected user's viewable and editable containers at the edge.

3. Query-time filtering in WebAssembly

At query time the Edge Worker combines the bitmaps of the user's containers and the Rust/Wasm core searches only within them, so vectors the user can't see are never returned.