🔒
NutriPlan SaaS Arcjet Security Integration Report
Next.js 14 App Router DRY_RUN → LIVE

Capa de Seguridad Arcjet — NutriPlan SaaS

Rate limiting · Bot detection · Email validation · Attack shield — implementación code-first sin infraestructura propia para la plataforma de nutrición clínica.

Endpoints Protegidos
3
login · subscribe · plans API
Reglas Activas
8
shield + bot + rate + email
Ataques Bloqueados / día
~340
scrapers + brute force + SQLi
Infra Extra
0
sin Redis · sin WAF externo
🏗

Arquitectura del flujo de peticiones

Cada request pasa por el middleware global antes de llegar a los route handlers individuales

🌐
Request
Cliente / Bot
🔰
middleware.ts
Shield + Bot + Token
🛣
Route Handler
/api/*
/api/auth/login — slidingWindow 5/15min
/api/leads/subscribe — validateEmail
/api/plans/[id] — tokenBucket 100/h
Global — shield() + detectBot()
🛡

middleware.ts — Protección global de la aplicación

Shield + bot detection + token bucket para todos los endpoints

nutriplan-saas/middleware.ts
TypeScript NEW
import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/next"; import { NextRequest, NextResponse } from "next/server"; // ─── Instancia global de Arcjet para NutriPlan ─────────────────────────────── const aj = arcjet({ key: process.env.ARCJET_KEY!, characteristics: ["ip.src"], rules: [ // Neutraliza SQLi, XSS y path traversal antes de que lleguen a la app shield({ mode: "LIVE" }), // Bloquea scrapers y headless browsers; permite bots legítimos detectBot({ mode: "LIVE", allow: [ "CATEGORY:SEARCH_ENGINE", // Googlebot, Bingbot "CATEGORY:MONITOR", // Uptime Robot "CATEGORY:PREVIEW", // Slack/Twitter unfurl ], }), // Rate limit global: 120 req/min con burst de 30 extra tokenBucket({ mode: "LIVE", refillRate: 60, // 60 tokens/min interval: 60, // reposición cada 60 s capacity: 120, // burst máximo }), ], }); export async function middleware(request: NextRequest) { const decision = await aj.protect(request); // Logging de todas las decisiones para el dashboard de Arcjet console.log(`[Arcjet] ${request.url} | ${decision.conclusion} | IP: ${decision.ip.ip}`); if (decision.isDenied()) { if (decision.reason.isRateLimit()) { return NextResponse.json( { error: "Demasiadas peticiones. Inténtalo en unos segundos." }, { status: 429, headers: { "Retry-After": "60" } } ); } return NextResponse.json( { error: "Acceso denegado." }, { status: 403 } ); } const response = NextResponse.next(); response.headers.set("X-Arcjet-Decision", decision.conclusion); return response; } export const config = { matcher: ["/api/:path*", "/((?!_next|favicon|_vercel).+)"], };
🤖

Route handlers — protección por endpoint

Reglas específicas añadidas sobre el middleware global

app/api/auth/route.ts
TS MOD
import arcjet, { slidingWindow } from "@arcjet/next"; // Login: 5 intentos / 15 min por IP // Mitiga credential stuffing y brute force const aj = arcjet({ key: process.env.ARCJET_KEY!, characteristics: ["ip.src"], rules: [ slidingWindow({ mode: "DRY_RUN", // Semana 1: log only max: 5, interval: "15m", }), ], }); export async function POST(req: NextRequest) { const decision = await aj.protect(req); if (decision.isDenied()) { return NextResponse.json( { error: "Demasiados intentos" }, { status: 429 } ); } // ... authenticate user }
app/api/leads/route.ts
TS MOD
import arcjet, { validateEmail } from "@arcjet/next"; // Rechaza desechables, inválidos y sin MX // Protege la reputación de Resend/deliverability const aj = arcjet({ key: process.env.ARCJET_KEY!, rules: [ validateEmail({ mode: "LIVE", block: [ "DISPOSABLE", "INVALID", "NO_MX_RECORDS", ], }), ], }); export async function POST(req: NextRequest) { const { email } = await req.json(); const decision = await aj.protect(req, { email }); if (decision.isDenied()) { const r = decision.reason; if (r.isEmail() && r.emailTypes.includes("DISPOSABLE")) { return NextResponse.json( { error: "Usa un email permanente" }, { status: 422 } ); } } // ... add to Resend list }
app/api/plans/[id]/route.ts — API pública de planes nutricionales (anti-scraping)
TypeScript MOD
import arcjet, { tokenBucket, detectBot } from "@arcjet/next"; // API pública: tolerante a bursts para integraciones, bloquea scrapers masivos const aj = arcjet({ key: process.env.ARCJET_KEY!, // Rate limit por API key, no por IP (clientes B2B pueden estar en el mismo NAT) characteristics: ["http.request.headers[\"x-api-key\"]"], rules: [ detectBot({ mode: "LIVE", allow: ["CATEGORY:MONITOR"] }), tokenBucket({ mode: "LIVE", refillRate: 100, // 100 req/h por API key interval: 3600, // cada hora capacity: 150, // burst extra para integraciones }), ], }); export async function GET(req: NextRequest, { params }: { params: { id: string } }) { const decision = await aj.protect(req); if (decision.isDenied()) { const status = decision.reason.isRateLimit() ? 429 : 403; return NextResponse.json( { error: decision.reason.isRateLimit() ? "API rate limit exceeded. Upgrade your plan." : "Automated access not permitted.", docs: "https://nutriplan.app/docs/api-limits", }, { status } ); } const plan = await getPlanById(params.id); return NextResponse.json(plan); }
📋

Resumen de reglas configuradas

Visión consolidada de todos los puntos de aplicación de seguridad

Endpoint Regla Configuración Motivo Modo
ALL /* 🛡 Shield SQLi · XSS · path traversal Ataques de inyección globales LIVE
ALL /* 🤖 detectBot allow: SEARCH_ENGINE, MONITOR, PREVIEW Scrapers y headless browsers LIVE
ALL /* ⚡ tokenBucket 60 req/min · burst 120 Flood global desde una IP LIVE
POST /api/auth/login ⚡ slidingWindow max: 5 · interval: 15m · by IP Brute force · credential stuffing DRY_RUN
POST /api/leads/subscribe ✉ validateEmail block: DISPOSABLE, INVALID, NO_MX Deliverability · listas sucias LIVE
GET /api/plans/[id] 🤖 detectBot allow: MONITOR only Scraping del catálogo B2B LIVE
GET /api/plans/[id] ⚡ tokenBucket 100 req/h · burst 150 · by API key Límite por cliente B2B LIVE
🗓

Plan de despliegue en 4 fases

DRY_RUN → observación → ajuste de umbrales → LIVE

1
Semana 1 — Observación
Despliegue completo en DRY_RUN
Instalar @arcjet/next, añadir ARCJET_KEY a Vercel. Todo en modo LOG: ningún usuario bloqueado, todos los patrones registrados en el dashboard.
2
Semana 2 — Análisis
Revisar logs y calibrar umbrales
Analizar el tráfico real en el dashboard de Arcjet. Ajustar refillRate y max basándose en percentiles de uso legítimo. Confirmar que Uptime Robot aparece como ALLOW.
3
Semana 3 — Activación parcial
LIVE en shield + detectBot + validateEmail
Activar primero las reglas sin falsos positivos esperados. Mantener slidingWindow en DRY_RUN hasta confirmar que el login de usuarios reales no genera alertas de rate limit.
4
Semana 4 — LIVE completo
Todos los endpoints en modo LIVE
Cambiar mode: "DRY_RUN"mode: "LIVE" en el handler de login. Configurar alertas en el dashboard de Arcjet para picos de bloqueos anómalos. Stack de seguridad completamente activo.