Database schema and Prisma client for NarrativeForge.
- PostgreSQL database with Prisma ORM
- Type-safe queries with auto-generated types
- Migration support for schema evolution
- Seed data for development/testing
- Utility functions for common operations
Users - Wallet-based authentication, stats, betting history
{
walletAddress: string
username?: string
totalBets: number
winRate: number
bets: Bet[]
}Stories - AI-generated interactive fiction
{
title: string
genre: string
status: StoryStatus
chapters: Chapter[]
totalBets: Decimal
}Chapters - Individual story segments with choices
{
chapterNumber: number
content: string (full text)
status: ChapterStatus
choices: Choice[]
bettingPool?: BettingPool
}Choices - Story branching options
{
text: string
isChosen: boolean
aiScore: number
totalBets: Decimal
bets: Bet[]
}BettingPools - Parimutuel betting on AI choices
{
status: PoolStatus
totalPool: Decimal
closesAt: Date
bets: Bet[]
}Bets - Individual user bets
{
user: User
choice: Choice
amount: Decimal
isWinner: boolean
payout?: Decimal
}AIGeneration - Logs for all AI operations Analytics - Daily platform metrics
cd packages/database
npm install# .env
DATABASE_URL="postgresql://user:password@localhost:5432/narrativeforge?schema=public"# Push schema to database (development)
npm run db:push
# Or create migration (production)
npm run db:migratenpm run db:seedThis creates sample data:
- 2 users (alice_crypto, bob_trader)
- 1 story ("The Last Starforge")
- 2 chapters (1 resolved, 1 with active betting)
- 4 choices
- 1 active betting pool
- Sample bets
npm run db:studioView and edit data at http://localhost:5555
import { prisma } from '@narrative-forge/database'
// Get active stories
const stories = await prisma.story.findMany({
where: { status: 'ACTIVE' },
include: {
chapters: {
orderBy: { chapterNumber: 'asc' },
},
},
})
// Get betting pool with choices
const pool = await prisma.bettingPool.findUnique({
where: { id: poolId },
include: {
chapter: {
include: {
choices: {
include: {
_count: {
select: { bets: true },
},
},
},
},
},
bets: {
include: {
user: true,
},
},
},
})
// Create a bet
const bet = await prisma.bet.create({
data: {
userId: user.id,
poolId: pool.id,
choiceId: choice.id,
amount: 10.5,
odds: 2.125,
txHash: '0x...',
},
})import { calculateOdds, calculatePayout, formatTimeRemaining } from '@narrative-forge/database'
// Calculate odds for a choice
const odds = calculateOdds(choiceBets, totalPool)
// Calculate payout for winning bet
const payout = calculatePayout(betAmount, choiceBets, totalPool, 0.85)
// Time remaining in pool
const timeLeft = formatTimeRemaining(pool.closesAt)
// Returns: "4h 23m"id- CUID primary keywalletAddress- Unique Ethereum addressusername- Optional display nametotalBets- Number of bets placedtotalWon- Total FORGE wontotalLost- Total FORGE lostwinRate- Win percentage (0-100)
id- CUID primary keytitle- Story namegenre- Sci-Fi, Fantasy, etc.status- ACTIVE, PAUSED, COMPLETED, ARCHIVEDcurrentChapter- Latest chapter numbertotalReaders- Unique readerstotalBets- Total FORGE wagered
id- CUID primary keystoryId- Foreign key to StorychapterNumber- Sequential numbercontent- Full chapter textstatus- DRAFT, PUBLISHED, BETTING, RESOLVEDaiModel- Model used for generationpublishedAt- Publication timestamp
id- CUID primary keychapterId- Foreign key to ChapterchoiceNumber- 1, 2, 3, etc.text- Choice descriptionisChosen- AI's selectionaiScore- AI's preference (0-100)totalBets- Total FORGE on this choice
id- CUID primary keychapterId- Foreign key to Chapter (unique)status- PENDING, OPEN, CLOSED, RESOLVING, RESOLVEDtotalPool- Sum of all betsclosesAt- Deadline for betscontractAddress- On-chain pool addresswinningChoiceId- Winner after resolution
id- CUID primary keyuserId- Foreign key to UserpoolId- Foreign key to BettingPoolchoiceId- Foreign key to Choiceamount- Bet amount in FORGEisWinner- True if wonpayout- Winnings (if won)
npm run db:migrate
# Name: add_user_avatar_fieldThis creates:
- Migration SQL in
prisma/migrations/ - Updates
@prisma/clienttypes
npm run db:resetawait prisma.$transaction(async (tx) => {
// Create bet
const bet = await tx.bet.create({ data: betData })
// Update choice totals
await tx.choice.update({
where: { id: choiceId },
data: {
totalBets: { increment: betAmount },
betCount: { increment: 1 },
},
})
// Update pool totals
await tx.bettingPool.update({
where: { id: poolId },
data: {
totalPool: { increment: betAmount },
totalBets: { increment: 1 },
},
})
})const user = await prisma.user.findUnique({
where: { walletAddress },
select: {
id: true,
username: true,
walletAddress: true,
// Don't include relations unless needed
},
})// Bad: Loads all bets (could be 1000s)
const pool = await prisma.bettingPool.findUnique({
where: { id },
include: { bets: true },
})
// Good: Aggregate instead
const pool = await prisma.bettingPool.findUnique({
where: { id },
include: {
_count: {
select: { bets: true },
},
},
})Already indexed in schema:
User.walletAddress(unique)Story.status + createdAtBettingPool.status + closesAtBet.userId, poolId, choiceId
import { prisma } from '@narrative-forge/database'
describe('Betting', () => {
beforeEach(async () => {
await prisma.bet.deleteMany()
})
it('creates a bet', async () => {
const bet = await prisma.bet.create({
data: {
userId: 'user_123',
poolId: 'pool_456',
choiceId: 'choice_789',
amount: 10,
},
})
expect(bet.amount).toBe(10)
})
})import { exec } from 'child_process'
beforeAll(async () => {
// Reset test database
exec('npm run db:reset -- --force')
})Check DATABASE_URL in .env:
DATABASE_URL="postgresql://user:password@localhost:5432/narrativeforge"Run migrations:
npm run db:pushGenerate client:
npm run db:generateReset and try again:
npm run db:reset# Production
DATABASE_URL="postgresql://user:password@production-host:5432/narrativeforge?schema=public&sslmode=require"
# Enable connection pooling
DATABASE_URL="postgresql://user:password@pooler.production.com:5432/narrativeforge?pgbouncer=true"# Don't use db:push in production!
# Always use migrations:
npx prisma migrate deploy# Backup
pg_dump -U user -h host narrativeforge > backup.sql
# Restore
psql -U user -h host narrativeforge < backup.sqlUser ─┬─> Bet ──> BettingPool ──> Chapter ──> Story
└─> Story (author)
Choice ──> Chapter
└─> Bet
AIGeneration (logs all AI operations)
Analytics (daily metrics)
- Set up your database (PostgreSQL locally or cloud)
- Run migrations:
npm run db:push - Seed data:
npm run db:seed - Build API routes using this schema
- Test queries in Prisma Studio
Happy coding! 🚀