Database
Advanced Database Features
Advanced Database Features
Explore advanced Convex features and sophisticated database patterns available in Solo Kit. This guide covers validators, complex queries, transactions, real-time subscriptions, and enterprise-grade database techniques.
Advanced Overview
Solo Kit Advanced Architecture
Solo Kit leverages enterprise-grade Convex features including:
- Type-safe Validators: Comprehensive input validation
- Real-time Subscriptions: Automatic reactive data updates
- Internal Functions: Secure server-only operations
- Scheduled Functions: Cron jobs and delayed execution
- File Storage: Integrated file handling
- Complex Query Patterns: Aggregations, joins, and analytics
Advanced Feature Categories
Schema-Level Features (Validators, Indexes, Types)
↓
Query-Level Features (Complex Queries, Joins, Aggregations)
↓
Real-time Features (Subscriptions, Optimistic Updates)
↓
Server Features (Internal Functions, Cron Jobs, Actions)Advanced Validators
Type-Safe Validation
Solo Kit uses Convex validators for type safety and data integrity:
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
users: defineTable({
// String validation
email: v.string(),
name: v.string(),
// Union types (enums)
role: v.union(v.literal('admin'), v.literal('user')),
status: v.union(v.literal('active'), v.literal('inactive'), v.literal('pending')),
// Optional fields
bio: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
// Numeric validation
loginCount: v.optional(v.number()),
// Boolean validation
emailVerified: v.optional(v.boolean()),
// Timestamps (numbers in milliseconds)
createdAt: v.number(),
updatedAt: v.number(),
lastLoginAt: v.optional(v.number()),
})
.index('by_email', ['email'])
.index('by_role', ['role'])
.index('by_status', ['status']),
});Complex Validators
// convex/validators.ts
import { v } from 'convex/values';
// Reusable validator definitions
export const userRoleValidator = v.union(v.literal('admin'), v.literal('user'));
export const subscriptionStatusValidator = v.union(
v.literal('active'),
v.literal('inactive'),
v.literal('canceled'),
v.literal('past_due')
);
// Nested object validators
export const addressValidator = v.object({
street: v.string(),
city: v.string(),
state: v.string(),
zipCode: v.string(),
country: v.optional(v.string()),
});
// Array validators
export const tagsValidator = v.array(v.string());
// Complex nested structures
export const userPreferencesValidator = v.object({
theme: v.union(v.literal('light'), v.literal('dark'), v.literal('system')),
notifications: v.object({
email: v.boolean(),
push: v.boolean(),
sms: v.optional(v.boolean()),
}),
language: v.optional(v.string()),
});Using validators in functions:
// convex/users.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
import { userRoleValidator, userPreferencesValidator } from './validators';
export const createUser = mutation({
args: {
email: v.string(),
name: v.string(),
role: userRoleValidator,
preferences: v.optional(userPreferencesValidator),
},
handler: async (ctx, args) => {
// All inputs are validated before reaching the handler
return await ctx.db.insert('users', {
...args,
createdAt: Date.now(),
updatedAt: Date.now(),
});
},
});Complex Query Patterns
Advanced Data Relationships
// convex/dashboard.ts
import { v } from 'convex/values';
import { query } from './_generated/server';
// Complex join with aggregations
export const getUserDashboardData = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
// Fetch user and all related data in parallel
const [user, preferences, subscriptions, transactions, posts] = await Promise.all([
ctx.db.get(args.userId),
ctx.db
.query('userPreferences')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.first(),
ctx.db
.query('subscriptions')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.collect(),
ctx.db
.query('transactions')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.order('desc')
.take(10),
ctx.db
.query('posts')
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.filter((q) => q.eq(q.field('status'), 'published'))
.order('desc')
.take(5),
]);
if (!user) return null;
// Calculate aggregations
const transactionStats = transactions.reduce(
(acc, t) => ({
total: acc.total + 1,
totalAmount: acc.totalAmount + (t.amount || 0),
}),
{ total: 0, totalAmount: 0 }
);
return {
user,
preferences,
activeSubscription: subscriptions.find((s) => s.status === 'active'),
recentTransactions: transactions,
transactionStats,
recentPosts: posts,
postCount: posts.length,
};
},
});Analytics and Aggregations
// convex/analytics.ts
import { v } from 'convex/values';
import { query } from './_generated/server';
// User activity analytics
export const getUserAnalytics = query({
args: {
startDate: v.number(),
endDate: v.number(),
},
handler: async (ctx, args) => {
const users = await ctx.db.query('users').collect();
// Filter by date range
const usersInRange = users.filter(
(u) => u.createdAt >= args.startDate && u.createdAt <= args.endDate
);
// Group by role
const byRole = usersInRange.reduce(
(acc, user) => {
acc[user.role] = (acc[user.role] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
// Group by day
const byDay = usersInRange.reduce(
(acc, user) => {
const day = new Date(user.createdAt).toISOString().split('T')[0];
acc[day] = (acc[day] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
return {
totalUsers: usersInRange.length,
byRole,
byDay,
averagePerDay: usersInRange.length / Object.keys(byDay).length || 0,
};
},
});
// Transaction analytics
export const getTransactionAnalytics = query({
args: {
userId: v.optional(v.id('users')),
startDate: v.number(),
endDate: v.number(),
},
handler: async (ctx, args) => {
let transactions = await ctx.db.query('transactions').collect();
// Filter by user if specified
if (args.userId) {
transactions = transactions.filter((t) => t.userId === args.userId);
}
// Filter by date range
transactions = transactions.filter(
(t) => t.createdAt >= args.startDate && t.createdAt <= args.endDate
);
// Calculate stats
const stats = {
count: transactions.length,
totalAmount: transactions.reduce((sum, t) => sum + (t.amount || 0), 0),
averageAmount:
transactions.length > 0
? transactions.reduce((sum, t) => sum + (t.amount || 0), 0) / transactions.length
: 0,
maxAmount: Math.max(...transactions.map((t) => t.amount || 0), 0),
minAmount: Math.min(...transactions.map((t) => t.amount || 0), 0),
};
// Group by status
const byStatus = transactions.reduce(
(acc, t) => {
acc[t.status] = (acc[t.status] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
return { stats, byStatus };
},
});Real-time Subscriptions
Automatic Real-time Updates
Convex provides automatic real-time subscriptions:
// React component with real-time data
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function UserDashboard({ userId }: { userId: string }) {
// Automatically updates when data changes
const dashboard = useQuery(api.dashboard.getUserDashboardData, { userId });
const updateProfile = useMutation(api.users.updateProfile);
if (!dashboard) return <Loading />;
return (
<div>
<h1>Welcome, {dashboard.user.name}</h1>
{/* These values update automatically when database changes */}
<p>Posts: {dashboard.postCount}</p>
<p>Transactions: {dashboard.transactionStats.total}</p>
<button
onClick={() =>
updateProfile({
userId,
updates: { name: 'New Name' },
})
}
>
Update Profile
</button>
</div>
);
}Optimistic Updates
// Optimistic updates for better UX
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function PostList({ posts }: { posts: Post[] }) {
const createPost = useMutation(api.posts.create);
const handleCreatePost = async () => {
// Optimistic update happens automatically
// UI updates immediately, then syncs with server
await createPost({
title: 'New Post',
content: 'Content here...',
});
};
return (
<div>
{posts.map((post) => (
<div key={post._id}>{post.title}</div>
))}
<button onClick={handleCreatePost}>Create Post</button>
</div>
);
}Internal Functions
Secure Server-Only Operations
// convex/internal.ts
import { v } from 'convex/values';
import { internalMutation, internalQuery } from './_generated/server';
// These functions can only be called from other Convex functions
// Never exposed to the client
export const adminDeleteUser = internalMutation({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
// Delete all related data
const relatedTables = ['userPreferences', 'sessions', 'subscriptions', 'transactions'];
for (const table of relatedTables) {
const records = await ctx.db
.query(table as any)
.withIndex('by_userId', (q) => q.eq('userId', args.userId))
.collect();
for (const record of records) {
await ctx.db.delete(record._id);
}
}
// Finally delete the user
await ctx.db.delete(args.userId);
return { success: true };
},
});
export const getSystemStats = internalQuery({
handler: async (ctx) => {
const [users, sessions, subscriptions] = await Promise.all([
ctx.db.query('users').collect(),
ctx.db.query('sessions').collect(),
ctx.db.query('subscriptions').collect(),
]);
return {
totalUsers: users.length,
activeSessions: sessions.filter((s) => s.expiresAt > Date.now()).length,
activeSubscriptions: subscriptions.filter((s) => s.status === 'active').length,
};
},
});Calling Internal Functions
// convex/admin.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
import { internal } from './_generated/api';
export const deleteUserAsAdmin = mutation({
args: {
adminId: v.id('users'),
targetUserId: v.id('users'),
},
handler: async (ctx, args) => {
// Verify admin status
const admin = await ctx.db.get(args.adminId);
if (!admin || admin.role !== 'admin') {
throw new Error('Unauthorized: Admin access required');
}
// Call internal function
await ctx.runMutation(internal.internal.adminDeleteUser, {
userId: args.targetUserId,
});
return { success: true };
},
});Scheduled Functions (Cron Jobs)
Setting Up Cron Jobs
// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';
const crons = cronJobs();
// Run every hour
crons.interval('cleanup-expired-sessions', { hours: 1 }, internal.cleanup.expiredSessions);
// Run daily at midnight UTC
crons.daily('daily-analytics', { hourUTC: 0 }, internal.analytics.generateDaily);
// Run weekly on Sunday
crons.weekly('weekly-report', { dayOfWeek: 'sunday', hourUTC: 9 }, internal.reports.generateWeekly);
// Run monthly on the 1st
crons.monthly('monthly-cleanup', { day: 1, hourUTC: 3 }, internal.cleanup.monthlyMaintenance);
export default crons;Scheduled Task Implementations
// convex/cleanup.ts
import { internalMutation } from './_generated/server';
export const expiredSessions = internalMutation({
handler: async (ctx) => {
const now = Date.now();
const expiredSessions = await ctx.db
.query('sessions')
.filter((q) => q.lt(q.field('expiresAt'), now))
.collect();
for (const session of expiredSessions) {
await ctx.db.delete(session._id);
}
console.log(`Cleaned up ${expiredSessions.length} expired sessions`);
return { cleaned: expiredSessions.length };
},
});
export const monthlyMaintenance = internalMutation({
handler: async (ctx) => {
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
// Clean old audit logs
const oldLogs = await ctx.db
.query('auditLogs')
.filter((q) => q.lt(q.field('createdAt'), thirtyDaysAgo))
.collect();
for (const log of oldLogs) {
await ctx.db.delete(log._id);
}
// Clean old metrics
const oldMetrics = await ctx.db
.query('metrics')
.filter((q) => q.lt(q.field('timestamp'), thirtyDaysAgo))
.collect();
for (const metric of oldMetrics) {
await ctx.db.delete(metric._id);
}
console.log(`Monthly cleanup: ${oldLogs.length} logs, ${oldMetrics.length} metrics`);
},
});Actions (External API Calls)
HTTP Actions
// convex/actions.ts
import { v } from 'convex/values';
import { action } from './_generated/server';
import { internal } from './_generated/api';
// Action for external API calls
export const sendEmail = action({
args: {
to: v.string(),
subject: v.string(),
body: v.string(),
},
handler: async (ctx, args) => {
// Make external API call
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email: args.to }] }],
from: { email: 'noreply@example.com' },
subject: args.subject,
content: [{ type: 'text/plain', value: args.body }],
}),
});
if (!response.ok) {
throw new Error(`Email failed: ${response.statusText}`);
}
// Log the action
await ctx.runMutation(internal.audit.logAction, {
action: 'email_sent',
details: { to: args.to, subject: args.subject },
});
return { success: true };
},
});
// Stripe integration action
export const createStripeCustomer = action({
args: {
userId: v.id('users'),
email: v.string(),
},
handler: async (ctx, args) => {
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const customer = await stripe.customers.create({
email: args.email,
metadata: { userId: args.userId },
});
// Update user with Stripe customer ID
await ctx.runMutation(internal.users.setStripeCustomerId, {
userId: args.userId,
stripeCustomerId: customer.id,
});
return { customerId: customer.id };
},
});Advanced Security Patterns
Row-Level Security
// convex/security.ts
import { v } from 'convex/values';
import { query, mutation } from './_generated/server';
// Get current user from auth context
async function getCurrentUser(ctx: any) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query('users')
.withIndex('by_email', (q) => q.eq('email', identity.email))
.first();
}
// Row-level security pattern
export const getUserData = query({
args: { targetUserId: v.id('users') },
handler: async (ctx, args) => {
const currentUser = await getCurrentUser(ctx);
if (!currentUser) {
throw new Error('Authentication required');
}
// Admin can access any user
if (currentUser.role === 'admin') {
return await ctx.db.get(args.targetUserId);
}
// Users can only access their own data
if (currentUser._id === args.targetUserId) {
return await ctx.db.get(args.targetUserId);
}
throw new Error('Access denied');
},
});
// Secure mutation with audit trail
export const updateUserSecure = mutation({
args: {
targetUserId: v.id('users'),
updates: v.object({
name: v.optional(v.string()),
bio: v.optional(v.string()),
}),
},
handler: async (ctx, args) => {
const currentUser = await getCurrentUser(ctx);
if (!currentUser) {
throw new Error('Authentication required');
}
const canUpdate = currentUser.role === 'admin' || currentUser._id === args.targetUserId;
if (!canUpdate) {
throw new Error('Access denied');
}
// Get old values for audit
const oldUser = await ctx.db.get(args.targetUserId);
// Perform update
await ctx.db.patch(args.targetUserId, {
...args.updates,
updatedAt: Date.now(),
});
// Log audit trail
await ctx.db.insert('auditLogs', {
userId: currentUser._id,
action: 'UPDATE',
entity: 'users',
entityId: args.targetUserId,
changes: JSON.stringify({
old: oldUser,
new: args.updates,
}),
createdAt: Date.now(),
});
return { success: true };
},
});Performance Optimization
Efficient Pagination
// convex/pagination.ts
import { v } from 'convex/values';
import { query } from './_generated/server';
export const getPaginatedUsers = query({
args: {
cursor: v.optional(v.id('users')),
limit: v.optional(v.number()),
direction: v.optional(v.union(v.literal('forward'), v.literal('backward'))),
},
handler: async (ctx, args) => {
const limit = args.limit ?? 20;
const direction = args.direction ?? 'forward';
let query = ctx.db.query('users').order(direction === 'forward' ? 'desc' : 'asc');
if (args.cursor) {
const cursorDoc = await ctx.db.get(args.cursor);
if (cursorDoc) {
query = query.filter((q) =>
direction === 'forward'
? q.lt(q.field('_creationTime'), cursorDoc._creationTime)
: q.gt(q.field('_creationTime'), cursorDoc._creationTime)
);
}
}
const results = await query.take(limit + 1);
const hasMore = results.length > limit;
const users = hasMore ? results.slice(0, -1) : results;
return {
users,
nextCursor: hasMore ? users[users.length - 1]._id : null,
hasMore,
};
},
});Batch Operations
// convex/batch.ts
import { v } from 'convex/values';
import { mutation } from './_generated/server';
export const batchUpdateUsers = mutation({
args: {
updates: v.array(
v.object({
id: v.id('users'),
data: v.object({
name: v.optional(v.string()),
bio: v.optional(v.string()),
}),
})
),
},
handler: async (ctx, args) => {
const results = [];
for (const update of args.updates) {
await ctx.db.patch(update.id, {
...update.data,
updatedAt: Date.now(),
});
results.push({ id: update.id, success: true });
}
return results;
},
});Next Steps
You've completed the comprehensive database documentation! Here are your next areas to explore:
- Authentication - Implement secure authentication
- API Development - Build robust APIs
- UI & Components - Create beautiful interfaces
- Deployment - Deploy to production
With Solo Kit's advanced database features, you have the foundation to build sophisticated, scalable, and maintainable applications that leverage the full power of Convex.