Security
Security boundaries, authentication, authorization, and data protection in Reqcore. Covers tenant isolation, input validation, document security, and headers.
Security
Reqcore enforces security at multiple layers: authentication, tenant isolation, input validation, document access, and HTTP headers.
Security Boundaries
| Boundary | Enforcement |
|---|---|
| Authentication | requireAuth(event) — throws 401 if no session |
| Organization membership | Better Auth org plugin — users can only access orgs they belong to |
| Tenant data isolation | Every query includes eq(table.organizationId, orgId) |
| Input validation | Zod schemas via readValidatedBody / getValidatedQuery |
| Rate limiting | createRateLimiter() on public endpoints (sliding window by IP) |
| Document access | Server-proxied streaming — no presigned URLs |
| Security headers | Global Nitro route rules |
Authentication
Reqcore uses Better Auth for session-based authentication:
- Sessions are stored server-side and identified by an HTTP-only cookie
- Auth routes are handled by a catch-all at
server/api/auth/[...all].ts - Client-side auth is managed via
app/utils/auth-client.ts
Route Protection
// Server: Protect an API route
export default defineEventHandler(async (event) => {
const { session, user } = await requireAuth(event)
const orgId = session.activeOrganizationId
// All queries scoped to orgId
})
// Client: Redirect unauthenticated users
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
// Redirects to /auth/sign-in if not authenticated
})
Multi-Tenant Isolation
The #1 security invariant: The organization ID is always derived from the authenticated session — never from user input (request body, query parameters, or URL params).
Request → Auth Guard → Extract orgId from session → Scope all queries by orgId
This prevents:
- Cross-tenant data access
- Escalation attacks via URL manipulation
- Data leakage between organizations
Input Validation
All API endpoints validate input using Zod schemas:
// Server: Validate request body
const body = await readValidatedBody(event, createJobSchema.parse)
// Server: Validate query parameters
const query = await getValidatedQuery(event, listJobsSchema.parse)
Validation schemas are defined in server/utils/schemas/ and shared between validation and TypeScript type inference.
Document Security
| Measure | Implementation |
|---|---|
| Private S3 bucket | Policy enforced on every startup — deletes any public access rules |
| Server-proxied access | Download and preview endpoints stream from S3; no presigned URLs exposed |
| MIME validation | Magic byte inspection (not just Content-Type header) |
| Filename sanitization | sanitizeFilename() strips path traversal, XSS payloads, unsafe characters |
| Storage key hidden | storageKey (S3 object key) is filtered from all API responses |
| Document limits | Max 20 documents per candidate on public endpoints |
| PDF-only preview | Only application/pdf can be previewed inline |
| Cache headers | Cache-Control: private, no-store on all document endpoints |
Rate Limiting
Public endpoints are protected by IP-based sliding window rate limiting:
- Implementation:
server/utils/rateLimit.ts - Scope: Applied to public application submission and authentication endpoints
- Algorithm: Sliding window per IP address
- Storage: In-memory (single instance)
HTTP Security Headers
Global security headers are applied via Nitro route rules:
// Applied to all routes
{
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'X-XSS-Protection': '1; mode=block',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
}
Exception: Document preview endpoints use X-Frame-Options: SAMEORIGIN to allow inline PDF rendering in the application.
Search Engine Isolation
Private pages are blocked from search engine crawling:
- Dashboard, auth, onboarding, and API routes are disallowed in
robots.txt - Private pages include
noindex, nofollowmeta tags viauseSeoMeta()
Recommendations for Self-Hosting
- Use HTTPS — Put a reverse proxy (Nginx, Caddy) with TLS in front of the application
- Change all default passwords — Run
setup.shto generate secrets - Keep Docker ports local — Ports are bound to
127.0.0.1by default; don't expose them - Back up your data — Schedule
pg_dumpfor PostgreSQL andmc mirrorfor S3 - Keep up to date — Pull the latest code and run migrations regularly
Next Steps
- Architecture Overview — System design overview
- Data Model — Database schema and relationships
- Environment Variables — Configuration reference