CORS Configuration
Cross-Origin Resource Sharing policies for PRIV API endpoints.
Overview
CORS (Cross-Origin Resource Sharing) controls which domains can make requests to PRIV APIs. We use a tiered CORS policy based on endpoint sensitivity.
Public SDK Endpoints
Open CORS for SDK event tracking from any website.
Authenticated Endpoints
Restricted CORS for sensitive operations with credentials.
CORS Tiers
PRIV implements three CORS tiers based on endpoint sensitivity:
Tier 1: Public SDK Endpoints
These endpoints accept requests from any origin to support the analytics SDK:
| Endpoint Pattern | CORS Policy |
|---|---|
/api/v1/analytics/* | Access-Control-Allow-Origin: * |
/api/v1/events/* | Access-Control-Allow-Origin: * |
/api/v1/identify/* | Access-Control-Allow-Origin: * |
// These endpoints work from any website
await fetch('https://api.priv.io/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer pk_live_xxxxx', // Publishable key
},
body: JSON.stringify({ events: [...] }),
})Security notes:
- No credentials (
Access-Control-Allow-Credentials: false) - Rate limited per API key
- Only publishable keys accepted
- Limited to read/write events only
Tier 2: Authenticated Endpoints
Sensitive endpoints require credentials and have restricted origins:
| Endpoint Pattern | CORS Policy |
|---|---|
/api/v1/user/* | Restricted origins + credentials |
/api/v1/billing/* | Restricted origins + credentials |
/api/v1/marketplace/* | Restricted origins + credentials |
/api/v1/admin/* | Restricted origins + credentials |
// Must be called from allowed origins
await fetch('https://api.priv.io/v1/user/profile', {
method: 'GET',
headers: {
'Authorization': 'Bearer sk_live_xxxxx', // Secret key
},
credentials: 'include', // Send cookies
})Security notes:
Access-Control-Allow-Credentials: true- Specific
Access-Control-Allow-Origin(not*) - HttpOnly cookies supported
- Secret keys required
Tier 3: Internal Endpoints
Some endpoints are not exposed via CORS:
| Endpoint Pattern | Access |
|---|---|
/api/internal/* | Server-to-server only |
/api/admin/* | Admin dashboard only |
/api/webhooks/* | Webhook callbacks only |
Configuration
Setting Allowed Origins
Configure allowed origins for your application:
# .env
ALLOWED_ORIGINS=https://app.priv.io,https://priv.io,https://staging.priv.ioNext.js Implementation
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || []
// Public SDK endpoints - allow all origins
const PUBLIC_ENDPOINTS = [
'/api/v1/analytics',
'/api/v1/events',
'/api/v1/identify',
]
// Endpoints that require credentials
const AUTHENTICATED_ENDPOINTS = [
'/api/v1/user',
'/api/v1/billing',
'/api/v1/marketplace',
]
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin')
const pathname = request.nextUrl.pathname
// Handle preflight requests
if (request.method === 'OPTIONS') {
return handlePreflight(request, origin, pathname)
}
// Add CORS headers to response
const response = NextResponse.next()
addCorsHeaders(response, origin, pathname)
return response
}
function handlePreflight(
request: NextRequest,
origin: string | null,
pathname: string
): NextResponse {
const response = new NextResponse(null, { status: 204 })
if (isPublicEndpoint(pathname)) {
// Public endpoints - allow all origins
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
response.headers.set(
'Access-Control-Allow-Headers',
'Content-Type, Authorization'
)
response.headers.set('Access-Control-Max-Age', '86400')
} else if (isAuthenticatedEndpoint(pathname) && isAllowedOrigin(origin)) {
// Authenticated endpoints - restricted origins
response.headers.set('Access-Control-Allow-Origin', origin!)
response.headers.set(
'Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS'
)
response.headers.set(
'Access-Control-Allow-Headers',
'Content-Type, Authorization'
)
response.headers.set('Access-Control-Allow-Credentials', 'true')
response.headers.set('Access-Control-Max-Age', '86400')
}
return response
}
function addCorsHeaders(
response: NextResponse,
origin: string | null,
pathname: string
): void {
if (isPublicEndpoint(pathname)) {
response.headers.set('Access-Control-Allow-Origin', '*')
} else if (isAuthenticatedEndpoint(pathname) && isAllowedOrigin(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin!)
response.headers.set('Access-Control-Allow-Credentials', 'true')
}
}
function isPublicEndpoint(pathname: string): boolean {
return PUBLIC_ENDPOINTS.some(ep => pathname.startsWith(ep))
}
function isAuthenticatedEndpoint(pathname: string): boolean {
return AUTHENTICATED_ENDPOINTS.some(ep => pathname.startsWith(ep))
}
function isAllowedOrigin(origin: string | null): boolean {
if (!origin) return false
return ALLOWED_ORIGINS.includes(origin)
}
export const config = {
matcher: '/api/:path*',
}CORS Headers Reference
Request Headers
| Header | Description |
|---|---|
Origin | The origin making the request |
Access-Control-Request-Method | Preflight: requested method |
Access-Control-Request-Headers | Preflight: requested headers |
Response Headers
| Header | Description |
|---|---|
Access-Control-Allow-Origin | Allowed origin(s) |
Access-Control-Allow-Methods | Allowed HTTP methods |
Access-Control-Allow-Headers | Allowed request headers |
Access-Control-Allow-Credentials | Whether to include credentials |
Access-Control-Expose-Headers | Headers accessible to JavaScript |
Access-Control-Max-Age | Preflight cache duration |
Common Patterns
Pattern 1: SDK Integration (Public)
// Works from any website
const response = await fetch('https://api.priv.io/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer pk_live_xxxxx',
},
// NO credentials - this is important
body: JSON.stringify({
events: [{ type: 'page_view', timestamp: new Date().toISOString() }],
}),
})Pattern 2: Dashboard API (Authenticated)
// Only works from allowed origins
const response = await fetch('https://api.priv.io/v1/user/profile', {
method: 'GET',
headers: {
'Authorization': 'Bearer sk_live_xxxxx',
},
credentials: 'include', // Include cookies for session
})Pattern 3: Server-to-Server (No CORS)
// Server-side only - CORS doesn't apply
// Use from API routes, Edge Functions, etc.
const response = await fetch('https://api.priv.io/v1/internal/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PRIV_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ operations: [...] }),
})Security Considerations
Never Combine * with Credentials
// DANGEROUS - Never do this!
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Credentials', 'true')
// This is actually blocked by browsers, but the intention is still wrongValidate Origin Header
// Don't trust the Origin header blindly
function validateOrigin(origin: string | null): boolean {
if (!origin) return false
// Check against whitelist
if (!ALLOWED_ORIGINS.includes(origin)) {
return false
}
// Additional validation: must be HTTPS in production
if (process.env.NODE_ENV === 'production') {
try {
const url = new URL(origin)
if (url.protocol !== 'https:') {
return false
}
} catch {
return false
}
}
return true
}Limit Exposed Headers
// Only expose headers that are necessary
response.headers.set(
'Access-Control-Expose-Headers',
'X-Request-Id, X-RateLimit-Remaining'
)
// Don't expose sensitive headers like:
// - X-Internal-User-Id
// - X-Debug-Info
// - etc.Troubleshooting
Common CORS Errors
Error: "No 'Access-Control-Allow-Origin' header"
Access to fetch at 'https://api.priv.io/v1/events' from origin
'https://example.com' has been blocked by CORS policyCauses:
- Origin not in allowed list
- Preflight request failed
- Server error before CORS headers added
Solutions:
- Add your domain to allowed origins in dashboard
- Check for server errors in API logs
- Verify preflight response has correct headers
Error: "Credentials not supported for wildcard origin"
The value of the 'Access-Control-Allow-Origin' header must not be
the wildcard '*' when the request's credentials mode is 'include'Cause: Trying to send credentials to a public endpoint
Solution: Remove credentials: 'include' from SDK requests:
// Wrong
fetch('https://api.priv.io/v1/events', {
credentials: 'include', // Remove this
})
// Correct
fetch('https://api.priv.io/v1/events', {
// No credentials for public endpoints
})Error: "Header not allowed by Access-Control-Allow-Headers"
Request header field X-Custom-Header is not allowed by
Access-Control-Allow-HeadersCause: Sending a header not in the allowed list
Solution: Only send allowed headers, or add custom header to allowlist:
// Standard allowed headers
const ALLOWED_HEADERS = [
'Content-Type',
'Authorization',
'X-Request-Id',
]
response.headers.set(
'Access-Control-Allow-Headers',
ALLOWED_HEADERS.join(', ')
)Testing CORS
Browser DevTools
- Open DevTools (F12)
- Go to Network tab
- Make a request
- Check Response Headers for CORS headers
curl Testing
# Test preflight request
curl -X OPTIONS https://api.priv.io/v1/events \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v
# Test actual request
curl -X POST https://api.priv.io/v1/events \
-H "Origin: https://example.com" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer pk_live_xxxxx" \
-d '{"events":[]}' \
-vAutomated Testing
import { describe, it, expect } from 'vitest'
describe('CORS Configuration', () => {
it('should allow any origin for public endpoints', async () => {
const response = await fetch('https://api.priv.io/v1/events', {
method: 'OPTIONS',
headers: {
'Origin': 'https://random-site.com',
'Access-Control-Request-Method': 'POST',
},
})
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*')
})
it('should restrict authenticated endpoints', async () => {
const response = await fetch('https://api.priv.io/v1/user/profile', {
method: 'OPTIONS',
headers: {
'Origin': 'https://unauthorized-site.com',
'Access-Control-Request-Method': 'GET',
},
})
expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull()
})
it('should allow whitelisted origins for authenticated endpoints', async () => {
const response = await fetch('https://api.priv.io/v1/user/profile', {
method: 'OPTIONS',
headers: {
'Origin': 'https://app.priv.io',
'Access-Control-Request-Method': 'GET',
},
})
expect(response.headers.get('Access-Control-Allow-Origin')).toBe(
'https://app.priv.io'
)
expect(response.headers.get('Access-Control-Allow-Credentials')).toBe(
'true'
)
})
})