| name | database-best-practices |
| description | Prisma ORM best practices for Shopify apps including multi-tenant data isolation, query optimization, transaction patterns, and migration strategies. Auto-invoked when working with database operations. |
| allowed-tools | ["Read","Edit","Write","Grep","Glob"] |
Database Best Practices Skill
Purpose
Provides best practices and patterns for database operations in Shopify apps using Prisma ORM, focusing on data isolation, query optimization, and safe migrations.
When This Skill Activates
- Working with Prisma schema or queries
- Creating database migrations
- Optimizing database performance
- Implementing multi-tenant data isolation
- Handling transactions
Critical: Multi-Tenant Data Isolation
ALWAYS filter by shopId - This prevents data leaks between shops.
const products = await db.product.findMany({
where: {
shopId: shop.id,
status: "active",
},
});
const products = await db.product.findMany({
where: { status: "active" },
});
Core Patterns
1. Safe Query Pattern
async function getShopProducts(shopId: string) {
return db.product.findMany({
where: { shopId },
select: {
id: true,
title: true,
vendor: true,
},
take: 50,
orderBy: { createdAt: "desc" },
});
}
2. Pagination Pattern
async function getPaginatedProducts(shopId: string, page: number = 1) {
const pageSize = 50;
const skip = (page - 1) * pageSize;
const [products, totalCount] = await Promise.all([
db.product.findMany({
where: { shopId },
skip,
take: pageSize,
orderBy: { createdAt: "desc" },
}),
db.product.count({
where: { shopId },
}),
]);
return {
products,
totalCount,
totalPages: Math.ceil(totalCount / pageSize),
currentPage: page,
};
}
3. Transaction Pattern
await db.$transaction(async (tx) => {
await tx.product.update({
where: { id: productId },
data: { status: "synced" },
});
await tx.auditLog.create({
data: {
shopId: shop.id,
entityType: "product",
entityId: productId,
action: "update",
changesSummary: "Product synced",
},
});
});
4. Upsert Pattern
await db.product.upsert({
where: {
shopId_productId: {
shopId: shop.id,
productId: shopifyProductId,
},
},
create: {
shopId: shop.id,
productId: shopifyProductId,
title: "Product Title",
},
update: {
title: "Updated Title",
updatedAt: new Date(),
},
});
5. Bulk Operations
await db.product.createMany({
data: products.map(p => ({
shopId: shop.id,
productId: p.id,
title: p.title,
})),
skipDuplicates: true,
});
await db.product.updateMany({
where: {
shopId: shop.id,
status: "pending",
},
data: {
status: "processed",
updatedAt: new Date(),
},
});
6. JSON Field Handling
const metadata = { tags: ["summer", "sale"], featured: true };
await db.product.create({
data: {
shopId: shop.id,
title: "Product",
metadata: JSON.stringify(metadata),
},
});
const product = await db.product.findUnique({
where: { id: productId },
});
const metadata = JSON.parse(product.metadata || "{}");
7. Error Handling
try {
await db.product.create({
data: { shopId, productId, title },
});
} catch (error) {
if (error.code === "P2002") {
await db.product.update({
where: { shopId_productId: { shopId, productId } },
data: { title, updatedAt: new Date() },
});
} else {
throw error;
}
}
8. N+1 Query Prevention
const products = await db.product.findMany();
for (const product of products) {
const shop = await db.shop.findUnique({
where: { id: product.shopId },
});
console.log(shop.shopDomain);
}
const products = await db.product.findMany({
include: {
shop: {
select: {
shopDomain: true,
},
},
},
});
Schema Design Patterns
Multi-Tenant Schema
// Base Shop model - required
model Shop {
id String @id @default(cuid())
shopDomain String @unique
accessToken String
installedAt DateTime @default(now())
// All shop-specific data references this
products Product[]
orders Order[]
}
// Shop-specific model
model Product {
id String @id @default(cuid())
shopId String
shop Shop @relation(fields: [shopId], references: [id], onDelete: Cascade)
productId String // Shopify product ID
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([shopId, productId])
@@index([shopId])
}
Common Indexes
// Essential indexes for performance
@@index([shopId]) // Filter by shop
@@index([shopId, status]) // Shop + status filter
@@index([createdAt]) // Time-based queries
@@index([updatedAt]) // Recently updated
@@unique([shopId, externalId]) // Prevent duplicates per shop
Migration Best Practices
1. Safe Migrations
npx prisma migrate dev --name add_vendor_field
npx prisma migrate deploy
2. Adding Fields Safely
// Step 1: Add as optional
model Product {
vendor String? // Optional first
}
// Step 2: Backfill data
// Run a script to populate vendor for existing records
// Step 3: Make required (in next migration)
model Product {
vendor String // Required after backfill
}
3. Data Migration Script
async function backfillVendor() {
const products = await db.product.findMany({
where: { vendor: null },
});
for (const product of products) {
await db.product.update({
where: { id: product.id },
data: {
vendor: extractVendorFromTitle(product.title),
},
});
}
console.log(`Updated ${products.length} products`);
}
Performance Optimization
1. Select Only Needed Fields
const products = await db.product.findMany();
const products = await db.product.findMany({
select: {
id: true,
title: true,
status: true,
},
});
2. Use Appropriate Indexes
// Index frequently queried fields
@@index([shopId, status]) // Composite index for common query
@@index([createdAt]) // Time-based sorting
@@index([vendor]) // Filtering by vendor
3. Batch Operations
const batchSize = 100;
let skip = 0;
while (true) {
const batch = await db.product.findMany({
where: { shopId: shop.id },
skip,
take: batchSize,
});
if (batch.length === 0) break;
await processBatch(batch);
skip += batchSize;
}
Common Prisma Errors
P2002 - Unique Constraint Violation
await db.product.upsert({
where: { shopId_productId: { shopId, productId } },
create: { shopId, productId, title },
update: { title, updatedAt: new Date() },
});
P2025 - Record Not Found
const product = await db.product.findUnique({
where: { id: productId },
});
if (!product) {
throw new Response("Product not found", { status: 404 });
}
Best Practices Checklist
Remember: Proper database practices prevent data leaks, improve performance, and ensure data integrity.