Data Model
This document defines the complete database schema for BookWish, organized by the 4-layer architecture model.
Core Entities (Layer 1)
Users
Supports both guest and registered accounts with tier-based feature access.
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
device_id VARCHAR(255), -- For guest sessions
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255),
display_name VARCHAR(100),
username VARCHAR(50) UNIQUE,
avatar_url TEXT,
home_store_id UUID REFERENCES stores(id),
tier VARCHAR(20) DEFAULT 'free', -- guest|free|premium|bookstore
is_guest BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Stores
Independent bookstore profiles with location, branding, and integrations.
CREATE TABLE stores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_user_id UUID NOT NULL REFERENCES users(id),
name VARCHAR(200) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
-- Location
address_line1 VARCHAR(255),
address_line2 VARCHAR(255),
city VARCHAR(100),
state VARCHAR(50),
postal_code VARCHAR(20),
country VARCHAR(2) DEFAULT 'US',
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
-- Contact & Hours
phone VARCHAR(20),
email VARCHAR(255),
hours JSONB, -- {"mon": "9am-6pm", ...}
-- Branding
logo_url TEXT,
banner_url TEXT,
primary_color VARCHAR(7),
-- Website
website_enabled BOOLEAN DEFAULT false,
custom_domain VARCHAR(255),
-- Integrations
square_merchant_id VARCHAR(255),
square_access_token TEXT, -- Encrypted
inventory_source VARCHAR(20) DEFAULT 'native', -- native|square
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Books
Global book catalog with external IDs for integrations.
CREATE TABLE books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
isbn_13 VARCHAR(13) UNIQUE,
isbn_10 VARCHAR(10),
title VARCHAR(500) NOT NULL,
subtitle VARCHAR(500),
authors TEXT[], -- Array for multiple authors
publisher VARCHAR(255),
published_date DATE,
description TEXT,
page_count INTEGER,
categories TEXT[],
cover_image_url TEXT,
thumbnail_url TEXT,
-- External IDs
google_books_id VARCHAR(50),
open_library_id VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_books_isbn_13 ON books(isbn_13);
CREATE INDEX idx_books_title_gin ON books USING gin(to_tsvector('english', title));
Wishlists
User-created collections of books with privacy controls.
CREATE TABLE wishlists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100),
is_primary BOOLEAN DEFAULT false,
is_private BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, slug)
);
Wishlist Items
Books added to wishlists with priority and status tracking.
CREATE TABLE wishlist_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wishlist_id UUID NOT NULL REFERENCES wishlists(id) ON DELETE CASCADE,
book_id UUID NOT NULL REFERENCES books(id),
priority VARCHAR(20) DEFAULT 'normal', -- high|normal|low
status VARCHAR(20) DEFAULT 'wish', -- wish|reading|finished
added_at TIMESTAMPTZ DEFAULT NOW(),
status_changed_at TIMESTAMPTZ,
UNIQUE(wishlist_id, book_id)
);
Orders
Order records supporting multiple fulfillment types and payment methods.
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_number VARCHAR(20) UNIQUE NOT NULL,
user_id UUID REFERENCES users(id), -- Nullable for guest orders
store_id UUID REFERENCES stores(id), -- Null = BookWish Direct
is_bookwish_direct BOOLEAN DEFAULT false,
-- Fulfillment
fulfillment_type VARCHAR(20) NOT NULL, -- pickup|ship|gift
-- Status
status VARCHAR(30) DEFAULT 'pending',
-- pending|confirmed|processing|ready_for_pickup|shipped|delivered|cancelled
-- Addresses
shipping_address_id UUID REFERENCES addresses(id),
billing_address_id UUID REFERENCES addresses(id),
-- Gift info
is_gift BOOLEAN DEFAULT false,
gift_message TEXT,
gift_recipient_name VARCHAR(100),
gift_recipient_email VARCHAR(255),
-- Pricing
subtotal_cents INTEGER NOT NULL,
tax_cents INTEGER DEFAULT 0,
shipping_cents INTEGER DEFAULT 0,
discount_cents INTEGER DEFAULT 0,
trade_credit_applied_cents INTEGER DEFAULT 0,
total_cents INTEGER NOT NULL,
-- Payment
stripe_payment_intent_id VARCHAR(255),
payment_status VARCHAR(20) DEFAULT 'pending',
-- Shipping
shipping_carrier VARCHAR(50),
tracking_number VARCHAR(100),
easypost_shipment_id VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Order Items
Line items for orders.
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
book_id UUID NOT NULL REFERENCES books(id),
inventory_id UUID REFERENCES inventory(id),
quantity INTEGER DEFAULT 1,
unit_price_cents INTEGER NOT NULL,
is_used BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Addresses
Shipping and billing addresses for users.
CREATE TABLE addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
label VARCHAR(50), -- home|work|other
name VARCHAR(100) NOT NULL,
line1 VARCHAR(255) NOT NULL,
line2 VARCHAR(255),
city VARCHAR(100) NOT NULL,
state VARCHAR(50) NOT NULL,
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(2) DEFAULT 'US',
phone VARCHAR(20),
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Store Operations Entities (Layer 2)
Inventory
Per-store book inventory with condition tracking and Square sync.
CREATE TABLE inventory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
book_id UUID NOT NULL REFERENCES books(id),
sku VARCHAR(100),
square_item_id VARCHAR(100),
quantity INTEGER DEFAULT 0,
reserved_quantity INTEGER DEFAULT 0, -- Held during checkout
price_cents INTEGER NOT NULL,
compare_at_price_cents INTEGER, -- Original price for sales
condition VARCHAR(20) DEFAULT 'new', -- new|like_new|good|fair
is_used BOOLEAN DEFAULT false,
location VARCHAR(100), -- Shelf/section in store
last_synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(store_id, book_id, condition)
);
CREATE INDEX idx_inventory_store ON inventory(store_id);
CREATE INDEX idx_inventory_book ON inventory(book_id);
Trade Credit Accounts
Per-customer, per-store credit balances.
CREATE TABLE trade_credit_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
store_id UUID NOT NULL REFERENCES stores(id),
balance_cents INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, store_id)
);
Trade Credit Transactions
Ledger of credit and debit transactions.
CREATE TABLE trade_credit_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES trade_credit_accounts(id),
amount_cents INTEGER NOT NULL, -- Positive = credit, Negative = debit
transaction_type VARCHAR(30) NOT NULL, -- trade_in|purchase|adjustment
reference_id UUID, -- Order ID or trade-in record
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Trade-Ins
Trade-in sessions for used book purchases.
CREATE TABLE trade_ins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
store_id UUID NOT NULL REFERENCES stores(id),
customer_user_id UUID REFERENCES users(id),
staff_user_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) DEFAULT 'pending', -- pending|accepted|rejected|completed
total_credit_cents INTEGER DEFAULT 0,
total_cash_cents INTEGER DEFAULT 0,
payout_method VARCHAR(20), -- credit|cash
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
Trade-In Items
Individual books in a trade-in session.
CREATE TABLE trade_in_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trade_in_id UUID NOT NULL REFERENCES trade_ins(id) ON DELETE CASCADE,
book_id UUID NOT NULL REFERENCES books(id),
condition VARCHAR(20) NOT NULL,
offered_credit_cents INTEGER,
offered_cash_cents INTEGER,
-- BooksRun integration
booksrun_offer_cents INTEGER,
flip_to_booksrun BOOLEAN DEFAULT false,
accepted BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Social Entities (Layer 3)
Lines
Short posts about books (the social unit of BookWish).
CREATE TABLE lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_user_id UUID NOT NULL REFERENCES users(id),
book_id UUID NOT NULL REFERENCES books(id),
-- Optional context
store_id UUID REFERENCES stores(id), -- For store posts
club_id UUID REFERENCES clubs(id),
challenge_id UUID REFERENCES challenges(id),
content TEXT NOT NULL,
-- Engagement counts (denormalized for performance)
like_count INTEGER DEFAULT 0,
reply_count INTEGER DEFAULT 0,
-- Reply threading
parent_line_id UUID REFERENCES lines(id),
is_deleted BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_lines_author ON lines(author_user_id);
CREATE INDEX idx_lines_book ON lines(book_id);
CREATE INDEX idx_lines_created ON lines(created_at DESC);
Reviews
Book reviews with ratings and optional program context.
CREATE TABLE reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
book_id UUID NOT NULL REFERENCES books(id),
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
content TEXT,
-- Optional context
club_id UUID REFERENCES clubs(id),
challenge_id UUID REFERENCES challenges(id),
like_count INTEGER DEFAULT 0,
is_deleted BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, book_id) -- One review per user per book
);
Notes (Scribbles)
Private book notes visible only to the user.
CREATE TABLE notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
book_id UUID NOT NULL REFERENCES books(id),
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_notes_user_book ON notes(user_id, book_id);
Follows
User-to-user following relationships.
CREATE TABLE follows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
follower_id UUID NOT NULL REFERENCES users(id),
followed_id UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(follower_id, followed_id),
CHECK (follower_id != followed_id)
);
Store Follows
User-to-store following relationships.
CREATE TABLE store_follows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
store_id UUID NOT NULL REFERENCES stores(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, store_id)
);
Likes
Engagement on lines and reviews.
CREATE TABLE likes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
-- Polymorphic: either line_id or review_id
line_id UUID REFERENCES lines(id) ON DELETE CASCADE,
review_id UUID REFERENCES reviews(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CHECK (
(line_id IS NOT NULL AND review_id IS NULL) OR
(line_id IS NULL AND review_id IS NOT NULL)
)
);
Programs Entities (Layer 4)
Book Clubs
Community reading groups.
CREATE TABLE clubs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_user_id UUID NOT NULL REFERENCES users(id),
host_store_id UUID REFERENCES stores(id), -- If store-hosted
name VARCHAR(200) NOT NULL,
slug VARCHAR(100),
description TEXT,
cover_image_url TEXT,
is_public BOOLEAN DEFAULT true,
max_members INTEGER,
start_date DATE,
end_date DATE,
status VARCHAR(20) DEFAULT 'active', -- draft|active|completed|archived
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Club Books
Books selected for club reading.
CREATE TABLE club_books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
club_id UUID NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
book_id UUID NOT NULL REFERENCES books(id),
reading_order INTEGER,
discussion_start_date DATE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(club_id, book_id)
);
Club Members
Club membership tracking.
CREATE TABLE club_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
club_id UUID NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
role VARCHAR(20) DEFAULT 'member', -- host|moderator|member
joined_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(club_id, user_id)
);
Challenges
Reading challenges with time-based goals.
CREATE TABLE challenges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_store_id UUID REFERENCES stores(id),
is_bookwish_hosted BOOLEAN DEFAULT false,
name VARCHAR(200) NOT NULL,
slug VARCHAR(100),
description TEXT,
cover_image_url TEXT,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Challenge Books
Books included in a challenge.
CREATE TABLE challenge_books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
challenge_id UUID NOT NULL REFERENCES challenges(id) ON DELETE CASCADE,
book_id UUID NOT NULL REFERENCES books(id),
is_spotlight BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(challenge_id, book_id)
);
Challenge Participants
Challenge participation tracking.
CREATE TABLE challenge_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
challenge_id UUID NOT NULL REFERENCES challenges(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id),
joined_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(challenge_id, user_id)
);
Notifications & Feed Support
Notifications
In-app and push notifications.
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
type VARCHAR(50) NOT NULL,
-- follow|like|reply|order_update|stock_alert|club_invite|challenge_start
title VARCHAR(200),
body TEXT,
-- Polymorphic references
actor_user_id UUID REFERENCES users(id),
line_id UUID REFERENCES lines(id),
review_id UUID REFERENCES reviews(id),
order_id UUID REFERENCES orders(id),
club_id UUID REFERENCES clubs(id),
challenge_id UUID REFERENCES challenges(id),
book_id UUID REFERENCES books(id),
is_read BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_notifications_user ON notifications(user_id, created_at DESC);
Push Tokens
Device push notification tokens.
CREATE TABLE push_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
token TEXT NOT NULL,
platform VARCHAR(20) NOT NULL, -- ios|android|web
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(token)
);
Order Notification Digest
Queue for daily digest emails to store owners.
-- Order digest queue (for stores with daily_digest preference)
CREATE TABLE order_digest_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
is_special_order BOOLEAN DEFAULT false,
processed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- BookWish operations digest queue (always daily)
CREATE TABLE bookwish_ops_digest_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
processed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Feed Query Strategy
The Share tab feed uses fan-out-on-read with caching:
// Feed query filters
interface FeedQuery {
scope: 'all' | 'following' | 'local' | 'reviews' | 'challenges';
cursor?: string;
limit?: number;
}
// Feed composition (pseudo-SQL)
SELECT lines.*, users.*, books.*
FROM lines
JOIN users ON lines.author_user_id = users.id
JOIN books ON lines.book_id = books.id
WHERE (
-- Following filter
(scope = 'following' AND author_user_id IN (SELECT followed_id FROM follows WHERE follower_id = ?))
OR
-- Local filter (home store + nearby)
(scope = 'local' AND store_id = ?)
OR
-- All: following + home store + clubs + system content
(scope = 'all' AND (
author_user_id IN (SELECT followed_id FROM follows WHERE follower_id = ?)
OR store_id = ?
OR club_id IN (SELECT club_id FROM club_members WHERE user_id = ?)
))
)
ORDER BY created_at DESC
LIMIT 20;