From perplexity-pack
Sets up Perplexity Enterprise RBAC with per-team API keys, gateway enforcement for models, budgets, domains, and rate limits.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin perplexity-packThis skill is limited to using the following tools:
Control access to Perplexity Sonar API at the organizational level. Perplexity does not have built-in RBAC -- you implement access control through: separate API keys per team/environment, a gateway that enforces model and budget policies, and domain restrictions for compliance.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Control access to Perplexity Sonar API at the organizational level. Perplexity does not have built-in RBAC -- you implement access control through: separate API keys per team/environment, a gateway that enforces model and budget policies, and domain restrictions for compliance.
| Layer | Mechanism | Perplexity Support |
|---|---|---|
| Authentication | API key per team | Yes (multiple keys) |
| Model restriction | Gateway enforcement | Build yourself |
| Budget cap | Per-key monthly limit | Via dashboard |
| Domain restriction | search_domain_filter | Yes (per-request) |
| Rate limiting | Gateway + key limits | Yes (per-key RPM) |
Generate separate keys at perplexity.ai/settings/api:
Key: pplx-support-bot-prod → Budget: $200/mo, sonar only
Key: pplx-research-team → Budget: $1000/mo, sonar + sonar-pro
Key: pplx-data-team → Budget: $500/mo, sonar only
Key: pplx-executive-reports → Budget: $300/mo, sonar-pro
// perplexity-gateway.ts
import OpenAI from "openai";
interface TeamPolicy {
apiKey: string;
allowedModels: string[];
maxTokensPerRequest: number;
maxRequestsPerMinute: number;
requiredDomainFilter?: string[]; // Force search to specific domains
blockedDomainFilter?: string[]; // Block specific domains
}
const TEAM_POLICIES: Record<string, TeamPolicy> = {
support: {
apiKey: process.env.PPLX_KEY_SUPPORT!,
allowedModels: ["sonar"],
maxTokensPerRequest: 512,
maxRequestsPerMinute: 30,
},
research: {
apiKey: process.env.PPLX_KEY_RESEARCH!,
allowedModels: ["sonar", "sonar-pro", "sonar-reasoning-pro"],
maxTokensPerRequest: 4096,
maxRequestsPerMinute: 50,
},
compliance: {
apiKey: process.env.PPLX_KEY_COMPLIANCE!,
allowedModels: ["sonar", "sonar-pro"],
maxTokensPerRequest: 2048,
maxRequestsPerMinute: 20,
requiredDomainFilter: ["sec.gov", "edgar.sec.gov", "law.cornell.edu"],
},
marketing: {
apiKey: process.env.PPLX_KEY_MARKETING!,
allowedModels: ["sonar"],
maxTokensPerRequest: 1024,
maxRequestsPerMinute: 20,
blockedDomainFilter: ["-competitor1.com", "-competitor2.com"],
},
};
function enforcePolicy(
team: string,
requestedModel: string,
requestedTokens: number
): { client: OpenAI; model: string; maxTokens: number; domainFilter?: string[] } {
const policy = TEAM_POLICIES[team];
if (!policy) throw new Error(`Unknown team: ${team}`);
if (!policy.allowedModels.includes(requestedModel)) {
console.warn(`Team ${team} not allowed ${requestedModel}, using ${policy.allowedModels[0]}`);
}
const model = policy.allowedModels.includes(requestedModel)
? requestedModel
: policy.allowedModels[0];
const maxTokens = Math.min(requestedTokens, policy.maxTokensPerRequest);
return {
client: new OpenAI({ apiKey: policy.apiKey, baseURL: "https://api.perplexity.ai" }),
model,
maxTokens,
domainFilter: policy.requiredDomainFilter || policy.blockedDomainFilter,
};
}
async function teamSearch(
team: string,
query: string,
requestedModel: string = "sonar"
) {
const { client, model, maxTokens, domainFilter } = enforcePolicy(
team, requestedModel, 2048
);
const response = await client.chat.completions.create({
model,
messages: [{ role: "user", content: query }],
max_tokens: maxTokens,
...(domainFilter && { search_domain_filter: domainFilter }),
} as any);
return {
answer: response.choices[0].message.content,
citations: (response as any).citations || [],
model: response.model,
team,
tokens: response.usage?.total_tokens,
};
}
// Usage
const result = await teamSearch("compliance", "latest SEC filing for AAPL", "sonar-pro");
// -> Uses sonar-pro (allowed for compliance team)
// -> Searches only sec.gov, edgar.sec.gov, law.cornell.edu
const supportResult = await teamSearch("support", "How to reset password", "sonar-pro");
// -> Downgrades to sonar (support team only allowed sonar)
class TeamUsageTracker {
private usage: Map<string, Array<{ timestamp: number; tokens: number; model: string; cost: number }>> = new Map();
record(team: string, tokens: number, model: string) {
const entries = this.usage.get(team) || [];
const cost = model === "sonar-pro" ? tokens * 0.000009 : tokens * 0.000001;
entries.push({ timestamp: Date.now(), tokens, model, cost });
this.usage.set(team, entries);
}
getDailySummary(team: string) {
const today = new Date().toDateString();
const entries = (this.usage.get(team) || []).filter(
(e) => new Date(e.timestamp).toDateString() === today
);
return {
team,
queries: entries.length,
totalTokens: entries.reduce((s, e) => s + e.tokens, 0),
estimatedCost: entries.reduce((s, e) => s + e.cost, 0).toFixed(4),
modelBreakdown: {
sonar: entries.filter((e) => e.model === "sonar").length,
"sonar-pro": entries.filter((e) => e.model === "sonar-pro").length,
},
};
}
}
Rotate API keys every 90 days. Name keys with quarter (pplx-research-2026Q1) for tracking.
set -euo pipefail
# 1. Generate new key at perplexity.ai/settings/api
# 2. Deploy new key alongside old key (24-hour overlap)
# 3. Verify new key works
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $NEW_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"sonar","messages":[{"role":"user","content":"test"}],"max_tokens":5}' \
https://api.perplexity.ai/chat/completions
# 4. Remove old key from perplexity.ai/settings/api
| Issue | Cause | Solution |
|---|---|---|
401 for a team | Key expired or revoked | Regenerate key for that team |
| Model downgrade unexpected | Policy restricting access | Check team's allowedModels |
| Compliance citations from wrong domain | Domain filter not applied | Verify requiredDomainFilter in policy |
| Budget exceeded | Team over monthly cap | Alert team lead, increase cap or throttle |
For migration planning, see perplexity-migration-deep-dive.