OneApp Docs
PackagesAuth

@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/security

AI 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
.env.local
# 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=xxxxxxxxxx

2. Add security headers middleware

middleware.ts
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

app/api/auth/login/route.ts
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

app/api/data/route.ts
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/security

Build configuration: Uses tsdown with createDistConfig('node', ...) for distribution builds.


Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationpackages/security
DependenciesArcjet, Nosecone, Upstash Redis
Node Required>=22.0.0
Edge SupportYes (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

PathDescription
@repo/security/serverGeneric server-side utilities
@repo/security/server/nextNext.js server integration
@repo/security/clientGeneric client utilities
@repo/security/client/nextNext.js client utilities
@repo/security/keysEnvironment 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); // Category

Block All Bots

// Block all automated traffic
// highlight-next-line
await secure([], request);

Available Bot Types

Individual Bots:

  • GOOGLEBOT - Google search crawler
  • BINGBOT - Bing search crawler
  • DUCKDUCKBOT - DuckDuckGo crawler
  • FACEBOOKBOT - Facebook link preview
  • TWITTERBOT - Twitter card preview
  • LINKEDINBOT - LinkedIn preview
  • SLACKBOT - Slack link preview
  • DISCORDBOT - Discord embed preview

Categories:

  • CRAWLER - All web crawlers
  • PREVIEW - Social media preview bots
  • MONITOR - Monitoring and uptime tools
  • AI - AI training scrapers
  • SEO - SEO analysis tools
  • ARCHIVE - 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 :::
StrategyUse CaseProsCons
Sliding WindowAPI endpoints, general protectionSmooth, no burst loopholesSlightly more complex
Fixed WindowSimple rate limits, reporting periodsSimple, low memoryAllows bursts at edges
Token BucketAPIs with burst toleranceFlexible, handles burstsMore 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-end

Custom 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-end

Failure Scenarios

Missing Environment Variables

Environment-Dependent

Development allows requests without configuration. Production requires proper setup.

EnvironmentRate LimitingBot Detection
DevelopmentNo-op limiter, logs warningSkips validation, allows all
ProductionThrows errorThrows error

Redis Connection Failures

EnvironmentBehavior
DevelopmentAllows requests, logs error
ProductionDenies requests (fail closed)

Arcjet API Failures

EnvironmentBehavior
DevelopmentAllows requests, logs error
ProductionDenies 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

  1. Graceful Degradation - Works without configuration in development
  2. Fail Closed - Denies requests on errors in production
  3. Framework Agnostic - Core logic works with any framework
  4. Type Safe - Full TypeScript support
  5. 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 suites

Common 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-end

Testing

Test Configuration

# Run tests
pnpm --filter @repo/security test

# With coverage
pnpm --filter @repo/security test:coverage

# Watch mode
pnpm --filter @repo/security test:watch

Mock 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"],
  },
}

External Resources

On this page