Skip to main content

Favorites & Collections Schema Deep Dive

Overview

The Ever Works Template implements a favorites system that serves as the user collection mechanism. There is no separate collections table -- user curation of items is handled through the favorites table, which stores user-saved items with denormalized metadata for efficient display. For admin-curated collections, the featured_items table provides a managed set of highlighted items.

Source file: template/lib/db/schema.ts Relations file: template/lib/db/migrations/relations.ts


Table: favorites

User-created bookmark/collection of items. Each user can save items to their personal favorites list.

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
userIduserIdtextNo-FK -> users.id (CASCADE)
itemSlugitem_slugtextNo-Item identifier
itemNameitem_nametextNo-Denormalized display name
itemIconUrlitem_icon_urltextYes-Denormalized icon URL
itemCategoryitem_categorytextYes-Denormalized category
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-

Indexes

NameColumnsType
user_item_favorite_unique_idx(userId, itemSlug)Unique
favorites_user_id_idxuserIdB-tree
favorites_item_slug_idxitemSlugB-tree
favorites_created_at_idxcreatedAtB-tree

Key Constraints

  • One favorite per user per item: The unique composite index user_item_favorite_unique_idx on (userId, itemSlug) prevents duplicate favorites.
  • Cascade deletion: When a user is deleted, all their favorites are automatically removed.

TypeScript Types

export type Favorite = typeof favorites.$inferSelect;
export type NewFavorite = typeof favorites.$inferInsert;
export type FavoriteWithUser = Favorite & {
user: typeof users.$inferSelect;
};

Admin-curated collection of highlighted items. Supports ordering, activation/deactivation, and optional time-based expiration.

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
itemSlugitem_slugtextNo-Item identifier
itemNameitem_nametextNo-Denormalized
itemIconUrlitem_icon_urltextYes-Denormalized
itemCategoryitem_categorytextYes-Denormalized
itemDescriptionitem_descriptiontextYes-Denormalized
featuredOrderfeatured_orderintegerNo0Sort order
featuredUntilfeatured_untiltimestampYes-Auto-expire date
isActiveis_activebooleanNotrueActive toggle
featuredByfeatured_bytextNo-Admin user ID
featuredAtfeatured_attimestampNonow()-
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-

Indexes

NameColumnsType
featured_items_item_slug_idxitemSlugB-tree
featured_items_featured_order_idxfeaturedOrderB-tree
featured_items_is_active_idxisActiveB-tree
featured_items_featured_at_idxfeaturedAtB-tree
featured_items_featured_until_idxfeaturedUntilB-tree

TypeScript Types

export type FeaturedItem = typeof featuredItems.$inferSelect;
export type NewFeaturedItem = typeof featuredItems.$inferInsert;

Relations

// From relations.ts
export const favoritesRelations = relations(favorites, ({ one }) => ({
user: one(users, {
fields: [favorites.userId],
references: [users.id]
}),
}));

Relations Diagram


Aspectfavoritesfeatured_items
Created byEnd usersAdmin users
Per-userYes (user-scoped)No (global)
OrderingBy createdAtBy featuredOrder
ExpirationNoneOptional featuredUntil
Active toggleNo (exists = active)Yes (isActive flag)
Foreign keyusers.idNone (stores admin ID as text)

Query Examples

Add item to favorites

import { db } from '@/lib/db/drizzle';
import { favorites } from '@/lib/db/schema';

await db.insert(favorites).values({
userId,
itemSlug: 'my-tool-slug',
itemName: 'My Tool',
itemIconUrl: '/icons/my-tool.png',
itemCategory: 'Productivity',
}).onConflictDoNothing(); // Prevent duplicates

Remove item from favorites

import { eq, and } from 'drizzle-orm';

await db
.delete(favorites)
.where(
and(
eq(favorites.userId, userId),
eq(favorites.itemSlug, 'my-tool-slug')
)
);

Check if item is favorited

const isFavorited = await db
.select({ id: favorites.id })
.from(favorites)
.where(
and(
eq(favorites.userId, userId),
eq(favorites.itemSlug, 'my-tool-slug')
)
)
.limit(1);

Get user's favorites list

const userFavorites = await db
.select()
.from(favorites)
.where(eq(favorites.userId, userId))
.orderBy(desc(favorites.createdAt));

Get most favorited items

import { sql } from 'drizzle-orm';

const popular = await db
.select({
itemSlug: favorites.itemSlug,
itemName: favorites.itemName,
count: sql<number>`count(*)`,
})
.from(favorites)
.groupBy(favorites.itemSlug, favorites.itemName)
.orderBy(sql`count(*) desc`)
.limit(10);
import { featuredItems } from '@/lib/db/schema';

await db.insert(featuredItems).values({
itemSlug: 'premium-tool',
itemName: 'Premium Tool',
itemCategory: 'Productivity',
featuredOrder: 1,
isActive: true,
featuredBy: adminUserId,
featuredUntil: new Date('2025-12-31'),
});
import { or, isNull, gte } from 'drizzle-orm';

const activeFeatured = await db
.select()
.from(featuredItems)
.where(
and(
eq(featuredItems.isActive, true),
or(
isNull(featuredItems.featuredUntil),
gte(featuredItems.featuredUntil, new Date())
)
)
)
.orderBy(asc(featuredItems.featuredOrder));

Design Notes

  • Denormalized item data. Both tables store itemName, itemIconUrl, and itemCategory directly rather than looking up the Git CMS at read time. This makes list queries fast but means data can become stale if items are renamed.
  • No collection grouping. Unlike a full "collection" system with folders/lists, favorites is a flat list per user. Items can be filtered by itemCategory for grouping.
  • Featured items are global. They appear the same for all users, unlike favorites which are per-user.