Overview
Built-in protection layers for production applications
Security works through standardized helpers in the @workspace/security
package. Using these helpers ensures consistent protection across all endpoints without manual header configuration.
Pro Tip
All security helpers (secureUserJson
, securePublicJson
, secureErrorJson
) automatically apply correct cache controls, security headers, and Vary directives.
Protection Architecture
The security system operates across three layers:
Browser Protection
Content Security Policy (CSP) enforced via middleware:
- Marketing app: Static CSP with
'unsafe-inline'
for simplicity - Dashboard app: Per-request nonce with
'strict-dynamic'
for maximum security
Every HTML response receives security headers that block inline scripts and restrict resource loading to approved origins.
Network Security
HTTPS enforcement with HSTS in production. BetterAuth manages secure, httpOnly cookies:
- Production:
__Host-session
prefix (requires HTTPS + no domain attribute) - Development:
session
cookie (allows HTTP localhost)
All authentication tokens remain httpOnly and secure, preventing client-side access.
Application Layer
Zod validation at all boundaries. Convex rate limiting with durable storage. Cache controls prevent user data leaks:
- User data:
Cache-Control: private, no-store
- Public data:
Cache-Control: public, max-age=60
- API routes: Automatic
Vary
headers for auth-bearing requests
Default Security Benefits
XSS Protection
Cross-site scripting gets blocked by Content Security Policy with nonce-based script execution
Cache Isolation
Data leaks between users cannot occur due to automatic cache header configuration
Rate Limiting
API abuse prevention without manual implementation through Convex durable limits
Input Validation
Schema enforcement happens consistently through Zod at all API boundaries
Every endpoint receives appropriate security headers without developer configuration. User data stays private through proper caching directives. Public content benefits from CDN distribution while maintaining security.
Automatic Protection
Error responses sanitize internal information automatically. The framework handles all header complexity so developers focus on business logic.
Developer Experience
Security happens transparently during development:
import { secureUserJson } from "@workspace/security";
export async function GET(request: Request) {
const user = await getCurrentUser();
// Automatically adds: Cache-Control: private, no-store
// Automatically adds: Vary: Cookie, Authorization
return secureUserJson({ user });
}
The @workspace/security
package exports functions that handle complex header logic:
Rate limiting requires a single function call in Convex actions:
import { userAction } from "./_helpers/builders";
import { assertLimitAction } from "./rateLimits";
export const updateProfile = userAction({
args: { name: v.string() },
handler: async (ctx, { name }) => {
// ctx.viewerId is auto-injected from userAction builder
// 10 updates per 60 seconds
await assertLimitAction(ctx, {
scope: "profile-update",
viewerId: ctx.viewerId,
max: 10,
windowMs: 60_000,
});
// Business logic here
await ctx.runMutation(api.users._updateProfile, { name });
},
});
CSRF Protection
All state-changing operations (POST/PUT/PATCH/DELETE) enforce same-origin checks:
Convex HTTP Routes
CSRF protection happens automatically in convex/http.ts
:
// All mutating methods wrapped with CSRF guard
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
http.route = ((spec) => {
if (MUTATING_METHODS.has(spec.method)) {
const originalHandler = spec.handler;
return originalRoute({
...spec,
handler: httpAction(async (ctx, request) => {
const violation = enforceHttpMutationGuards(request, csrfAllowlist);
if (violation) return violation; // Returns 403
return originalHandler(ctx, request);
}),
});
}
return originalRoute(spec);
});
Next.js API Routes
API routes in the dashboard app check Origin
, Referer
, and Sec-Fetch-Site
headers. See apps/dashboard/app/api/auth/[...all]/route.ts
for the assertSameOrigin()
implementation.
Implementation Structure
Security implementation follows clear patterns throughout the codebase:
import { buildDynamicCsp } from "~/lib/csp";
const nonce = createNonce();
const csp = buildDynamicCsp({ nonce, reportUri, isProd });
const response = NextResponse.next({ request: { headers: reqHeaders } });
response.headers.set(headerName, csp);
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), browsing-topics=()");
Middleware in apps/marketing/middleware.ts
and apps/dashboard/middleware.ts
handle page-level headers for their respective surfaces.
import { auth } from "@workspace/auth/server";
import { secureUserJson, secureErrorJson } from "@workspace/security";
import type { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
// Check authentication
const session = await auth.api.getSession({ headers: request.headers });
if (!session?.user?.id) {
return secureErrorJson(
{ message: "Unauthorized", code: "UNAUTHENTICATED" },
{ status: 401, userScoped: true }
);
}
// Business logic here
return secureUserJson({ success: true });
}
API routes use helpers from @workspace/security
for consistent response headers.
import { userAction } from "./_helpers/builders";
import { assertLimitAction } from "./rateLimits";
export const createTicket = userAction({
args: { title: v.string(), description: v.string() },
handler: async (ctx, { title, description }) => {
// ctx.viewerId is auto-injected and authenticated
// Rate limit: 5 tickets per hour
await assertLimitAction(ctx, {
scope: "support:create",
viewerId: ctx.viewerId,
max: 5,
windowMs: 3600_000,
});
// Business logic
await ctx.runMutation(api.support._insert, { title, description });
},
});
Rate limiting configuration lives with the endpoint logic. Use userAction
(not mutation
) for rate limiting.
Input validation happens at API boundaries using Zod. Authentication flows through BetterAuth with Convex session management.
Learn More
Security Headers
Complete guide to CSP, HSTS, cache controls, and all HTTP security headers
Rate Limiting
Endpoint protection configuration with Convex durable limits
CORS & CSRF
Cross-origin and cross-site request forgery protection
Security Headers
Complete guide to security headers in middleware and responses