Follow these security practices to protect your application and user data.
Authentication best practices
Always use Server Components for auth
import { requireAuth } from '@/lib/auth-server'
// ✅ Good - Server Component with server-side auth
export default async function DashboardPage() {
const user = await requireAuth() // Redirects if not authenticated
return <UserDashboard user={user} />
}
// ❌ Bad - Client Component trying to check auth
'use client'
export function DashboardPage() {
const user = useCurrentUser() // Can be bypassed
if (!user) return <LoginPrompt />
return <UserDashboard user={user} />
}
Implement proper logout
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export async function signOutAction() {
// Clear all auth cookies
cookies().delete('devkit4ai-token')
cookies().delete('devkit4ai-refresh-token')
// Redirect to login
redirect('/login')
}
Validate return URLs
export function sanitizeReturnUrl(url: string | null): string | null {
if (!url) return null
// Only allow same-origin relative paths
if (!url.startsWith('/')) return null
if (url.startsWith('//')) return null
// Prevent control characters
if (/[\x00-\x1f]/.test(url)) return null
// Prevent excessively long URLs
if (url.length > 2048) return null
return url
}
API key management
Never expose keys client-side
// ✅ Good - Server Action
'use server'
export async function fetchData() {
const key = process.env.DEVKIT4AI_DEVELOPER_KEY
// Key stays on server
}
// ❌ Bad - Client Component
'use client'
export function MyComponent() {
const key = process.env.DEVKIT4AI_DEVELOPER_KEY // Undefined!
}
Rotate keys regularly
Generate new keys
Create new keys in Cloud Admin every 90 days or immediately if compromised.
Update production first
Set new keys in production environment variables and deploy.
Verify functionality
Test critical flows (login, API calls, AI features).
Revoke old keys
Only after confirming new keys work everywhere.
Monitor key usage
Regularly check Cloud Admin for:
- Unexpected API key usage patterns
- Failed authentication attempts
- Rate limit violations
Validate on client AND server
'use server'
export async function updateProfile(formData: FormData) {
const email = formData.get('email') as string
// Validate even though client did
if (!isValidEmail(email)) {
return { error: 'Invalid email address' }
}
// Proceed with update
}
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
Sanitize user-generated content
components/user-content.tsx
import DOMPurify from 'isomorphic-dompurify'
interface Props {
content: string
}
export function UserContent({ content }: Props) {
// Sanitize before rendering
const clean = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
ALLOWED_ATTR: []
})
return (
<div dangerouslySetInnerHTML={{ __html: clean }} />
)
}
Prevent SQL injection
Your Starter Kit doesn’t directly access databases. The Cloud API handles all data operations securely.
If you add custom backend logic, use parameterized queries.
HTTPS and transport security
Enforce HTTPS in production
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Redirect HTTP to HTTPS
if (
process.env.NODE_ENV === 'production' &&
request.headers.get('x-forwarded-proto') !== 'https'
) {
return NextResponse.redirect(
`https://${request.headers.get('host')}${request.nextUrl.pathname}`,
301
)
}
}
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
}
]
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders
}
]
}
}
export default nextConfig
Error handling
Never expose sensitive info in errors
// ❌ Bad
catch (error) {
return { error: error.message } // Might leak stack traces
}
// ✅ Good
catch (error) {
console.error('API error:', error) // Log for debugging
return { error: 'Unable to process request' } // Generic message
}
Log errors without sensitive data
function logError(error: Error, context: Record<string, any>) {
const safeContext = { ...context }
// Remove sensitive fields
delete safeContext.password
delete safeContext.apiKey
delete safeContext.token
console.error('Error:', error.message, safeContext)
}
Dependency security
Regular updates
# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Update all dependencies
npm update
# Update Next.js
npm install next@latest react@latest react-dom@latest
Monitor dependencies
Enable GitHub Dependabot:
- Repository Settings → Security & Analysis
- Enable Dependabot alerts
- Enable Dependabot security updates
Review before installing
Before adding new packages:
- Check npm/GitHub stats (downloads, stars, last update)
- Review open issues and security advisories
- Check license compatibility
- Consider bundle size impact
Session security
Set appropriate timeouts
Access tokens expire after 30 minutes. Implement refresh logic:
export async function refreshAccessToken() {
const refreshToken = cookies().get('devkit4ai-refresh-token')?.value
if (!refreshToken) {
redirect('/login')
}
const response = await fetch(`${apiUrl}/api/v1/auth/refresh`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${refreshToken}` }
})
if (!response.ok) {
redirect('/login?error=session_expired')
}
const tokens = await response.json()
storeTokensInCookies(tokens)
}
Handle concurrent sessions
Decide your policy:
- Allow multiple sessions (default)
- Invalidate previous sessions on new login
- Limit to N concurrent sessions
Implement in your login flow:
'use server'
export async function loginAction(formData: FormData) {
// Login succeeds
const tokens = await apiLogin(email, password)
// Optional: invalidate other sessions
if (SINGLE_SESSION_MODE) {
await revokeOtherSessions(tokens.user_id)
}
storeTokensInCookies(tokens)
}
Rate limiting
Respect API rate limits
async function callApiWithBackoff(
url: string,
options: RequestInit,
maxRetries = 3
) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options)
if (response.status !== 429) {
return response
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
throw new Error('Rate limit exceeded')
}
Implement client-side throttling
import { useState, useCallback } from 'react'
function useThrottle(callback: Function, delay: number) {
const [lastCall, setLastCall] = useState(0)
return useCallback((...args: any[]) => {
const now = Date.now()
if (now - lastCall >= delay) {
setLastCall(now)
return callback(...args)
}
}, [callback, delay, lastCall])
}
// Usage
const throttledSearch = useThrottle(search, 1000) // Max 1 call/second
Security checklist
Next steps