Data Models & Schema Design
Flour n Sugar uses PostgreSQL, Prisma, and Better Auth. The model separates platform authority from a person's role within a particular store, so one person can be a customer, own a store, and work for another.
Entity Relationship Overview
User ── 1:1 ── CustomerProfile
│
├── 1:N ── StoreMember ── N:1 ── Store ── 1:N ── Product ── 1:N ── ProductVariant
│ │
│ └── StoreRole (OWNER, MANAGER, BAKER, STAFF, DELIVERY, ACCOUNTANT)
│
└── PlatformRole (USER, MODERATOR, SUPPORT, ADMIN)
Subcategory ── 1:N ── CatalogProduct
│
└── referenced by store Products
Core Prisma Schema Specification
This is the intended MVP shape. It keeps authorization boundaries explicit while allowing product, image, and catalogue fields to evolve independently.
enum PlatformRole {
USER
MODERATOR
SUPPORT
ADMIN
}
enum StoreRole {
OWNER
MANAGER
BAKER
STAFF
DELIVERY
ACCOUNTANT
}
enum StoreStatus {
DRAFT
PUBLISHED
SUSPENDED
}
enum ProductUnit {
KG
GRAM
POUND
PIECE
DOZEN
BOX
PACK
SLICE
SERVING
INCH
ML
LITER
}
model User {
id String @id @default(uuid())
name String
email String @unique
emailVerified Boolean @default(false)
image String?
platformRole PlatformRole @default(USER)
storeMemberships StoreMember[]
customerProfile CustomerProfile?
sessions Session[]
accounts Account[]
}
model Store {
id String @id @default(uuid())
slug String @unique
name String
status StoreStatus @default(DRAFT)
members StoreMember[]
products Product[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model StoreMember {
userId String
storeId String
role StoreRole
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([userId, storeId])
@@index([storeId])
}
model Product {
id String @id @default(uuid())
storeId String
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
name String
description String?
images String[] @default([])
isEggless Boolean @default(false)
isVegan Boolean @default(false)
isGlutenFree Boolean @default(false)
isAvailable Boolean @default(true)
variants ProductVariant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storeId])
}
model ProductVariant {
id String @id @default(uuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
name String? // Optional display name, e.g. "Half Kg"
quantity Decimal? @db.Decimal(10, 3)
unit ProductUnit?
price Decimal @db.Decimal(10, 2)
isAvailable Boolean @default(true)
sortOrder Int @default(0)
@@index([productId, sortOrder])
}
Authorization and Data-Isolation Rules
PlatformRolecontrols platform-wide operations such as moderation and support; it is not a substitute for store permissions.StoreMembercontrols access to one store. A role is a default permission bundle, allowing future refinement without changing the tenant relationship.- Store-facing queries and mutations must always scope records through the active store membership.
- Public discovery and storefront reads must return only
PUBLISHEDstores. Owners and authorized staff can access their own drafts.