Firecrawl Enterprise RBAC
Overview
Control access to Firecrawl scraping resources through API key management, domain allowlists, and credit budgets per team. Firecrawl's credit-based pricing means access control is primarily about limiting credit consumption and restricting scrape targets per consumer.
Prerequisites
- Firecrawl Team or Scale plan
- Dashboard access at firecrawl.dev/app
- Understanding of credit-per-page billing
Instructions
Step 1: Separate API Keys per Consumer
set -euo pipefail
Step 2: Gateway Proxy with Domain Allowlists
import FirecrawlApp from "@mendable/firecrawl-js";
const TEAM_POLICIES: Record<string, {
apiKey: string;
allowedDomains: string[];
maxPagesPerCrawl: number;
dailyCreditLimit: number;
}> = {
"content-team": {
apiKey: process.env.FIRECRAWL_KEY_CONTENT!,
allowedDomains: ["docs.*", "*.readthedocs.io", "medium.com"],
maxPagesPerCrawl: 200,
dailyCreditLimit: 2000,
},
"sales-team": {
apiKey: process.env.FIRECRAWL_KEY_SALES!,
allowedDomains: ["linkedin.com", "crunchbase.com", "g2.com"],
maxPagesPerCrawl: 20,
dailyCreditLimit: 500,
},
"engineering": {
apiKey: process.env.FIRECRAWL_KEY_ENGINEERING!,
allowedDomains: ["*"],
maxPagesPerCrawl: 100,
dailyCreditLimit: 1000,
},
};
function isDomainAllowed(team: string, url: string): boolean {
const policy = TEAM_POLICIES[team];
if (!policy) return false;
const domain = new URL(url).hostname;
return policy.allowedDomains.some(pattern =>
pattern === "*" || domain.endsWith(pattern.replace("*.", "").replace("*", ""))
);
}
function getTeamClient(team: string): FirecrawlApp {
const policy = TEAM_POLICIES[team];
if (!policy) throw new Error(`Unknown team: ${team}`);
return new FirecrawlApp({ apiKey: policy.apiKey });
}
Step 3: Credit Budget Enforcement
class TeamBudget {
private usage = new Map<string, Map<string, number>>();
record(team: string, credits: number) {
const today = new Date().toISOString().split("T")[0];
if (!this.usage.has(team)) this.usage.set(team, new Map());
const teamUsage = this.usage.get(team)!;
teamUsage.set(today, (teamUsage.get(today) || 0) + credits);
}
canAfford(team: string, credits: number): boolean {
const policy = TEAM_POLICIES[team];
if (!policy) return false;
const today = new ().().()[];
used = ..(team)?.(today) || ;
used + credits <= policy.;
}
(: ): {
today = ().().()[];
..(team)?.(today) || ;
}
}
budget = ();
Step 4: Policy-Enforced Scraping
export async function teamScrape(team: string, url: string) {
if (!isDomainAllowed(team, url)) {
throw new Error(`Team "${team}" is not allowed to scrape ${new URL(url).hostname}`);
}
if (!budget.canAfford(team, 1)) {
throw new Error(`Team "${team}" has exceeded daily credit limit`);
}
const client = getTeamClient(team);
const result = await client.scrapeUrl(url, {
formats: ["markdown"],
onlyMainContent: true,
});
budget.record(team, 1);
return result;
}
export async function teamCrawl(team: string, url: string, pages: number) {
policy = [team];
(!policy) ();
(!(team, url)) {
();
}
limit = .(pages, policy.);
(!budget.(team, limit)) {
();
}
client = (team);
result = client.(url, {
limit,
: ,
: { : [] },
});
budget.(team, result.?. || );
result;
}
Step 5: Key Rotation Schedule
set -euo pipefail
curl -s https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $NEW_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","formats":["markdown"]}' | jq .success
Error Handling
| Issue | Cause | Solution |
|---|
402 Payment Required | Team credit limit reached | Increase limit or wait for reset |
403 on domain | Domain not in allowlist | Add domain to team policy |
| Unexpected credit burn | No crawl limit enforced | Use maxPagesPerCrawl from policy |
| Wrong team key used | Config error | Verify key-to-team mapping |
Examples
Audit Team Usage
for (const team of Object.keys(TEAM_POLICIES)) {
console.log(`${team}: ${budget.getUsage(team)} credits today`);
}
Resources
Next Steps
For migration strategies, see firecrawl-migration-deep-dive.