@repo/security
Bot detection, rate limiting, and security headers for Next.js. Block scrapers with AI-powered detection. Prevent abuse with distributed rate limiting. Protect users with CSP and HSTS headers. Works in development without config, requires setup in production.
Quick Start
Add production security in 10 minutes:
pnpm add @repo/securityAI bot detection, Redis rate limiting, security headers. Skip to Quick Start →
Why @repo/security?
Bots scrape your content for AI training. Attackers brute-force login endpoints. Missing security headers expose users to XSS and clickjacking. Rate limiting with in-memory stores doesn't scale across servers. OAuth providers get blocked by Content Security Policy. Manual bot allow lists are incomplete.
@repo/security solves this with Arcjet AI bot detection, Upstash Redis rate limiting, and Nosecone security headers.
Production-ready with fail-closed security, graceful dev degradation, multiple rate limit strategies, and OAuth-compatible CSP.
Use cases
- API protection — Rate limit endpoints to prevent abuse (10 req/min, 100 req/hour)
- Bot blocking — Allow search engines, block AI scrapers and malicious bots
- Auth security — Limit login attempts (5 tries per 15 min) to prevent brute force
- Content protection — Prevent scraping with AI-powered bot detection
- Header security — CSP, HSTS, X-Frame-Options automatically configured
How it works
@repo/security combines three security layers:
import { secure, applyRateLimit } from "@repo/security/server/next";
import { noseconeMiddleware } from "@repo/security/middleware";
// 1. Security headers (middleware)
export default noseconeMiddleware(noseconeOptions);
// 2. Bot detection + rate limiting (API routes)
export async function POST(request: Request) {
// Allow search engines, block AI scrapers
await secure(["GOOGLEBOT", "BINGBOT"], request);
// 10 requests per minute per IP
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
const result = await applyRateLimit(ip, "api");
if (!result.success) {
return new Response("Too many requests", { status: 429 });
}
return Response.json({ success: true });
}Uses Arcjet Shield for AI bot detection, Upstash Redis for distributed rate limiting, and Nosecone for security headers.
Key features
AI bot detection — Arcjet Shield identifies scrapers with machine learning
Rate limiting — Sliding window, fixed window, token bucket (Redis-backed)
Security headers — CSP, HSTS, X-Frame-Options, Permissions Policy
Development mode — Works without config, logs warnings instead of errors
Production fail-closed — Denies requests on errors (Redis down, Arcjet timeout)
Multiple strategies — Pre-configured limiters for API, auth, upload, webhook, search
Quick Start
1. Install and configure API keys
pnpm add @repo/security# Get keys from:
# Arcjet: https://app.arcjet.com
# Upstash: https://console.upstash.com
ARCJET_KEY=ajkey_xxxxxxxxxxxxx
UPSTASH_REDIS_REST_URL=https://xxxxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxxxxxxxxx2. Add security headers middleware
import { noseconeMiddleware, noseconeOptions } from "@repo/security/middleware";
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"]
};
export default noseconeMiddleware(noseconeOptions);3. Protect API routes with bot detection
import { secure, applyRateLimit } from "@repo/security/server/next";
export async function POST(request: Request) {
// Block bots, allow search engines
await secure(["GOOGLEBOT", "BINGBOT"], request);
// Rate limit: 5 login attempts per 15 minutes
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
const result = await applyRateLimit(ip, "auth");
if (!result.success) {
return new Response("Too many requests", {
status: 429,
headers: { "Retry-After": String(Math.ceil((result.reset - Date.now()) / 1000)) }
});
}
// Process login
return Response.json({ success: true });
}4. Use pre-configured rate limiters
import { rateLimiters } from "@repo/security/server/next";
export async function GET(request: Request) {
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
// 100 requests per minute for API endpoints
const result = await rateLimiters.api.limit(ip);
if (!result.success) {
return new Response("Rate limit exceeded", { status: 429 });
}
return Response.json({ data: [] });
}That's it! Your app now has bot detection, rate limiting, and security headers protecting all routes.
Development mode
Works without API keys in development—gracefully degrades and logs warnings. Production requires proper configuration.
Distribution
This package is available as @oneapp/security for use outside the monorepo.
npm install @oneapp/securityBuild configuration: Uses tsdown with
createDistConfig('node', ...) for distribution builds.
Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | packages/security |
| Dependencies | Arcjet, Nosecone, Upstash Redis |
| Node Required | >=22.0.0 |
| Edge Support | Yes (Next.js middleware) |
Features
🤖 Bot Detection (Arcjet)
- AI-powered - Intelligent bot classification with Shield protection
- Customizable - Allow/deny lists for specific bots and categories
- Decision logging - Complete audit trail for all security decisions
- Low latency - Edge-optimized for minimal performance impact
🛡️ Security Headers (Nosecone)
- CSP - Content Security Policy with customizable directives
- HSTS - HTTP Strict Transport Security
- X-Frame-Options - Clickjacking protection
- Cross-Origin Policies - COEP, COOP, CORP headers
- Permissions Policy - Feature permission control
⏱️ Rate Limiting (Upstash)
- Sliding Window - Smooth rate limiting over rolling time windows
- Fixed Window - Traditional rate limiting per fixed period
- Token Bucket - Burst handling with token refill
- Distributed - Redis-backed for multi-instance deployments
- Pre-configured - Ready-to-use limiters for common use cases
Export Paths
| Path | Description |
|---|---|
@repo/security/server | Generic server-side utilities |
@repo/security/server/next | Next.js server integration |
@repo/security/client | Generic client utilities |
@repo/security/client/next | Next.js client utilities |
@repo/security/keys | Environment variables |
Bot Detection
Allow Specific Bots
import { secure } from "@repo/security/server/next";
// Allow search engine crawlers
// highlight-next-line
await secure(["GOOGLEBOT", "BINGBOT", "DUCKDUCKBOT"], request);
// Allow social media preview bots
await secure(["FACEBOOKBOT", "TWITTERBOT", "LINKEDINBOT"], request);
// Allow monitoring tools
await secure(["MONITOR"], request); // Category
// Allow AI crawlers
await secure(["AI"], request); // CategoryBlock All Bots
// Block all automated traffic
// highlight-next-line
await secure([], request);Available Bot Types
Individual Bots:
GOOGLEBOT- Google search crawlerBINGBOT- Bing search crawlerDUCKDUCKBOT- DuckDuckGo crawlerFACEBOOKBOT- Facebook link previewTWITTERBOT- Twitter card previewLINKEDINBOT- LinkedIn previewSLACKBOT- Slack link previewDISCORDBOT- Discord embed preview
Categories:
CRAWLER- All web crawlersPREVIEW- Social media preview botsMONITOR- Monitoring and uptime toolsAI- AI training scrapersSEO- SEO analysis toolsARCHIVE- Web archiving services
Error Handling
Production Behavior
In production, bot detection failures deny requests (fail closed). In development, requests are allowed with warnings.
import { secure, SecurityDenialError } from "@repo/security/server/next";
try {
await secure(["GOOGLEBOT"], request);
// Request is allowed
} catch (error) {
// highlight-start
if (error instanceof SecurityDenialError) {
console.error("Security denial:", {
reason: error.reason, // 'bot' | 'rate_limit' | 'shield' | 'unknown'
ip: error.metadata?.ip,
path: error.metadata?.path,
userAgent: error.metadata?.userAgent,
decisionId: error.metadata?.decisionId
});
return new Response("Access denied", { status: 403 });
}
// highlight-end
throw error;
}Rate Limiting
Pre-configured Limiters
import { rateLimiters } from "@repo/security/server/next";
// API endpoints - 100 requests per minute
// highlight-next-line
const apiResult = await rateLimiters.api.limit(identifier);
// Authentication - 5 attempts per 15 minutes
const authResult = await rateLimiters.auth.limit(userId);
// File uploads - 10 uploads per hour
const uploadResult = await rateLimiters.upload.limit(userId);
// Webhooks - 1000 requests per hour
const webhookResult = await rateLimiters.webhook.limit(webhookId);
// Search - 500 requests per minute
const searchResult = await rateLimiters.search.limit(ip);Basic Usage
import { applyRateLimit } from "@repo/security/server/next";
// Apply rate limit
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
// highlight-next-line
const result = await applyRateLimit(ip, "api");
if (!result.success) {
return new Response("Too many requests", {
status: 429,
headers: {
"X-RateLimit-Limit": String(result.limit),
"X-RateLimit-Remaining": String(result.remaining),
"X-RateLimit-Reset": String(result.reset),
"Retry-After": String(Math.ceil((result.reset - Date.now()) / 1000))
}
});
}Custom Rate Limiters
import { createRateLimiter, slidingWindow, fixedWindow, tokenBucket } from "@repo/security/server/next";
// Sliding window - 50 requests per 10 seconds
// highlight-start
const customLimiter = createRateLimiter({
limiter: slidingWindow(50, "10 s"),
prefix: "custom:api"
});
// highlight-end
// Fixed window - 1000 requests per hour
const hourlyLimiter = createRateLimiter({
limiter: fixedWindow(1000, "1 h"),
prefix: "hourly"
});
// Token bucket - 100 requests with 10 per second refill
const burstLimiter = createRateLimiter({
limiter: tokenBucket(100, "10 s"),
prefix: "burst"
});Rate Limit Strategies
Strategy Selection
- Sliding Window: Best for smooth rate limiting, prevents burst attacks
- Fixed Window: Simpler, allows bursts at window boundaries
- Token Bucket: Allows controlled bursts while limiting sustained load :::
| Strategy | Use Case | Pros | Cons |
|---|---|---|---|
| Sliding Window | API endpoints, general protection | Smooth, no burst loopholes | Slightly more complex |
| Fixed Window | Simple rate limits, reporting periods | Simple, low memory | Allows bursts at edges |
| Token Bucket | APIs with burst tolerance | Flexible, handles bursts | More configuration needed |
Rate Limit Utilities
import { applyRateLimit, isRateLimited, getRateLimitInfo } from "@repo/security/server/next";
// Check if blocked (doesn't consume tokens)
const isBlocked = await isRateLimited(identifier, "api");
if (isBlocked) {
return new Response("Rate limited", { status: 429 });
}
// Get info without applying limit
// highlight-start
const info = await getRateLimitInfo(identifier, "api");
console.log(`Remaining: ${info.remaining}/${info.limit}`);
// highlight-end
// Apply limit and get full result
const result = await applyRateLimit(identifier, "api");Progressive Rate Limiting
import { createRateLimiter, slidingWindow } from "@repo/security/server/next";
// Different limits per tier
// highlight-start
const limiters = {
free: createRateLimiter({
limiter: slidingWindow(10, "1 m"),
prefix: "free"
}),
pro: createRateLimiter({
limiter: slidingWindow(100, "1 m"),
prefix: "pro"
}),
enterprise: createRateLimiter({
limiter: slidingWindow(1000, "1 m"),
prefix: "enterprise"
})
};
// highlight-end
// Apply based on user tier
const limiter = limiters[user.tier];
const result = await limiter.limit(user.id);Authenticated vs Anonymous
import { applyRateLimit } from "@repo/security/server/next";
export async function POST(request: Request) {
const session = await getSession(request);
// Different rate limits for authenticated vs anonymous
// highlight-start
const identifier = session?.userId
? `user:${session.userId}`
: `ip:${request.headers.get("x-forwarded-for") ?? "unknown"}`;
const limiterType = session?.userId ? "api" : "auth";
// highlight-end
const result = await applyRateLimit(identifier, limiterType);
if (!result.success) {
return new Response("Rate limit exceeded", { status: 429 });
}
// Process request
}Security Headers
Default Configuration
// middleware.ts
import { noseconeMiddleware, noseconeOptions } from "@repo/security/middleware";
// Use default production-ready headers
// highlight-next-line
export default noseconeMiddleware(noseconeOptions);Default headers include:
- Content-Security-Policy
- Strict-Transport-Security
- X-Frame-Options: DENY
- X-Content-Type-Options: nosniff
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy
Custom Configuration
// middleware.ts
import { noseconeMiddleware } from "@repo/security/middleware";
import type { NoseconeOptions } from "@nosecone/next";
// highlight-start
const customOptions: NoseconeOptions = {
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.example.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
frameSrc: ["'none'"]
}
},
// Strict Transport Security
strictTransportSecurity: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// X-Frame-Options
xFrameOptions: "DENY",
// X-Content-Type-Options
xContentTypeOptions: "nosniff",
// Referrer Policy
referrerPolicy: "strict-origin-when-cross-origin",
// Permissions Policy
permissionsPolicy: {
camera: ["none"],
microphone: ["none"],
geolocation: ["none"],
payment: ["self"]
}
};
// highlight-end
export default noseconeMiddleware(customOptions);CSP for OAuth Providers
:::tip OAuth Integration When using OAuth providers, add their domains to CSP directives.
const oauthCSP: NoseconeOptions = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
// highlight-start
connectSrc: ["'self'", "https://accounts.google.com", "https://github.com", "https://discord.com"],
frameSrc: ["https://accounts.google.com"]
// highlight-end
}
}
};Environment Management
Type-Safe Environment
import { env, safeEnv, isProduction, hasArcjetConfig, hasUpstashConfig } from "@repo/security/keys";
// Access validated variables
console.log(env.ARCJET_KEY); // string | undefined
console.log(env.NODE_ENV); // 'development' | 'test' | 'production'
// Safe access (never throws)
const config = safeEnv();
console.log(config.UPSTASH_REDIS_REST_URL);
// Helper functions
// highlight-start
if (isProduction()) {
// Production-only logic
}
if (hasArcjetConfig()) {
// Arcjet is configured
}
if (hasUpstashConfig()) {
// Redis is configured
}
// highlight-endCustom Logger
import { setLogger } from "@repo/security/keys";
import pino from "pino";
// highlight-start
const logger = pino({
level: "info",
transport: {
target: "pino-pretty"
}
});
setLogger({
warn: (message, context) => logger.warn(context, message),
error: (message, context) => logger.error(context, message)
});
// highlight-endFailure Scenarios
Missing Environment Variables
Environment-Dependent
Development allows requests without configuration. Production requires proper setup.
| Environment | Rate Limiting | Bot Detection |
|---|---|---|
| Development | No-op limiter, logs warning | Skips validation, allows all |
| Production | Throws error | Throws error |
Redis Connection Failures
| Environment | Behavior |
|---|---|
| Development | Allows requests, logs error |
| Production | Denies requests (fail closed) |
Arcjet API Failures
| Environment | Behavior |
|---|---|
| Development | Allows requests, logs error |
| Production | Denies requests (fail closed) |
Rate Limit Identifier Validation
// ✅ Valid identifiers
await applyRateLimit("user-123", "api");
await applyRateLimit("192.168.1.1", "api");
await applyRateLimit("api:v1:users", "api");
// ❌ Invalid identifiers
await applyRateLimit("user@example.com", "api"); // Contains @
await applyRateLimit("user/123", "api"); // Contains /
await applyRateLimit("a".repeat(256), "api"); // Too long (>255 chars)Valid characters: Alphanumeric, hyphens, underscores, colons, dots Max length: 255 characters
Architecture
Design Principles
- Graceful Degradation - Works without configuration in development
- Fail Closed - Denies requests on errors in production
- Framework Agnostic - Core logic works with any framework
- Type Safe - Full TypeScript support
- Observable - Structured logging for monitoring
Module Organization
@repo/security/
├── src/
│ ├── server.ts # Generic server exports
│ ├── server-next.ts # Next.js server (Arcjet, rate limiting)
│ ├── client.ts # Generic client exports
│ └── client-next.ts # Next.js client exports
├── keys.ts # Environment variable validation
└── __tests__/ # Test suitesCommon Patterns
Combined Protection
import { secure, applyRateLimit } from "@repo/security/server/next";
export async function POST(request: Request) {
// 1. Bot detection
// highlight-start
await secure(["GOOGLEBOT", "BINGBOT"], request);
// highlight-end
// 2. Rate limiting
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
const result = await applyRateLimit(ip, "api");
if (!result.success) {
return new Response("Too many requests", { status: 429 });
}
// 3. Process request
return Response.json({ success: true });
}Dynamic Rate Limits
import { createRateLimiter, slidingWindow } from "@repo/security/server/next";
// Adjust limits based on time of day
// highlight-start
const peakHours = new Date().getHours() >= 9 && new Date().getHours() <= 17;
const limit = peakHours ? 50 : 100;
const limiter = createRateLimiter({
limiter: slidingWindow(limit, "1 m"),
prefix: "dynamic"
});
// highlight-endTesting
Test Configuration
# Run tests
pnpm --filter @repo/security test
# With coverage
pnpm --filter @repo/security test:coverage
# Watch mode
pnpm --filter @repo/security test:watchMock Security in Tests
// Set test environment
process.env.NODE_ENV = "test";
// Security functions will gracefully degrade
import { secure, applyRateLimit } from "@repo/security/server/next";
// Won't throw in test environment
await secure([], request);
await applyRateLimit("test-id", "api");Troubleshooting
Rate Limit Not Working
Check that Redis is configured and accessible:
import { hasUpstashConfig } from "@repo/security/keys";
if (!hasUpstashConfig()) {
console.error("Upstash Redis not configured");
}Bot Detection Not Blocking
Verify Arcjet configuration:
import { hasArcjetConfig } from "@repo/security/keys";
if (!hasArcjetConfig()) {
console.error("Arcjet not configured");
}CSP Blocking Resources
Check browser console for CSP violations and add domains to whitelist:
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", "https://trusted-cdn.com"],
},
}Related Packages
- @repo/auth - Authentication and authorization
- @repo/auth-mobile - Mobile authentication
- @repo/db-upstash-redis - Redis client
External Resources
- Arcjet Docs - Bot detection and rate limiting
- Nosecone Docs - Security headers
- Upstash Redis Docs - Redis documentation
- OWASP Security Headers - Security header reference