Exa Enterprise RBAC
Overview
Manage access to Exa search API through API key scoping and application-level controls. Exa is API-key-based (no built-in RBAC), so access control is implemented through multiple API keys per use case, application-layer permission enforcement, domain restrictions per team, and per-key usage monitoring.
Prerequisites
- Exa API account with team/enterprise plan
- Dashboard access at dashboard.exa.ai
- Multiple API keys for key isolation
Instructions
Step 1: Key-Per-Use-Case Architecture
import Exa from "exa-js";
const exaClients = {
ragPipeline: new Exa(process.env.EXA_KEY_RAG!),
researchTool: new Exa(process.env.EXA_KEY_RESEARCH!),
customerSearch: new Exa(process.env.EXA_KEY_CUSTOMER!),
};
export function getExaForUseCase(
useCase: keyof typeof exaClients
): Exa {
const client = exaClients[useCase];
if (!client) throw new Error(`No Exa client for use case: ${useCase}`);
return client;
}
Step 2: Application-Level Permission Enforcement
interface ExaPermissions {
maxResults: number;
allowedTypes: ("auto" | "neural" | "keyword" | "fast" | "deep")[];
allowedCategories: string[];
includeDomains?: string[];
dailySearchLimit: number;
}
const ROLE_PERMISSIONS: Record<string, ExaPermissions> = {
"rag-pipeline": {
maxResults: 10,
allowedTypes: ["neural", "auto"],
allowedCategories: [],
dailySearchLimit: 10000,
},
"research-analyst": {
maxResults: 25,
allowedTypes: ["neural", "keyword", "auto", "deep"],
allowedCategories: ["research paper", "news"],
dailySearchLimit: 500,
},
"marketing-team": {
maxResults: 5,
allowedTypes: ["keyword", "auto"],
allowedCategories: [, ],
: ,
},
: {
: ,
: [, ],
: [],
: [, , , ],
: ,
},
};
(): { : ; ?: } {
perms = [role];
(!perms) { : , : };
(!perms..(searchType )) {
{ : , : };
}
(numResults > perms.) {
{ : , : };
}
(category && perms.. > && !perms..(category)) {
{ : , : };
}
{ : };
}
Step 3: Domain Restrictions per Team
async function enforcedSearch(
exa: Exa,
role: string,
query: string,
opts: any = {}
) {
const perms = ROLE_PERMISSIONS[role];
if (!perms) throw new Error(`Unknown role: ${role}`);
const validation = validateSearchRequest(
role,
opts.type || "auto",
opts.numResults || 10,
opts.category
);
if (!validation.allowed) throw new Error(validation.reason);
return exa.searchAndContents(query, {
...opts,
numResults: Math.min(opts.numResults || 10, perms.maxResults),
type: opts.type || "auto",
includeDomains: perms.includeDomains || opts.includeDomains,
});
}
Step 4: Per-Key Usage Tracking
class KeyUsageTracker {
private usage = new Map<string, { count: number; resetAt: number }>();
checkAndIncrement(role: string): void {
const perms = ROLE_PERMISSIONS[role];
if (!perms) throw new Error(`Unknown role: ${role}`);
const now = Date.now();
const dayStart = new Date().setHours(0, 0, 0, 0);
let entry = this.usage.get(role);
if (!entry || entry.resetAt < now) {
entry = { count: 0, resetAt: dayStart + 24 * 60 * 60 * 1000 };
}
if (entry.count >= perms.dailySearchLimit) {
throw new (
);
}
entry.++;
..(role, entry);
}
() {
entry = ..(role);
limit = [role]?. || ;
{
: entry?. || ,
limit,
: limit - (entry?. || ),
};
}
}
Step 5: Key Rotation Procedure
set -euo pipefail
curl -s -o /dev/null -w "%{http_code}" \
-X POST https://api.exa.ai/search \
-H "x-api-key: $NEW_EXA_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"key rotation test","numResults":1}'
Error Handling
| Issue | Cause | Solution |
|---|
401 on search | Invalid or revoked API key | Regenerate in dashboard |
429 rate limited | Key-level rate limit exceeded | Distribute across keys |
| Daily limit hit | Search budget exhausted | Adjust limits or wait for reset |
| Wrong domain results | Missing domain filter | Apply includeDomains per role |
Resources
Next Steps
For policy enforcement, see exa-policy-guardrails. For multi-env setup, see exa-multi-env-setup.