The Resync application uses Drizzle ORM with Cloudflare D1 (production) and SQLite (development) for data storage. All operations are handled through Next.js 15 Server Actions for type safety and performance.
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β users β β templates β β resumes β
βββββββββββββββ€ ββββββββββββββββ€ βββββββββββββββ€
β id (PK) β β id (PK) β β id (PK) β
β stackId β β name β β userId (FK) β
β email β β description β β templateId β
β name β β category β β title β
β avatar β β isPublic β β content* β
β createdAt β β isPremium β β isPublic β
β updatedAt β β createdAt β β isPublished β
βββββββββββββββ ββββββββββββββββ β createdAt β
β updatedAt β
βββββββββββββββ
*content field stores complete resume data as JSON
The content field in resumes stores all resume data as JSON:
interface ResumeContent {
personal: PersonalInfo;
experience: Experience[];
education: Education[];
skills: Skill[];
projects: Project[];
certifications: Certification[];
languages: Language[];
awards: Award[];
customSections: CustomSection[];
}All database operations use Server Actions located in src/lib/actions.ts:
Creates or updates a user from StackAuth data.
const user = await createOrUpdateUser({
id: "stack_user_id",
primaryEmail: "user@example.com",
displayName: "John Doe",
profileImageUrl: "https://avatar.url"
});Retrieves all public templates.
const templates = await getTemplates();
// Returns: Template[]Gets a specific template by ID.
const template = await getTemplateById("modern");
// Returns: Template | nullGets all resumes for the authenticated user.
const resumes = await getUserResumes();
// Returns: Resume[]Gets a specific resume by ID (user must own it).
const resume = await getResumeById("resume_id");
// Returns: Resume | nullCreates a new resume for the authenticated user.
const result = await createResume({
title: "My Resume",
templateId: "modern",
content: { /* resume data */ }
});
// Returns: { success: boolean; resumeId?: string; error?: string }Updates an existing resume.
const result = await updateResume("resume_id", {
title: "Updated Title",
content: { /* updated resume data */ },
isPublic: true
});
// Returns: { success: boolean; error?: string }Deletes a resume (user must own it).
const result = await deleteResume("resume_id");
// Returns: { success: boolean; error?: string }Creates a copy of an existing resume.
const result = await duplicateResume("resume_id");
// Returns: { success: boolean; resumeId?: string; error?: string }Returns all public templates.
Response:
{
"success": true,
"templates": [
{
"id": "modern",
"name": "Modern",
"description": "Clean modern design",
"category": "professional",
"isPublic": true,
"isPremium": false
}
]
}Returns all resumes for authenticated user.
Response:
{
"success": true,
"resumes": [
{
"id": "resume_id",
"title": "My Resume",
"templateId": "modern",
"isPublic": false,
"isPublished": false,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}
]
}Returns specific resume with full content.
Response:
{
"success": true,
"resume": {
"id": "resume_id",
"title": "My Resume",
"templateId": "modern",
"content": {
"personal": {
"full_name": "John Doe",
"email": "john@example.com"
},
"experience": [],
"education": []
}
}
}Creates new resume or updates existing one.
Create Resume:
{
"title": "New Resume",
"templateId": "modern",
"content": { /* resume data */ }
}Update Resume:
{
"id": "resume_id",
"content": { /* updated resume data */ }
}Tests database connection and returns statistics.
Response:
{
"success": true,
"data": {
"counts": {
"users": 5,
"resumes": 12,
"templates": 5
},
"recentUsers": [],
"templates": [],
"timestamp": "2024-01-01T00:00:00.000Z"
}
}All protected endpoints use StackAuth for authentication:
// Check authentication in API routes
const user = await stackServerApp.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}Server Actions automatically handle authentication through the getAuthenticatedUser() helper.
All data is validated using Zod schemas:
const userSchema = z.object({
stackId: z.string().min(1),
email: z.string().email(),
name: z.string().min(1).max(100),
avatar: z.string().url().optional(),
});const resumeSchema = z.object({
title: z.string().min(1).max(100),
templateId: z.string().min(1),
content: resumeDataSchema.optional(),
isPublic: z.boolean().default(false),
isPublished: z.boolean().default(false),
});const resumeDataSchema = z.object({
personal: z.object({
full_name: z.string().min(1).max(100),
email: z.string().email(),
phone: z.string().max(50).optional(),
// ... other fields
}),
experience: z.array(experienceSchema),
education: z.array(educationSchema),
// ... other sections
});For complex queries, you can use the database directly:
import { db, users, resumes } from '@/lib/db';
import { eq, desc, and } from 'drizzle-orm';
// Get user with resume count
const userWithResumeCount = await db
.select({
id: users.id,
name: users.name,
resumeCount: count(resumes.id)
})
.from(users)
.leftJoin(resumes, eq(users.id, resumes.userId))
.where(eq(users.id, userId))
.groupBy(users.id);Database seeding is handled by src/lib/db/seed.ts:
# Seed database
npm run db:seedDefault templates are automatically created:
- Modern
- Professional
- Creative
- Minimal
- Academic
- Database:
./dev.db(SQLite file) - ORM: Drizzle with better-sqlite3
- Migrations: Auto-applied with
db:push
- Database: Cloudflare D1
- ORM: Drizzle with D1 HTTP API
- Migrations: Applied with
db:migrate:prod
All operations include proper error handling:
try {
const result = await createResume(data);
if (!result.success) {
console.error('Resume creation failed:', result.error);
}
} catch (error) {
console.error('Unexpected error:', error);
}Common error responses:
401 Unauthorized- Authentication required400 Bad Request- Invalid data404 Not Found- Resource not found500 Internal Server Error- Server error
- JSON Storage: Single query for complete resume data
- Indexes: Strategic indexing on frequently queried fields
- Server Actions: Reduced client-server round trips
- Type Safety: Compile-time error prevention
- Prepared Statements: Automatic query optimization
// Database connection check
import { isDatabaseAvailable, getDatabaseType } from '@/lib/db';
console.log('Database available:', isDatabaseAvailable());
console.log('Database type:', getDatabaseType());For migrating from other ORMs or databases, see the Migration Guide.