Database Security
Database Security
Learn how to secure your Solo Kit database against common vulnerabilities and implement enterprise-grade security practices. This guide covers input validation, access control, and comprehensive security monitoring with Convex.
Security Overview
Solo Kit Security Architecture
Solo Kit implements defense-in-depth security with multiple layers:
- Connection Security: Automatic TLS encryption with Convex
- Authentication: BetterAuth with secure session management
- Authorization: Role-based access control (RBAC) with function-level constraints
- Input Validation: TypeScript type safety and Zod validation in Convex functions
- Data Protection: Encryption at rest and in transit (handled by Convex)
- Monitoring: Comprehensive security scanning and audit logging
Security Principles
Zero Trust Database Design:
- Principle of Least Privilege: Minimal required permissions only
- Defense in Depth: Multiple security layers working together
- Secure by Default: Safe configurations out-of-the-box
- Continuous Monitoring: Real-time security health checks
Input Validation & Type Safety
Convex Function Protection
Solo Kit uses Convex functions which provide automatic input validation:
// convex/users.ts
import { v } from 'convex/values';
import { query, mutation } from './_generated/server';
// SAFE: Convex validates all inputs automatically
export const getUserById = query({
args: { id: v.id('users') },
handler: async (ctx, args) => {
// Input is automatically validated by Convex
const user = await ctx.db.get(args.id);
return user;
},
});
// SAFE: All Convex queries use validated arguments
export const getUserByEmail = query({
args: { email: v.string() },
handler: async (ctx, args) => {
// Safe validated query
const user = await ctx.db
.query('users')
.withIndex('by_email', (q) => q.eq('email', args.email))
.first();
return user;
},
});Why Convex is Secure:
- Automatic Validation: All arguments are validated against declared schemas
- Type Safety: TypeScript prevents many injection vectors
- No SQL: NoSQL queries eliminate SQL injection entirely
- Sandboxed Execution: Functions run in isolated environments
Input Validation at Schema Level
Solo Kit implements schema-level validation in Convex:
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
users: defineTable({
email: v.string(),
name: v.string(),
role: v.union(v.literal('admin'), v.literal('user')),
emailVerified: v.optional(v.boolean()),
// ... other fields
})
.index('by_email', ['email'])
.index('by_role', ['role']),
});Multiple validation layers:
// convex/verificationTokens.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
import { z } from 'zod';
// Additional Zod validation for complex patterns
const emailSchema = z.string().email();
export const createVerificationToken = mutation({
args: {
email: v.string(),
type: v.union(v.literal('email_verification'), v.literal('password_reset')),
token: v.string(),
expiresAt: v.number(),
},
handler: async (ctx, args) => {
// Additional email validation
const validatedEmail = emailSchema.parse(args.email);
return await ctx.db.insert('verificationTokens', {
...args,
email: validatedEmail,
createdAt: Date.now(),
});
},
});Authentication & Authorization
Role-Based Access Control
Solo Kit implements comprehensive RBAC with Convex:
// convex/schema.ts
export default defineSchema({
users: defineTable({
email: v.string(),
name: v.string(),
role: v.union(v.literal('admin'), v.literal('user')), // Type-safe roles
// ... other fields
}),
});Role-based query patterns:
// convex/admin.ts
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { getCurrentUser } from './auth';
// Admin-only operations
export const adminOnly = async <T>(ctx: any, operation: () => Promise<T>): Promise<T> => {
const user = await getCurrentUser(ctx);
if (!user || user.role !== 'admin') {
throw new Error('Access denied: Admin privileges required');
}
return await operation();
};
// Usage example
export const deleteAnyUser = mutation({
args: { targetUserId: v.id('users') },
handler: async (ctx, args) => {
return await adminOnly(ctx, async () => {
await ctx.db.delete(args.targetUserId);
return true;
});
},
});Secure Session Management
Solo Kit uses BetterAuth for secure session handling:
// apps/web/lib/auth.ts
export const authOptions: BetterAuthOptions = {
// Session-based authentication (more secure than JWT-only)
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day - extends on activity
freshAge: 60 * 15, // 15 minutes for sensitive operations
storeSessionInDatabase: true, // Sessions are revocable
// Performance optimization with security
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes - short cache for security
},
},
// Enhanced cookie security
advanced: {
useSecureCookies: process.env.NODE_ENV === 'production',
cookies: {
sessionToken: {
name: 'better-auth.session_token',
attributes: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'lax', // CSRF protection
path: '/',
httpOnly: true, // Prevent XSS attacks
},
},
},
},
// Trusted origins for CORS protection
trustedOrigins: [authConfig.baseURL],
};Security features:
- HttpOnly Cookies: Prevents XSS access to session tokens
- Secure Cookies: HTTPS-only in production
- SameSite Protection: CSRF attack prevention
- Database Sessions: Revocable sessions for security
- Session Rotation: Automatic session refresh
Password Security
Argon2 password hashing with legacy support:
// apps/web/lib/utils/password.ts
export async function hashPassword(password: string): Promise<string> {
// Use Argon2 for new passwords (industry standard)
return await hash(password, {
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 parallel threads
hashLength: 32, // 32 bytes output
});
}
export async function verifyLegacyPassword(
password: string,
hash: string
): Promise<{ isValid: boolean; needsRehash: boolean }> {
// Support migration from bcrypt/scrypt
if (hash.startsWith('$argon2')) {
// Already Argon2
const isValid = await verify(hash, password);
return { isValid, needsRehash: false };
} else if (hash.startsWith('$2b$')) {
// Legacy bcrypt - verify and mark for rehash
const isValid = await bcrypt.compare(password, hash);
return { isValid, needsRehash: isValid };
}
// Unknown format
return { isValid: false, needsRehash: false };
}Connection Security
Secure Convex Configuration
Environment variable protection:
# .env.local (never committed)
NEXT_PUBLIC_CONVEX_URL="https://your-project.convex.cloud"
CONVEX_DEPLOY_KEY="your-deploy-key"Convex security features:
- Automatic TLS: All connections encrypted by default
- Edge Deployment: Low-latency secure connections
- Sandboxed Functions: Isolated execution environments
- Automatic Retries: Built-in connection resilience
Convex Client Configuration
Solo Kit's connection architecture includes security features:
// apps/web/lib/convex-client.ts
import { ConvexReactClient } from 'convex/react';
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL!;
export const convex = new ConvexReactClient(convexUrl);
// Server-side Convex client with authentication
// apps/web/lib/convex-server.ts
import { ConvexHttpClient } from 'convex/browser';
export function getConvexClient() {
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!convexUrl) {
throw new Error('NEXT_PUBLIC_CONVEX_URL is not configured');
}
return new ConvexHttpClient(convexUrl);
}Data Protection & Cleanup
Automated Token Cleanup
Solo Kit includes automatic security token cleanup:
// convex/cleanup.ts
import { internalMutation } from './_generated/server';
export const cleanupExpiredTokens = internalMutation({
handler: async (ctx) => {
const now = Date.now();
// Find and delete expired verification tokens
const expiredTokens = await ctx.db
.query('verificationTokens')
.filter((q) => q.lt(q.field('expiresAt'), now))
.collect();
let deletedCount = 0;
for (const token of expiredTokens) {
await ctx.db.delete(token._id);
deletedCount++;
}
if (deletedCount > 0) {
console.info(`Cleaned up ${deletedCount} expired verification tokens`);
}
return deletedCount;
},
});
// User-specific cleanup for security incidents
export const cleanupUserTokens = internalMutation({
args: {
userId: v.id('users'),
tokenType: v.optional(v.union(v.literal('email_verification'), v.literal('password_reset'))),
},
handler: async (ctx, args) => {
let tokensQuery = ctx.db
.query('verificationTokens')
.withIndex('by_userId', (q) => q.eq('userId', args.userId));
const tokens = await tokensQuery.collect();
const filteredTokens = args.tokenType
? tokens.filter((t) => t.type === args.tokenType)
: tokens;
for (const token of filteredTokens) {
await ctx.db.delete(token._id);
}
return filteredTokens.length;
},
});Token Replay Attack Prevention
Used token tracking for security:
// convex/schema.ts
export default defineSchema({
usedTokens: defineTable({
jti: v.string(), // JWT ID from token
tokenHash: v.string(), // Hashed token for security
usedAt: v.number(),
expiresAt: v.number(),
purpose: v.string(), // 'purchase-verification', etc.
email: v.string(), // For audit trail
})
.index('by_jti', ['jti'])
.index('by_expiresAt', ['expiresAt']),
});
// convex/tokens.ts
export const markTokenAsUsed = mutation({
args: {
jti: v.string(),
tokenHash: v.string(),
purpose: v.string(),
email: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert('usedTokens', {
...args,
usedAt: Date.now(),
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
});
},
});
export const isTokenUsed = query({
args: { jti: v.string() },
handler: async (ctx, args) => {
const token = await ctx.db
.query('usedTokens')
.withIndex('by_jti', (q) => q.eq('jti', args.jti))
.first();
return token !== null;
},
});Security Monitoring
Comprehensive Security Scanning
Solo Kit includes automated security scanning:
# Run comprehensive security scan
pnpm security-scanSecurity scan features (scripts/security-scan.ts):
- Dependency vulnerability scanning: pnpm audit integration
- Source code security scanning: Pattern-based vulnerability detection
- Authentication flow analysis: Session and token validation
- CORS and CSP policy validation: Header security checks
- Input sanitization checks: Validation prevention checks
- Rate limiting verification: DoS protection validation
- Security headers validation: HTTPS and security header checks
Database Health Monitoring
Built-in database security health checks:
// API route: app/api/health/database/route.ts
import { getConvexClient } from '@/lib/convex-server';
import { api } from '@/convex/_generated/api';
export async function GET() {
try {
const startTime = Date.now();
const convex = getConvexClient();
// Test basic connectivity
await convex.query(api.health.check);
const responseTime = Date.now() - startTime;
return Response.json({
status: 'healthy',
responseTime: `${responseTime}ms`,
provider: 'convex',
timestamp: new Date().toISOString(),
});
} catch (error) {
return Response.json(
{
status: 'unhealthy',
error: error.message,
provider: 'convex',
},
{ status: 503 }
);
}
}Audit Logging
Security event logging patterns:
// convex/audit.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
export const logSecurityEvent = mutation({
args: {
eventType: v.union(
v.literal('login'),
v.literal('logout'),
v.literal('admin_action'),
v.literal('token_cleanup')
),
userId: v.optional(v.id('users')),
details: v.optional(v.any()),
ipAddress: v.optional(v.string()),
},
handler: async (ctx, args) => {
await ctx.db.insert('auditLogs', {
...args,
timestamp: Date.now(),
});
},
});
// Usage examples in application code
// await logSecurityEvent({ eventType: 'login', userId, details: { method: 'email' } });
// await logSecurityEvent({ eventType: 'admin_action', userId: adminId, details: { action: 'user_delete', targetUserId } });Security Best Practices
1. Environment Security
Secure environment variable management:
# .env.local (never committed)
NEXT_PUBLIC_CONVEX_URL="https://your-project.convex.cloud"
CONVEX_DEPLOY_KEY="your-deploy-key"
# .env (safe defaults, committed)
EMAIL_PROVIDER=console
PAYMENTS_PROVIDER=disabled
# NEVER: Commit real credentials to gitEnvironment validation:
// Validate critical security settings
function validateSecurityEnvironment() {
const errors = [];
if (process.env.NODE_ENV === 'production') {
if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
errors.push('Production must have NEXT_PUBLIC_CONVEX_URL configured');
}
if (!process.env.BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET.length < 32) {
errors.push('Production must have secure auth secret (32+ characters)');
}
}
return errors;
}2. Access Control Patterns
Implement consistent authorization:
// convex/auth.ts
export async function requireRole(ctx: any, allowedRoles: string[]) {
const user = await getCurrentUser(ctx);
if (!user) {
throw new Error('Authentication required');
}
if (!allowedRoles.includes(user.role)) {
throw new Error(`Access denied. Required roles: ${allowedRoles.join(', ')}`);
}
return user;
}
// Usage
export const deleteUser = mutation({
args: { targetUserId: v.id('users') },
handler: async (ctx, args) => {
await requireRole(ctx, ['admin']);
// Admin-only operation
await ctx.db.delete(args.targetUserId);
},
});3. Input Sanitization
Always validate and sanitize:
import { z } from 'zod';
import { v } from 'convex/values';
import { mutation } from './_generated/server';
// Schema validation for security
const userUpdateSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
bio: z.string().max(500).optional(),
});
export const updateUser = mutation({
args: {
userId: v.id('users'),
updates: v.object({
name: v.optional(v.string()),
email: v.optional(v.string()),
bio: v.optional(v.string()),
}),
},
handler: async (ctx, args) => {
// Validate input before database operation
const validatedUpdates = userUpdateSchema.parse(args.updates);
await ctx.db.patch(args.userId, {
...validatedUpdates,
updatedAt: Date.now(),
});
},
});4. Rate Limiting
Implement rate limiting for security:
// convex/rateLimits.ts
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
export const checkRateLimit = query({
args: {
identifier: v.string(), // IP or user ID
action: v.string(), // 'login', 'signup', etc.
maxAttempts: v.optional(v.number()),
windowMinutes: v.optional(v.number()),
},
handler: async (ctx, args) => {
const maxAttempts = args.maxAttempts ?? 5;
const windowMinutes = args.windowMinutes ?? 15;
const windowStart = Date.now() - windowMinutes * 60 * 1000;
// Count recent attempts
const attempts = await ctx.db
.query('rateLimits')
.filter((q) =>
q.and(
q.eq(q.field('identifier'), args.identifier),
q.eq(q.field('action'), args.action),
q.gte(q.field('windowStart'), windowStart)
)
)
.collect();
const currentCount = attempts.length;
return {
allowed: currentCount < maxAttempts,
remaining: Math.max(0, maxAttempts - currentCount),
};
},
});
export const recordRateLimitAttempt = mutation({
args: {
identifier: v.string(),
action: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert('rateLimits', {
...args,
windowStart: Date.now(),
createdAt: Date.now(),
});
},
});Encryption & Data Protection
Data at Rest
Convex encryption features:
- Automatic Encryption: All data encrypted at rest by Convex
- Key Management: Handled by Convex infrastructure
- Compliance: SOC 2 Type II certified
Data in Transit
Connection encryption:
- TLS 1.3: All connections use modern TLS
- Certificate Pinning: Automatic certificate validation
- Edge Security: Global edge network with security features
Application-level Encryption
Encrypt sensitive data before storage:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; // 32-byte key
export function encryptSensitive(data: string): string {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
export function decryptSensitive(encryptedData: string): string {
const [ivHex, encrypted] = encryptedData.split(':');
const iv = Buffer.from(ivHex, 'hex');
const decipher = createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}Next Steps
Strengthen your database security with these additional guides:
- Backup & Restore - Secure data protection
- Monitoring - Security monitoring
- Advanced - Advanced security patterns
- Performance - Secure performance optimization
Implementing comprehensive database security ensures your Solo Kit application protects user data and maintains trust at scale.