'Manage Exa API key scoping, team access controls, and domain restrictions.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add jeremylongshore/tons-of-skills-marketplace --skill exa-enterprise-rbac --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Exa Enterprise Rbac?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeremylongshore-exa-enterprise-rbac-3dc8cdc2)More formats (shields.io, HTML) on the badges page.
---
name: exa-enterprise-rbac
description: 'Manage Exa API key scoping, team access controls, and domain restrictions.
Use when implementing multi-key access control, configuring per-team search limits,
or setting up organization-level Exa governance.
Trigger with phrases like "exa access control", "exa RBAC",
"exa enterprise", "exa team keys", "exa permissions".
'
allowed-tools: Read, Write, Edit, Bash(curl:*)
version: 1.11.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- exa
- rbac
- enterprise
compatibility: Designed for Claude Code
---
# Exa Enterprise RBAC
## Output
- A least-privilege Exa project/credential role configuration with named owners, review evidence, and a tested revocation path.
- Verified separation between development, staging, and production access scopes.
## Examples
Grant a development service identity only the search/research capability it needs, verify it against a non-sensitive query, and confirm it cannot use production credentials or administrative actions. Record ownership and access-review date; revoke broad roles and correct group mappings before continuing.
## 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
```typescript
// config/exa-keys.ts
import Exa from "exa-js";
// Create separate clients for each use case
const exaClients = {
// High-volume RAG pipeline — production key with higher limits
ragPipeline: new Exa(process.env.EXA_KEY_RAG!),
// Internal research tool — lower volume key
researchTool: new Exa(process.env.EXA_KEY_RESEARCH!),
// Customer-facing search — separate key for isolation
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
```typescript
// middleware/exa-permissions.ts
interface ExaPermissions {
maxResults: number;
allowedTypes: ("auto" | "neural" | "keyword" | "fast" | "deep")[];
allowedCategories: string[];
includeDomains?: string[]; // restrict to these domains
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: ["company", "news"],
dailySearchLimit: 100,
},
"compliance-team": {
maxResults: 10,
allowedTypes: ["keyword", "auto"],
allowedCategories: [],
includeDomains: ["nist.gov", "owasp.org", "sans.org", "sec.gov"],
dailySearchLimit: 200,
},
};
function validateSearchRequest(
role: string,
searchType: string,
numResults: number,
category?: string
): { allowed: boolean; reason?: string } {
const perms = ROLE_PERMISSIONS[role];
if (!perms) return { allowed: false, reason: "Unknown role" };
if (!perms.allowedTypes.includes(searchType as any)) {
return { allowed: false, reason: `Search type ${searchType} not allowed for ${role}` };
}
if (numResults > perms.maxResults) {
return { allowed: false, reason: `Max ${perms.maxResults} results for ${role}` };
}
if (category && perms.allowedCategories.length > 0 && !perms.allowedCategories.includes(category)) {
return { allowed: false, reason: `Category ${category} not allowed for ${role}` };
}
return { allowed: true };
}
```
### Step 3: Domain Restrictions per Team
```typescript
// Enforce domain restrictions so compliance-sensitive teams
// only see results from vetted sources
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",
// Merge domain restrictions from role permissions
includeDomains: perms.includeDomains || opts.includeDomains,
});
}
```
### Step 4: Per-Key Usage Tracking
```typescript
// Track usage per API key / role for budget enforcement
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 Error(
`Daily search limit (${perms.dailySearchLimit}) exceeded for ${role}`
);
}
entry.count++;
this.usage.set(role, entry);
}
getUsage(role: string) {
const entry = this.usage.get(role);
const limit = ROLE_PERMISSIONS[role]?.dailySearchLimit || 0;
return {
used: entry?.count || 0,
limit,
remaining: limit - (entry?.count || 0),
};
}
}
```
### Step 5: Key Rotation Procedure
```bash
set -euo pipefail
# 1. Create new key in Exa dashboard (dashboard.exa.ai)
# 2. Deploy new key alongside old key
# 3. Verify new key works
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}'
# 4. Switch traffic to new key
# 5. Monitor for errors
# 6. Revoke old key in dashboard after 24h
```
## 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
- [Exa API Documentation](https://docs.exa.ai)
- [Exa Dashboard](https://dashboard.exa.ai)
- [Exa API Key Usage](https://docs.exa.ai/reference/team-management/get-api-key-usage)
## Next Steps
For policy enforcement, see `exa-policy-guardrails`. For multi-env setup, see `exa-multi-env-setup`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!