feat: Add scalability & network effects architecture

## Strategic Additions

### 1. Product Strategy (PRODUCT_STRATEGY.md)
Complete go-to-market and competitive moat strategy:

**Network Effects Design:**
- Data Network Effect: AI improves with every interaction
- Content Network Effect: Community-contributed cases
- Social Network Effect: Study groups & peer learning
- Marketplace Network Effect: Two-sided creator economy

**Defensible Moats:**
- Data Moat: Proprietary ML models trained on millions of interactions
- Network Moat: Social lock-in via study groups
- Content Moat: Largest validated case library
- Brand Moat: Community identity and trust
- Regulatory Moat: Official partnerships with medical schools

**SaaS Business Model:**
- Phase 1: Freemium (10% conversion target)
- Phase 2: Tiered pricing ($19-79/month)
- Phase 3: B2B SaaS (medical schools)
- Phase 4: API licensing & Enterprise

**10-Year Vision:**
Year 1-2: Best residency exam prep in Brazil
Year 3-5: Platform for all medical education
Year 5-7: Expand to Latin America
Year 7-10: Global medical education platform ($50M+ ARR)

### 2. Network Effects Schema (schema-network-effects.sql)
Complete database extension for social platform features:

**New Tables (20+):**
- Community cases & reviews (content network)
- Study groups & challenges (social network)
- Marketplace & creator profiles (two-sided market)
- Forum & discussions (community)
- Peer matching & interactions (social graph)
- Leaderboards & competitions (gamification)
- Calibration & ML models (data network)

**Key Features:**
- Row Level Security on all tables
- Automatic triggers for stats updates
- Materialized views for analytics
- Cross-table relationships for network effects

### 3. Scalability Architecture (SCALABILITY_ARCHITECTURE.md)
Technical roadmap from 0 to 1M+ users:

**Stage 1 (0-10k users):**
- Cost: $410/month
- Stack: Vercel + Supabase + Claude API
- No caching needed

**Stage 2 (10k-100k users):**
- Cost: $2,919/month ($0.029/user)
- Add: Redis caching, read replicas, monitoring
- 70% cache hit rate reduces AI costs

**Stage 3 (100k-1M users):**
- Cost: $11,700/month ($0.012/user)
- Add: Database sharding (4 shards)
- Microservices for AI, analytics
- Background job processing
- 95% AI response caching

**Stage 4 (1M+ users):**
- Cost: $50-100k/month
- Full microservices architecture
- Dedicated services per domain
- ClickHouse for analytics
- Global CDN distribution

**Key Insights:**
- Cost per user DECREASES with scale (economies of scale)
- 95% AI cost reduction through intelligent caching
- Zero-downtime deployments required
- Progressive scaling (build for today, architect for tomorrow)

## Why This Matters

**For Investors:**
- Clear path to $50M+ ARR
- Defensible moats (4-5 years to replicate)
- 93% gross margins at scale
- Network effects create winner-take-all dynamics

**For Developers:**
- Concrete technical roadmap
- Know exactly when to scale what
- Cost predictability
- No premature optimization

**For Users:**
- Platform gets better with every user (network effects)
- Social features create stickiness
- Community-driven content
- Clear value proposition

This is the blueprint for building an unassailable position
in medical education.
This commit is contained in:
Claude
2025-11-25 20:38:06 +00:00
parent 5af8a85508
commit 410b4602e8
3 changed files with 2435 additions and 0 deletions
+716
View File
@@ -0,0 +1,716 @@
# MEDCARDS.AI - Product Strategy & Network Effects Architecture
## 🎯 Product Vision: From Tool to Platform
**Current State**: Individual study tool (MVP)
**Future State**: Network-powered medical education platform with defensible moats
---
## 🔄 Network Effects Strategy
### 1. **Data Network Effect** (Primary Moat)
#### The Flywheel
```
More Students → More Interactions → Better AI Predictions →
Better Learning Outcomes → More Students → ...
```
**Implementation:**
Every interaction improves the system for ALL users:
```typescript
// Database additions to existing schema
CREATE TABLE case_difficulty_calibration (
case_id UUID REFERENCES clinical_cases(id),
actual_difficulty_score NUMERIC, -- Calculated from real user performance
expected_vs_actual_delta NUMERIC, -- How off were we?
sample_size INTEGER,
confidence_level NUMERIC,
updated_at TIMESTAMP
);
CREATE TABLE prediction_model_versions (
id UUID PRIMARY KEY,
version TEXT,
training_data_size INTEGER,
accuracy_metrics JSONB,
deployed_at TIMESTAMP,
performance_improvement_vs_previous NUMERIC
);
```
**Value Proposition:**
- First 1,000 users: AI accuracy ~70%
- At 10,000 users: AI accuracy ~85%
- At 100,000 users: AI accuracy ~95%
**→ Late entrants can never match prediction quality without the data**
---
### 2. **Content Network Effect** (Secondary Moat)
#### Community-Contributed Cases
**Phase 1: Curated Contributions**
```typescript
CREATE TABLE community_cases (
id UUID PRIMARY KEY,
created_by_user_id UUID REFERENCES users(id),
case_content JSONB, -- Same structure as clinical_cases
status TEXT CHECK (status IN ('draft', 'submitted', 'under_review', 'approved', 'rejected')),
community_rating NUMERIC,
times_used INTEGER DEFAULT 0,
success_rate NUMERIC,
curator_notes TEXT,
approved_by_user_id UUID REFERENCES users(id),
approved_at TIMESTAMP,
earnings_generated NUMERIC DEFAULT 0 -- For revenue sharing
);
CREATE TABLE case_reviews (
id UUID PRIMARY KEY,
case_id UUID REFERENCES community_cases(id),
reviewer_user_id UUID REFERENCES users(id),
clinical_accuracy_score INTEGER CHECK (1 <= score <= 5),
educational_value_score INTEGER CHECK (1 <= score <= 5),
review_text TEXT,
is_expert_review BOOLEAN DEFAULT false -- Verified doctors/professors
);
```
**Incentive Mechanics:**
- Users who create approved cases earn credits
- Credits = access to premium features OR cash payout
- Top contributors get "Verified Educator" badge
- Cases that perform well (high success in teaching) earn more
**Network Effect:**
- 1,000 users → ~50 quality cases/month
- 10,000 users → ~500 quality cases/month
- 100,000 users → ~5,000 quality cases/month
**→ Library becomes impossible to replicate**
---
### 3. **Social Learning Network Effect**
#### Study Groups & Peer Competition
```typescript
CREATE TABLE study_groups (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
created_by_user_id UUID REFERENCES users(id),
is_public BOOLEAN DEFAULT false,
member_limit INTEGER,
created_at TIMESTAMP,
-- Group configuration
focus_specialties TEXT[],
target_exam TEXT, -- "REVALIDA 2025", "USP Clínica Médica", etc.
study_schedule JSONB, -- When they study together
-- Group stats
total_cases_solved INTEGER DEFAULT 0,
avg_group_success_rate NUMERIC,
active_members_count INTEGER
);
CREATE TABLE study_group_members (
group_id UUID REFERENCES study_groups(id),
user_id UUID REFERENCES users(id),
joined_at TIMESTAMP,
role TEXT CHECK (role IN ('owner', 'admin', 'member')),
contribution_score INTEGER DEFAULT 0, -- Based on activity
PRIMARY KEY (group_id, user_id)
);
CREATE TABLE group_challenges (
id UUID PRIMARY KEY,
group_id UUID REFERENCES study_groups(id),
created_by_user_id UUID REFERENCES users(id),
challenge_type TEXT, -- "speed_run", "accuracy_battle", "specialty_mastery"
case_pool UUID[], -- Array of case IDs for this challenge
start_time TIMESTAMP,
end_time TIMESTAMP,
prize_type TEXT, -- "badges", "credits", "bragging_rights"
status TEXT CHECK (status IN ('upcoming', 'active', 'completed'))
);
CREATE TABLE challenge_leaderboard (
challenge_id UUID REFERENCES group_challenges(id),
user_id UUID REFERENCES users(id),
score INTEGER,
time_completed_seconds INTEGER,
rank INTEGER,
PRIMARY KEY (challenge_id, user_id)
);
CREATE TABLE peer_interactions (
id UUID PRIMARY KEY,
from_user_id UUID REFERENCES users(id),
to_user_id UUID REFERENCES users(id),
interaction_type TEXT, -- "study_together", "case_recommendation", "explanation_request"
context JSONB,
created_at TIMESTAMP
);
```
**Social Features:**
1. **Study Groups**
- Create private/public groups
- Compete on group leaderboards
- Shared progress tracking
- Group study sessions (everyone does same cases simultaneously)
2. **Peer Challenges**
- "Beat my time on this cardiology case!"
- Weekly group tournaments
- Specialty mastery races
3. **Collaborative Learning**
- Ask peer who scored high: "How did you approach this?"
- Share case explanations
- Study buddy matching algorithm
**Network Effect:**
- Student invites 3 friends to their study group
- Friends see their progress and want to compete
- Group creates challenges → more engagement
- Students stay because their friends are here
**→ Social lock-in (WhatsApp effect)**
---
### 4. **Marketplace Network Effect**
#### Two-Sided Market: Students ↔ Educators
```typescript
CREATE TABLE premium_content (
id UUID PRIMARY KEY,
creator_user_id UUID REFERENCES users(id),
content_type TEXT, -- "course", "case_pack", "specialty_bundle", "ai_tutor_session"
title TEXT NOT NULL,
description TEXT,
price_credits INTEGER,
price_reais NUMERIC, -- For direct purchase
content_metadata JSONB,
/*
{
"case_count": 50,
"specialty": "cardiologia",
"difficulty_range": [3, 5],
"includes_video_explanations": true,
"creator_credentials": "Cardiologista HC-USP"
}
*/
-- Performance metrics
purchases_count INTEGER DEFAULT 0,
avg_rating NUMERIC,
review_count INTEGER,
revenue_generated NUMERIC,
is_verified BOOLEAN DEFAULT false, -- Verified quality
created_at TIMESTAMP
);
CREATE TABLE content_purchases (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
content_id UUID REFERENCES premium_content(id),
purchased_at TIMESTAMP,
price_paid_credits INTEGER,
price_paid_reais NUMERIC
);
CREATE TABLE creator_profiles (
user_id UUID PRIMARY KEY REFERENCES users(id),
is_verified_educator BOOLEAN DEFAULT false,
credentials TEXT, -- "Médico residente R3 Cardiologia USP"
bio TEXT,
-- Creator stats
total_content_created INTEGER DEFAULT 0,
total_revenue_earned NUMERIC DEFAULT 0,
follower_count INTEGER DEFAULT 0,
avg_content_rating NUMERIC,
-- Payout info
payout_method TEXT,
payout_details JSONB
);
CREATE TABLE creator_followers (
follower_user_id UUID REFERENCES users(id),
creator_user_id UUID REFERENCES users(id),
followed_at TIMESTAMP,
PRIMARY KEY (follower_user_id, creator_user_id)
);
```
**Marketplace Mechanics:**
**For Students:**
- Buy specialized case packs from top educators
- Subscribe to favorite creators
- Access expert-made content
- Get 1-on-1 AI tutoring sessions (premium)
**For Educators:**
- Create and sell content
- Earn 70% of sales (platform keeps 30%)
- Build following and reputation
- Verified badges for credentials
**Network Effect:**
- More students → attract more educators (bigger market)
- More educators → more quality content → attract more students
- Best educators make real money → more educators join
- Platform becomes THE marketplace for medical ed content
**→ Two-sided marketplace moat**
---
## 🏰 Defensible Moats Summary
### 1. **Data Moat** (Strongest)
- Millions of student-case interactions
- Proprietary adaptive algorithm trained on real performance
- Prediction accuracy improves with scale
- **Time to replicate**: 3-5 years minimum
### 2. **Network Effects Moat**
- Social graph (study groups, peer learning)
- Content library (community cases)
- Marketplace (two-sided)
- **Switching cost**: Lose all friends, content, progress
### 3. **Brand & Community Moat**
- "The platform where serious residents study"
- Community trust and identity
- User-generated content and culture
- **Intangible but powerful**
### 4. **Regulatory/Trust Moat** (Future)
- Official partnerships with medical schools
- Endorsements from medical councils
- Verified by actual residency programs
- **Exclusive relationships**
### 5. **Technology Moat**
- Proprietary AI architecture
- Medical-specific NLP models
- Clinical reasoning engine
- **Patent-pending algorithms**
---
## 💰 SaaS Business Model Evolution
### Phase 1: Freemium (Launch - 12 months)
**Free Tier:**
- 10 cases/day
- Basic AI feedback
- Solo study only
- Generic study plan
**Premium ($29/month or R$149/month):**
- Unlimited cases
- Advanced AI tutor (chat)
- Study groups & challenges
- Personalized adaptive learning
- Performance analytics
- Badge system
- 100 credits/month for marketplace
**Conversion Strategy:**
- Free tier proves value
- Hit daily limit → upgrade friction point
- Study group invites from premium users
- "Your friends are Premium, join them"
**Target**: 10% conversion (industry standard)
---
### Phase 2: Tiered SaaS (12-24 months)
**Free:** 5 cases/day
**Basic ($19/month):** 20 cases/day + groups
**Pro ($39/month):** Unlimited + AI tutor + analytics
**Elite ($79/month):** Everything + marketplace credits + priority support + verified mentor matching
**New Revenue Stream: Credits**
- Buy credits for marketplace
- $10 = 100 credits
- Spend on premium cases, tutoring, etc.
---
### Phase 3: B2B SaaS (18+ months)
**Target**: Medical Schools & Prep Courses
**School Plans:**
- $999/month for 100 students
- $4,999/month for unlimited students
- White-label option
- Admin dashboard with class analytics
- Custom case library management
- Integration with school LMS
**Value Prop for Schools:**
- Track student progress
- Identify struggling students early
- Improve board exam pass rates
- Data-driven curriculum decisions
**Moat**: Once a school adopts, students use it → network effect when they graduate and tell others
---
### Phase 4: Enterprise & API (24+ months)
**API Access:**
- Other edtech companies license our AI
- Healthcare systems for resident training
- $0.10 per AI inference
**Enterprise Partnerships:**
- Hospitals for resident education
- Medical associations for CME
- Insurance companies (better trained doctors = better outcomes)
---
## 📈 Scalability Architecture
### Current Architecture (Good for 0-10k users)
```
Vercel Edge Functions → Supabase PostgreSQL → Claude API
```
### Growth Architecture (10k-100k users)
```typescript
// Add to schema
CREATE TABLE cache_ai_responses (
cache_key TEXT PRIMARY KEY,
response_data JSONB,
created_at TIMESTAMP,
hit_count INTEGER DEFAULT 0,
ttl INTEGER DEFAULT 3600 -- seconds
);
-- Index for faster lookups
CREATE INDEX idx_cache_ttl ON cache_ai_responses(created_at)
WHERE (EXTRACT(EPOCH FROM (NOW() - created_at)) < ttl);
```
**Caching Strategy:**
- Common case feedback cached (80% hit rate)
- AI responses for popular cases
- User profiles in Redis
- CDN for static assets
**Database Optimization:**
- Read replicas for analytics queries
- Partitioning interactions table by month
- Materialized views for dashboards
---
### Scale Architecture (100k-1M+ users)
**Microservices Split:**
```
├── Case Service (Supabase)
├── AI Service (Dedicated Claude inference server)
├── User Service (Supabase)
├── Analytics Service (Separate read DB)
└── Marketplace Service (Separate transaction DB)
```
**Infrastructure:**
- PostgreSQL: Supabase Pro → Dedicated instance with pgBouncer
- Caching: Vercel Edge Cache → Redis (Upstash) → CloudFlare CDN
- AI: Claude API → Anthropic batch API (cheaper for non-real-time)
- Background Jobs: Inngest or Temporal for async processing
- Monitoring: Datadog + Sentry
**Cost at Scale:**
- 100k active users
- 1M cases/day
- Estimated: $15k/month infrastructure
- AI costs: $5k/month (with caching)
- **Total**: ~$20k/month = $0.20/user/month
- **Revenue** (10% paid at $29): $290k/month
- **Gross Margin**: 93%
---
## 🎮 Gamification & Engagement Design
### Core Engagement Loop (Daily)
```
1. Open App → See streak (don't break it!)
2. Dashboard shows: "Your friend João just beat your cardiology score"
3. Do 5 quick cases to regain #1 spot
4. Unlock badge → Share on WhatsApp
5. Friend sees → comes back to compete
```
### Retention Mechanics
**Daily:**
- Streak counter (Duolingo-style)
- Daily challenge case (bonus points)
- Study group activity feed
**Weekly:**
- Group leaderboard reset
- Weekly progress report email
- "You vs Last Week" comparison
**Monthly:**
- Specialty mastery level-ups
- Community case voting
- Creator earnings payout
**Quarterly:**
- Nationwide leaderboards
- Seasonal tournaments ($1000 prize)
- Medical school rankings
---
## 🌐 Community Features (Social Layer)
### Discussion Forum
```typescript
CREATE TABLE forum_posts (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
category TEXT, -- "case_discussion", "study_tips", "exam_strategies"
title TEXT NOT NULL,
content TEXT NOT NULL,
related_case_id UUID REFERENCES clinical_cases(id),
upvotes INTEGER DEFAULT 0,
view_count INTEGER DEFAULT 0,
created_at TIMESTAMP
);
CREATE TABLE forum_comments (
id UUID PRIMARY KEY,
post_id UUID REFERENCES forum_posts(id),
user_id UUID REFERENCES users(id),
content TEXT NOT NULL,
upvotes INTEGER DEFAULT 0,
is_expert_answer BOOLEAN DEFAULT false,
created_at TIMESTAMP
);
```
**Use Cases:**
- "Can someone explain this cardio case differently?"
- "Study tips for neurologia?"
- "Who else is taking REVALIDA March 2025?"
**Network Effect**: More users → more discussions → more value → more users
---
### Study Buddy Matching
```typescript
CREATE TABLE study_preferences (
user_id UUID PRIMARY KEY REFERENCES users(id),
target_exam TEXT,
exam_date DATE,
weak_specialties TEXT[],
preferred_study_times TEXT[], -- "weekday_mornings", "weekend_afternoons"
study_style TEXT, -- "competitive", "collaborative", "solo_with_accountability"
looking_for_buddy BOOLEAN DEFAULT false
);
-- ML-powered matching
CREATE TABLE study_buddy_matches (
id UUID PRIMARY KEY,
user1_id UUID REFERENCES users(id),
user2_id UUID REFERENCES users(id),
match_score NUMERIC, -- Compatibility score
match_reason JSONB,
status TEXT CHECK (status IN ('suggested', 'accepted', 'active', 'ended')),
created_at TIMESTAMP
);
```
**Algorithm:**
- Match by: similar level, complementary weaknesses, same exam date, compatible schedules
- "You're both weak in neuro → practice together"
- "João is strong where you're weak → learn from him"
---
## 🚀 Go-to-Market Strategy
### Phase 1: Seed Community (0-100 users)
**Tactic**: Manual recruitment from specific medical school
- Offer free premium for 6 months
- Recruit 20 students from USP/UNIFESP
- Ask them to invite friends
- Dogfood the product hard
### Phase 2: Single University Dominance (100-1000 users)
**Tactic**: Win one school completely
- Become "the platform" at USP Medicina
- 70%+ of students using it
- Leverage social proof: "Everyone at USP uses this"
- Case studies of students who passed
### Phase 3: University Expansion (1k-10k users)
**Tactic**: Replicate to other top schools
- UNIFESP, UFRJ, UFMG, etc.
- University ambassadors (pay in credits)
- School leaderboards (create competition)
- "USP vs UNIFESP" challenges
### Phase 4: National Scale (10k-100k users)
**Tactic**: Paid acquisition + viral loops
- Facebook/Instagram ads targeting "residência médica"
- Referral program: "Invite 3 friends → 1 month free"
- Content marketing (blog about exam strategies)
- YouTube: "How I passed with 85% using MedCards"
### Phase 5: Platform Lock-in (100k+ users)
**Tactic**: Become infrastructure
- Partner with medical schools officially
- Licensing to prep courses
- Government partnerships (SUS resident training)
---
## 📊 Success Metrics (North Star + Supporting)
### North Star Metric
**Weekly Active Cases Solved**
- Measures: Engagement × Value delivered
- Target Growth: 20% MoM
### Supporting Metrics
**Acquisition:**
- Signups/week
- Source attribution
- Activation rate (completed 10 cases in first week)
**Engagement:**
- DAU/MAU ratio (target: >40%)
- Cases per session
- Streak retention
**Monetization:**
- Free → Paid conversion rate
- MRR growth
- LTV/CAC ratio
**Network Effects:**
- Study group creation rate
- Avg group size
- Community case submissions/week
- Marketplace transactions/week
**Retention:**
- D7, D30, D90 retention
- Churn rate
- Win-back rate
---
## 🎯 Product Roadmap
### Q1 2025: Foundation + MVP
- Core case training
- Basic AI feedback
- Authentication
- Solo study mode
### Q2 2025: Social Layer
- Study groups
- Peer challenges
- Leaderboards
- Basic community features
### Q3 2025: Marketplace
- Community case submissions
- Premium content
- Creator tools
- Credits system
### Q4 2025: B2B Pilot
- School admin dashboard
- Class analytics
- Custom case libraries
- API access (beta)
### 2026: Platform
- Mobile app (React Native)
- API productization
- International expansion
- Enterprise features
---
## 💡 Moat Reinforcement Strategy
**Continuous Improvement Loop:**
1. **Data Moat**: Every case solved → better AI → better outcomes → more users
2. **Content Moat**: Best community cases promoted → creators earn → more quality content
3. **Network Moat**: Study group features → invite friends → social lock-in
4. **Brand Moat**: Best students use it → aspirational brand → more sign-ups
**Defensive Tactics:**
- Long-term contracts with medical schools (lock-in)
- Exclusive partnerships with exam boards
- Patent AI methodology (if truly novel)
- Build community identity ("MedCards Residents")
---
## 🔮 10-Year Vision
**Year 1-2**: Best residency exam prep in Brazil
**Year 3-5**: Platform for all medical education in Brazil (undergrad → CME)
**Year 5-7**: Expand to Latin America (same market dynamics)
**Year 7-10**: Global platform for medical education
**End State:**
- 500k+ active learners
- $50M+ ARR
- Acquisition target for Duolingo, Coursera, or major medical publisher
- OR: IPO as EdTech/HealthTech platform
---
**This is how you build an unassailable position in medical education.**
Ready to implement the enhanced schema with network effects?
+731
View File
@@ -0,0 +1,731 @@
# MEDCARDS.AI - Scalability Architecture & Technical Infrastructure
## 🎯 Scaling Philosophy
**Build for 10k users, architect for 1M users.**
This document outlines how MEDCARDS.AI scales from MVP (1k users) to platform (1M+ users) without major rewrites.
---
## 📊 Growth Stages & Infrastructure Evolution
### Stage 1: MVP (0-10k users)
**Monthly Active Users**: 0-10,000
**Daily Interactions**: 0-100k
**Infrastructure Cost**: $500-1,000/month
**Stack:**
```
Frontend: Vercel Edge Network
Backend: Next.js Server Actions (Vercel Serverless)
Database: Supabase Free/Pro (PostgreSQL)
AI: Anthropic Claude API (pay-per-use)
Cache: None (database only)
CDN: Vercel automatic
```
**Why it works:**
- Serverless scales automatically
- No DevOps required
- Pay only for usage
- Deploy in minutes
**Bottlenecks:**
- None at this scale
- Database has 10GB limit (sufficient for 10k users)
---
### Stage 2: Growth (10k-100k users)
**Monthly Active Users**: 10,000-100,000
**Daily Interactions**: 100k-1M
**Infrastructure Cost**: $2,000-5,000/month
**Stack Upgrades:**
```
Frontend: Vercel Edge Network (same)
Backend: Next.js Server Actions (same)
Database: Supabase Pro → Team plan
- Connection pooling (pgBouncer)
- Read replicas for analytics
- 100GB storage
AI: Anthropic Claude API + Response caching
Cache: Upstash Redis (Vercel KV)
- Cache AI responses (24h TTL)
- Cache user sessions
- Rate limiting
CDN: CloudFlare in front of Vercel (optional)
Monitoring: Vercel Analytics + Sentry
```
**Architecture Pattern:**
```typescript
// lib/cache/redis.ts
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export async function getCachedAIResponse(cacheKey: string) {
return await redis.get(cacheKey);
}
export async function setCachedAIResponse(
cacheKey: string,
response: any,
ttlSeconds: number = 86400 // 24 hours
) {
await redis.setex(cacheKey, ttlSeconds, JSON.stringify(response));
}
// Usage in AI feedback generation
export async function generateFeedback(context: FeedbackContext): Promise<AIFeedback> {
const cacheKey = `feedback:${context.case.id}:${context.student_answer.selected_answer_id}`;
// Try cache first
const cached = await getCachedAIResponse(cacheKey);
if (cached) {
console.log('Cache hit for feedback');
return JSON.parse(cached as string);
}
// Generate new feedback
const feedback = await callClaudeAPI(context);
// Cache for future students
await setCachedAIResponse(cacheKey, feedback);
return feedback;
}
```
**Database Optimizations:**
```sql
-- Partition interactions table by month (reduces query time)
CREATE TABLE interactions_2025_01 PARTITION OF interactions
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE interactions_2025_02 PARTITION OF interactions
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- Indexes for hot queries
CREATE INDEX CONCURRENTLY idx_interactions_user_recent
ON interactions(user_id, created_at DESC)
WHERE created_at > NOW() - INTERVAL '30 days';
-- Materialized view for dashboard stats (refresh every hour)
CREATE MATERIALIZED VIEW user_stats_cache AS
SELECT
user_id,
COUNT(*) as total_cases,
AVG(CASE WHEN is_correct THEN 1.0 ELSE 0.0 END) as success_rate,
MAX(created_at) as last_activity
FROM interactions
GROUP BY user_id;
CREATE UNIQUE INDEX ON user_stats_cache(user_id);
-- Auto-refresh via pg_cron
SELECT cron.schedule('refresh-user-stats', '0 * * * *',
'REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats_cache');
```
**Expected Performance:**
- API response time: <200ms (p95)
- Database query time: <50ms (p95)
- AI response time: 1-3s (depending on Claude API)
- Cache hit rate: 70-80% for common operations
---
### Stage 3: Scale (100k-1M users)
**Monthly Active Users**: 100,000-1,000,000
**Daily Interactions**: 1M-10M
**Infrastructure Cost**: $10,000-30,000/month
**Major Architecture Changes:**
#### 1. **Database Sharding Strategy**
**Shard by User ID** (most queries are user-scoped):
```sql
-- Shard 1: Users with ID hash % 4 = 0
-- Shard 2: Users with ID hash % 4 = 1
-- Shard 3: Users with ID hash % 4 = 2
-- Shard 4: Users with ID hash % 4 = 3
-- Routing logic in application
function getShardForUser(userId: string): number {
const hash = hashUserId(userId);
return hash % 4;
}
// Connection pool per shard
const shardConnections = {
0: createSupabaseClient(SHARD_0_URL),
1: createSupabaseClient(SHARD_1_URL),
2: createSupabaseClient(SHARD_2_URL),
3: createSupabaseClient(SHARD_3_URL),
};
export function getDbForUser(userId: string) {
const shard = getShardForUser(userId);
return shardConnections[shard];
}
```
**Cross-shard queries** (leaderboards, analytics) go to read replicas or data warehouse.
#### 2. **AI Infrastructure Optimization**
**Problem**: Claude API costs scale linearly ($1M+ users = $50k+/month in AI costs)
**Solution**: Multi-tier AI strategy
```typescript
// Tier 1: Pre-computed responses (instant, free)
// For common case + answer combinations (80% of traffic)
const precomputedFeedback = await db
.from('precomputed_feedback')
.select('*')
.eq('case_id', caseId)
.eq('selected_answer', answerId)
.single();
if (precomputedFeedback) return precomputedFeedback;
// Tier 2: Cached responses (fast, cheap)
// For less common combinations (15% of traffic)
const cached = await redis.get(`feedback:${caseId}:${answerId}`);
if (cached) return JSON.parse(cached);
// Tier 3: Real-time AI generation (slow, expensive)
// For rare combinations or premium users (5% of traffic)
const feedback = await generateWithClaude(context);
await redis.setex(`feedback:${caseId}:${answerId}`, 86400, JSON.stringify(feedback));
return feedback;
```
**Cost Impact:**
- Before: 1M API calls/day × $0.003 = $3,000/day = $90,000/month
- After: 50k API calls/day × $0.003 = $150/day = $4,500/month
- **Savings**: $85,500/month (95% reduction)
#### 3. **Background Job Processing**
**Move heavy operations off request path:**
```typescript
// lib/jobs/queue.ts
import { Inngest } from 'inngest';
const inngest = new Inngest({ name: 'MedCards' });
// Heavy operations run async
export const calculateUserMetrics = inngest.createFunction(
{ name: 'Calculate User Metrics' },
{ event: 'user/interaction.created' },
async ({ event }) => {
const userId = event.data.userId;
// Recalculate all user stats
const stats = await computeComprehensiveStats(userId);
// Update database
await db.from('users').update({ progress: stats }).eq('id', userId);
// Check for badge unlocks
await checkBadgeUnlocks(userId, stats);
// Update leaderboards
await updateLeaderboards(userId, stats);
}
);
// Badge unlock notifications
export const notifyBadgeUnlock = inngest.createFunction(
{ name: 'Notify Badge Unlock' },
{ event: 'badge/unlocked' },
async ({ event }) => {
// Send email
// Push notification
// Update UI via WebSocket
}
);
```
**Benefits:**
- API response time: 2s → 200ms
- Better user experience
- Can retry failed jobs
- Scale workers independently
#### 4. **Read/Write Separation**
```typescript
// lib/db/routing.ts
// Write operations → Primary database
export async function writeInteraction(data: InteractionData) {
return await primaryDb.from('interactions').insert(data);
}
// Read operations → Read replicas (distribute load)
const readReplicas = [replicaDb1, replicaDb2, replicaDb3];
let currentReplica = 0;
export async function getUser Interactions(userId: string) {
const db = readReplicas[currentReplica % readReplicas.length];
currentReplica++;
return await db
.from('interactions')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(20);
}
```
#### 5. **CDN & Static Asset Optimization**
```typescript
// next.config.ts
export default {
images: {
loader: 'cloudinary', // Or imgix, cloudflare
domains: ['res.cloudinary.com'],
},
// Serve heavy assets from CDN
assetPrefix: process.env.CDN_URL,
};
```
**Asset Strategy:**
- Case images → CloudFlare R2 (S3-compatible, cheaper)
- User avatars → CloudFlare Images (auto-optimization)
- Video explanations → Mux (video streaming CDN)
---
### Stage 4: Platform (1M+ users)
**Monthly Active Users**: 1M+
**Daily Interactions**: 10M+
**Infrastructure Cost**: $50,000-100,000/month
**Full Microservices Architecture:**
```
┌─────────────────────────────────────────────────────────┐
│ CloudFlare CDN │
└─────────────────────┬───────────────────────────────────┘
┌─────────────┴─────────────┐
│ Load Balancer │
└─────────────┬─────────────┘
┌─────────────┴─────────────────────────────┐
│ │
┌───────▼────────┐ ┌────────▼────────┐
│ Web Frontend │ │ Mobile API │
│ (Vercel Edge) │ │ (Dedicated) │
└───────┬────────┘ └────────┬────────┘
│ │
└─────────────┬───────────────────────────────┘
┌─────────────▼──────────────────────┐
│ API Gateway │
│ (Rate limiting, Auth) │
└─────────────┬──────────────────────┘
┌─────────────┴──────────────────────────────┐
│ │
┌───────▼──────────┐ ┌────────────┐ ┌─────────────▼────────┐
│ User Service │ │ Cache │ │ Case Service │
│ (Supabase) │ │ (Redis) │ │ (Dedicated DB) │
└──────────────────┘ └────────────┘ └──────────────────────┘
│ │
│ ┌─────────────┐ │
└──────────────► AI Service ◄────────────────┘
│ (Claude + │
│ Fine-tune) │
└──────┬───────┘
┌─────────▼──────────┐
│ Analytics Service │
│ (ClickHouse) │
└────────────────────┘
```
**Service Breakdown:**
| Service | Tech | Purpose |
|---------|------|---------|
| User Service | Supabase | User profiles, auth, progress |
| Case Service | Dedicated PostgreSQL | Clinical cases, interactions |
| AI Service | Claude API + Custom models | Feedback, coaching, adaptive |
| Analytics | ClickHouse | Real-time analytics, dashboards |
| Search | Elasticsearch | Case search, user search |
| Notifications | Pusher / Socket.io | Real-time updates |
| Jobs | Temporal | Background processing |
| Cache | Redis Cluster | Multi-layer caching |
---
## 💰 Cost Breakdown by Stage
### Stage 1: MVP (10k users)
```
Vercel Pro: $20/month
Supabase Pro: $25/month
Anthropic API: $300/month (100k AI calls)
Domain + SSL: $15/month
Monitoring: $50/month
──────────────────────────────────
TOTAL: $410/month
Cost per user: $0.041/month
```
### Stage 2: Growth (100k users)
```
Vercel Enterprise: $500/month
Supabase Team: $599/month
Anthropic API: $1,500/month (500k AI calls, 70% cached)
Upstash Redis: $200/month
CloudFlare Pro: $20/month
Sentry: $100/month
──────────────────────────────────
TOTAL: $2,919/month
Cost per user: $0.029/month
```
### Stage 3: Scale (1M users)
```
Vercel Enterprise: $2,000/month
Supabase (4 shards): $2,400/month ($600 each)
Anthropic API: $4,500/month (cached 95%)
Redis Cluster: $1,000/month
CloudFlare: $200/month
Sentry: $500/month
Inngest (jobs): $300/month
Datadog: $800/month
──────────────────────────────────
TOTAL: $11,700/month
Cost per user: $0.012/month
```
**Key Insight**: Cost per user DECREASES as you scale (economies of scale).
---
## 🔥 Performance Targets
### API Response Times (p95)
- **Homepage load**: <500ms
- **Dashboard load**: <800ms
- **Case presentation**: <300ms
- **Submit answer**: <400ms
- **AI feedback**: <2s (with streaming)
- **Chat message**: <500ms (streaming)
### Database Query Times (p95)
- **Simple SELECT**: <10ms
- **Complex JOIN**: <50ms
- **Analytics query**: <200ms
- **Leaderboard**: <100ms (cached)
### Availability
- **Uptime SLA**: 99.9% (8.76 hours downtime/year)
- **Zero-downtime deployments**: Required
- **Disaster recovery**: <15 minute RPO/RTO
---
## 🛡️ Reliability & Monitoring
### Error Budget
```
Monthly Uptime Target: 99.9%
Error Budget: 0.1% = 43 minutes downtime/month
Week 1: 5 minutes → 37 minutes left
Week 2: 10 minutes → 27 minutes left
Week 3: 30 minutes → -3 minutes (EXCEEDED!)
→ Freeze feature releases
→ Focus on stability
→ Root cause analysis
```
### Monitoring Stack
```typescript
// lib/monitoring/metrics.ts
import * as Sentry from '@sentry/nextjs';
import { track } from '@vercel/analytics';
// Track all API calls
export async function monitoredAPICall<T>(
operation: string,
fn: () => Promise<T>
): Promise<T> {
const startTime = Date.now();
try {
const result = await fn();
const duration = Date.now() - startTime;
// Success metrics
track('api_call_success', {
operation,
duration,
});
return result;
} catch (error) {
// Error tracking
Sentry.captureException(error, {
tags: { operation },
extra: { duration: Date.now() - startTime },
});
// Error metrics
track('api_call_error', {
operation,
error: error.message,
});
throw error;
}
}
// Usage
export async function submitAnswer(data: AnswerData) {
return monitoredAPICall('submit_answer', async () => {
// ... actual implementation
});
}
```
### Alerts Configuration
```yaml
alerts:
- name: High Error Rate
condition: error_rate > 5%
window: 5 minutes
severity: critical
notify: pagerduty
- name: Slow API Responses
condition: p95_latency > 2 seconds
window: 10 minutes
severity: warning
notify: slack
- name: Database Connection Pool Exhaustion
condition: available_connections < 10
severity: critical
notify: pagerduty
- name: AI API Rate Limit Approaching
condition: anthropic_remaining_requests < 100
severity: warning
notify: slack
- name: Daily Active Users Drop
condition: dau_vs_yesterday_decrease > 20%
severity: warning
notify: slack
```
---
## 📈 Capacity Planning
### User Growth Projections
```
Month 1: 100 users
Month 3: 1,000 users (10x growth)
Month 6: 10,000 users (10x growth)
Month 12: 50,000 users (5x growth)
Month 18: 150,000 users (3x growth)
Month 24: 500,000 users (3.3x growth)
```
### Infrastructure Scaling Triggers
| Metric | Trigger | Action |
|--------|---------|--------|
| Database CPU | >70% for 1h | Add read replica |
| Database Storage | >80% used | Upgrade plan OR archive old data |
| API Error Rate | >5% for 5min | Scale up serverless OR rollback |
| Redis Memory | >80% used | Upgrade OR implement LRU eviction |
| AI API Costs | >$10k/month | Implement aggressive caching |
### Scaling Checklist
**At 10k users:**
- [ ] Enable Redis caching
- [ ] Add database indexes
- [ ] Set up monitoring
- [ ] Implement rate limiting
**At 50k users:**
- [ ] Add read replicas
- [ ] Implement job queue
- [ ] Aggressive AI response caching
- [ ] CloudFlare Pro
**At 100k users:**
- [ ] Database sharding
- [ ] Microservices architecture
- [ ] Dedicated analytics database
- [ ] Content delivery optimization
---
## 🚀 Deployment Strategy
### Zero-Downtime Deployments
```bash
# Blue-Green Deployment on Vercel
1. Deploy new version to staging
2. Run smoke tests
3. Deploy to production (Vercel handles canary rollout)
4. Monitor error rates for 15 minutes
5. If errors spike: automatic rollback
6. If stable: full rollout
```
### Database Migrations
```typescript
// migrations/0015_add_community_cases.ts
export async function up() {
// Safe migration: additive only
await db.schema
.createTable('community_cases')
.addColumn('id', 'uuid', (col) => col.primaryKey())
.addColumn('created_at', 'timestamp')
// ... other columns
.execute();
}
export async function down() {
// Rollback (but never run in production!)
await db.schema.dropTable('community_cases').execute();
}
```
**Migration Rules:**
1. Never drop columns (deprecate instead)
2. Add new columns as nullable
3. Backfill data async
4. Test on staging with production data snapshot
---
## 🔒 Security at Scale
### Rate Limiting
```typescript
// middleware.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'), // 100 requests per minute
});
export async function middleware(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return new Response('Rate limit exceeded', { status: 429 });
}
return NextResponse.next();
}
```
### DDoS Protection
```
CloudFlare WAF → Vercel → Application
- CloudFlare: Block malicious IPs, rate limit per IP
- Vercel: Edge protection, DDoS mitigation
- Application: User-level rate limits
```
### Data Encryption
```
- At Rest: Supabase encrypts all data (AES-256)
- In Transit: TLS 1.3 everywhere
- Backups: Encrypted, geographically distributed
- Secrets: Managed via Vercel environment variables
```
---
## 📊 Analytics Architecture
### Real-Time Analytics
```sql
-- ClickHouse table for real-time analytics (better than PostgreSQL for OLAP)
CREATE TABLE analytics.interactions (
user_id UUID,
case_id UUID,
is_correct Boolean,
time_to_answer Int32,
created_at DateTime,
specialty String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (created_at, user_id);
-- Fast aggregations
SELECT
specialty,
COUNT(*) as total,
AVG(is_correct) as success_rate
FROM analytics.interactions
WHERE created_at > now() - INTERVAL 7 DAY
GROUP BY specialty;
-- Executes in <50ms on 100M rows
```
### Data Warehouse Strategy
```
Operational DB (PostgreSQL) → CDC → Data Warehouse (ClickHouse)
Analytics Dashboard (Metabase/Looker)
```
---
## 🎯 Summary: Scaling Path
```
MVP (0-10k): Simple stack, manual processes, good enough
Growth (10-100k): Add caching, optimize database, automate
Scale (100k-1M): Sharding, microservices, background jobs
Platform (1M+): Full distribution, dedicated services, ML ops
Philosophy: Scale progressively, not prematurely.
Build what you need TODAY, architect for TOMORROW.
```
**Next Steps**: Implement MVP stack, monitor metrics, scale when triggers hit.
@@ -0,0 +1,988 @@
-- ============================================================================
-- MEDCARDS.AI - Network Effects & Social Features Schema Extension
-- This extends the base schema with community, marketplace, and social features
-- ============================================================================
-- ============================================================================
-- DATA NETWORK EFFECT: Learning from Collective Intelligence
-- ============================================================================
-- Track real-world difficulty vs predicted difficulty
CREATE TABLE case_difficulty_calibration (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
case_id UUID NOT NULL REFERENCES clinical_cases(id) ON DELETE CASCADE,
-- Calibration metrics
actual_difficulty_score NUMERIC(5, 2), -- Based on real user performance
predicted_difficulty_score NUMERIC(5, 2), -- What we thought it would be
difficulty_delta NUMERIC(5, 2), -- How off were we?
sample_size INTEGER NOT NULL, -- Number of interactions used for calculation
confidence_level NUMERIC(3, 2), -- Statistical confidence (0.00-1.00)
-- Performance breakdown
performance_by_level JSONB, -- {"beginner": 0.3, "intermediate": 0.6, "advanced": 0.8}
time_distribution JSONB, -- {"p50": 180, "p75": 240, "p90": 320}
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT valid_confidence CHECK (confidence_level >= 0 AND confidence_level <= 1)
);
CREATE INDEX idx_calibration_case ON case_difficulty_calibration(case_id);
CREATE INDEX idx_calibration_updated ON case_difficulty_calibration(updated_at DESC);
-- Track AI model versions and performance
CREATE TABLE prediction_model_versions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
version TEXT UNIQUE NOT NULL,
deployed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Training data
training_data_size INTEGER NOT NULL,
training_period_start TIMESTAMP WITH TIME ZONE,
training_period_end TIMESTAMP WITH TIME ZONE,
-- Performance metrics
accuracy_metrics JSONB NOT NULL,
/*
{
"case_selection_accuracy": 0.85,
"difficulty_prediction_mae": 0.3,
"time_prediction_mape": 15.2,
"student_success_prediction_auc": 0.78
}
*/
performance_improvement_vs_previous NUMERIC(5, 2), -- Percentage improvement
-- Model metadata
model_architecture TEXT,
hyperparameters JSONB,
notes TEXT,
is_active BOOLEAN DEFAULT false
);
CREATE INDEX idx_model_active ON prediction_model_versions(is_active) WHERE is_active = true;
-- ============================================================================
-- CONTENT NETWORK EFFECT: Community-Contributed Cases
-- ============================================================================
CREATE TABLE community_cases (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Creator
created_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Case content (same structure as clinical_cases)
case_code TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
clinical_presentation TEXT NOT NULL,
patient_data JSONB,
question TEXT NOT NULL,
options JSONB NOT NULL,
correct_answer_id TEXT NOT NULL,
explanation TEXT NOT NULL,
clinical_reasoning TEXT NOT NULL,
key_concepts TEXT[],
differential_diagnosis TEXT[],
-- Classification
specialty TEXT NOT NULL,
subspecialty TEXT,
difficulty_level INTEGER CHECK (difficulty_level BETWEEN 1 AND 5),
clinical_algorithm TEXT,
-- Review status
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'submitted', 'under_review', 'approved', 'rejected', 'needs_revision')),
submitted_at TIMESTAMP WITH TIME ZONE,
reviewed_at TIMESTAMP WITH TIME ZONE,
approved_by_user_id UUID REFERENCES users(id),
-- Community feedback
community_rating NUMERIC(3, 2), -- 0.00 to 5.00
rating_count INTEGER DEFAULT 0,
times_used INTEGER DEFAULT 0,
success_rate NUMERIC(5, 2),
-- Moderation
curator_notes TEXT,
revision_requests TEXT[],
-- Monetization
is_premium BOOLEAN DEFAULT false,
price_credits INTEGER DEFAULT 0,
earnings_generated NUMERIC(10, 2) DEFAULT 0,
-- Quality signals
expert_verified BOOLEAN DEFAULT false,
flagged_count INTEGER DEFAULT 0,
tags TEXT[]
);
CREATE INDEX idx_community_cases_creator ON community_cases(created_by_user_id);
CREATE INDEX idx_community_cases_status ON community_cases(status);
CREATE INDEX idx_community_cases_specialty ON community_cases(specialty) WHERE status = 'approved';
CREATE INDEX idx_community_cases_rating ON community_cases(community_rating DESC) WHERE status = 'approved';
-- Reviews for community cases
CREATE TABLE case_reviews (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
case_id UUID NOT NULL REFERENCES community_cases(id) ON DELETE CASCADE,
reviewer_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Review scores
clinical_accuracy_score INTEGER CHECK (clinical_accuracy_score BETWEEN 1 AND 5),
educational_value_score INTEGER CHECK (educational_value_score BETWEEN 1 AND 5),
clarity_score INTEGER CHECK (clarity_score BETWEEN 1 AND 5),
overall_score NUMERIC(3, 2), -- Calculated average
-- Feedback
review_text TEXT NOT NULL,
strengths TEXT[],
areas_for_improvement TEXT[],
-- Reviewer credibility
is_expert_review BOOLEAN DEFAULT false, -- Verified doctors/professors
reviewer_specialty TEXT,
-- Helpfulness
helpful_count INTEGER DEFAULT 0,
UNIQUE(case_id, reviewer_user_id) -- One review per user per case
);
CREATE INDEX idx_reviews_case ON case_reviews(case_id);
CREATE INDEX idx_reviews_expert ON case_reviews(is_expert_review) WHERE is_expert_review = true;
-- Case quality flags (for moderation)
CREATE TABLE case_flags (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
case_id UUID NOT NULL REFERENCES community_cases(id) ON DELETE CASCADE,
flagged_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
flag_reason TEXT NOT NULL CHECK (flag_reason IN (
'clinical_inaccuracy',
'misleading_information',
'inappropriate_content',
'duplicate',
'poor_quality',
'other'
)),
description TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'resolved', 'dismissed')),
resolution_notes TEXT,
resolved_by_user_id UUID REFERENCES users(id),
resolved_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_flags_case ON case_flags(case_id);
CREATE INDEX idx_flags_status ON case_flags(status) WHERE status = 'pending';
-- ============================================================================
-- SOCIAL NETWORK EFFECT: Study Groups & Peer Learning
-- ============================================================================
CREATE TABLE study_groups (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Group identity
name TEXT NOT NULL,
description TEXT,
created_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Access control
is_public BOOLEAN DEFAULT false,
requires_approval BOOLEAN DEFAULT false,
invite_code TEXT UNIQUE, -- For private groups
member_limit INTEGER DEFAULT 50,
-- Configuration
focus_specialties TEXT[],
target_exam TEXT, -- "REVALIDA 2025", "USP Clínica Médica 2025"
exam_date DATE,
study_schedule JSONB, -- {"monday": ["19:00-21:00"], "saturday": ["09:00-12:00"]}
-- Group stats
total_cases_solved INTEGER DEFAULT 0,
avg_group_success_rate NUMERIC(5, 2),
active_members_count INTEGER DEFAULT 0,
total_study_hours NUMERIC(10, 2) DEFAULT 0,
-- Visibility
is_archived BOOLEAN DEFAULT false,
-- Group culture
group_image_url TEXT,
tags TEXT[]
);
CREATE INDEX idx_groups_public ON study_groups(is_public) WHERE is_public = true AND is_archived = false;
CREATE INDEX idx_groups_creator ON study_groups(created_by_user_id);
CREATE INDEX idx_groups_exam ON study_groups(target_exam) WHERE is_archived = false;
CREATE TABLE study_group_members (
group_id UUID NOT NULL REFERENCES study_groups(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Role
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
-- Member stats
contribution_score INTEGER DEFAULT 0, -- Based on activity and helpfulness
cases_solved_in_group INTEGER DEFAULT 0,
last_active_at TIMESTAMP WITH TIME ZONE,
-- Preferences
notifications_enabled BOOLEAN DEFAULT true,
PRIMARY KEY (group_id, user_id)
);
CREATE INDEX idx_group_members_user ON study_group_members(user_id);
CREATE INDEX idx_group_members_active ON study_group_members(last_active_at DESC);
-- Group activity feed
CREATE TABLE group_activities (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
group_id UUID NOT NULL REFERENCES study_groups(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
activity_type TEXT NOT NULL CHECK (activity_type IN (
'member_joined',
'member_left',
'challenge_created',
'challenge_completed',
'milestone_reached',
'case_recommended',
'discussion_started'
)),
activity_data JSONB, -- Context-specific data
visibility TEXT DEFAULT 'group' CHECK (visibility IN ('group', 'members_only', 'public'))
);
CREATE INDEX idx_activities_group ON group_activities(group_id, created_at DESC);
-- ============================================================================
-- COMPETITIVE FEATURES: Challenges & Leaderboards
-- ============================================================================
CREATE TABLE group_challenges (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
group_id UUID NOT NULL REFERENCES study_groups(id) ON DELETE CASCADE,
created_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Challenge details
title TEXT NOT NULL,
description TEXT,
challenge_type TEXT NOT NULL CHECK (challenge_type IN (
'speed_run', -- Solve X cases as fast as possible
'accuracy_battle', -- Highest success rate wins
'specialty_mastery', -- Focus on specific specialty
'daily_streak', -- Longest streak wins
'total_cases' -- Most cases solved
)),
-- Rules
case_pool UUID[], -- Specific cases OR null for any cases
specialty_filter TEXT,
difficulty_filter INTEGER,
-- Timing
start_time TIMESTAMP WITH TIME ZONE NOT NULL,
end_time TIMESTAMP WITH TIME ZONE NOT NULL,
-- Rewards
prize_type TEXT CHECK (prize_type IN ('badges', 'credits', 'bragging_rights', 'real_prize')),
prize_details JSONB, -- {"credits": 500, "badge_id": "uuid"}
-- Status
status TEXT DEFAULT 'upcoming' CHECK (status IN ('upcoming', 'active', 'completed', 'cancelled')),
-- Stats
participant_count INTEGER DEFAULT 0,
total_cases_solved INTEGER DEFAULT 0
);
CREATE INDEX idx_challenges_group ON group_challenges(group_id);
CREATE INDEX idx_challenges_status ON group_challenges(status, start_time);
CREATE TABLE challenge_participants (
challenge_id UUID NOT NULL REFERENCES group_challenges(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Performance
score INTEGER DEFAULT 0,
cases_solved INTEGER DEFAULT 0,
success_rate NUMERIC(5, 2),
time_spent_seconds INTEGER DEFAULT 0,
rank INTEGER,
-- Completion
completed_at TIMESTAMP WITH TIME ZONE,
PRIMARY KEY (challenge_id, user_id)
);
CREATE INDEX idx_participants_challenge ON challenge_participants(challenge_id, score DESC);
CREATE INDEX idx_participants_user ON challenge_participants(user_id);
-- Global leaderboards
CREATE TABLE leaderboards (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
leaderboard_type TEXT NOT NULL CHECK (leaderboard_type IN (
'global_weekly',
'global_monthly',
'global_all_time',
'specialty_weekly',
'university_weekly',
'study_group'
)),
-- Filters
specialty TEXT, -- For specialty leaderboards
university TEXT, -- For university leaderboards
study_group_id UUID REFERENCES study_groups(id),
-- Period
period_start TIMESTAMP WITH TIME ZONE NOT NULL,
period_end TIMESTAMP WITH TIME ZONE NOT NULL,
-- Rankings (denormalized for performance)
rankings JSONB NOT NULL,
/*
[
{"user_id": "uuid", "username": "João", "score": 9500, "cases_solved": 150, "success_rate": 0.85},
{"user_id": "uuid", "username": "Maria", "score": 9200, "cases_solved": 145, "success_rate": 0.87},
...top 100
]
*/
last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(leaderboard_type, specialty, university, study_group_id, period_start)
);
CREATE INDEX idx_leaderboards_type ON leaderboards(leaderboard_type, period_end DESC);
-- ============================================================================
-- PEER INTERACTIONS: Direct User Connections
-- ============================================================================
CREATE TABLE peer_interactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
from_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
to_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
interaction_type TEXT NOT NULL CHECK (interaction_type IN (
'study_together_request',
'case_recommendation',
'explanation_request',
'kudos', -- "Nice job on that case!"
'challenge_invite',
'mentor_request'
)),
context JSONB, -- Additional data depending on type
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'declined', 'expired')),
response_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_interactions_to_user ON peer_interactions(to_user_id, status);
CREATE INDEX idx_interactions_from_user ON peer_interactions(from_user_id);
-- Study buddy matching preferences
CREATE TABLE study_preferences (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Exam goals
target_exam TEXT,
exam_date DATE,
target_specialty TEXT, -- For residency
-- Learning profile
weak_specialties TEXT[],
strong_specialties TEXT[],
preferred_study_times TEXT[], -- "weekday_mornings", "weekend_afternoons", etc.
study_hours_per_week INTEGER,
-- Personality
study_style TEXT CHECK (study_style IN ('competitive', 'collaborative', 'independent_with_accountability', 'mentor', 'mentee')),
communication_preference TEXT CHECK (communication_preference IN ('chat', 'video', 'async')),
-- Matching
looking_for_buddy BOOLEAN DEFAULT false,
open_to_group_invites BOOLEAN DEFAULT true,
university TEXT,
current_year INTEGER, -- Year of medical school
-- Bio
bio TEXT,
interests TEXT[]
);
CREATE INDEX idx_preferences_looking ON study_preferences(looking_for_buddy) WHERE looking_for_buddy = true;
CREATE INDEX idx_preferences_exam ON study_preferences(target_exam, exam_date);
CREATE TABLE study_buddy_matches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user1_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user2_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Match quality
match_score NUMERIC(3, 2) NOT NULL, -- 0.00 to 1.00
match_reason JSONB NOT NULL,
/*
{
"compatibility_factors": [
"Both preparing for REVALIDA 2025",
"Complementary strengths: You're strong in cardio, they're strong in neuro",
"Similar study schedule preferences"
],
"suggested_first_activity": "Try a cardiology challenge together"
}
*/
-- Status
status TEXT DEFAULT 'suggested' CHECK (status IN ('suggested', 'accepted', 'declined', 'active', 'ended')),
accepted_at TIMESTAMP WITH TIME ZONE,
ended_at TIMESTAMP WITH TIME ZONE,
-- Activity tracking
study_sessions_together INTEGER DEFAULT 0,
cases_solved_together INTEGER DEFAULT 0,
CONSTRAINT different_users CHECK (user1_id != user2_id),
UNIQUE(user1_id, user2_id)
);
CREATE INDEX idx_matches_user1 ON study_buddy_matches(user1_id, status);
CREATE INDEX idx_matches_user2 ON study_buddy_matches(user2_id, status);
-- ============================================================================
-- MARKETPLACE: Two-Sided Market for Content
-- ============================================================================
CREATE TABLE premium_content (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
creator_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Content details
content_type TEXT NOT NULL CHECK (content_type IN (
'case_pack', -- Bundle of cases
'specialty_course', -- Complete specialty review
'exam_simulation', -- Full mock exam
'video_explanations', -- Video content
'study_guide', -- PDF/written guide
'flashcard_deck', -- Spaced repetition cards
'ai_tutor_session' -- 1-on-1 AI tutoring (premium)
)),
title TEXT NOT NULL,
description TEXT NOT NULL,
detailed_description TEXT,
-- Pricing
price_credits INTEGER NOT NULL,
price_reais NUMERIC(10, 2), -- For direct purchase
is_subscription BOOLEAN DEFAULT false, -- Monthly access vs one-time
-- Content metadata
content_metadata JSONB NOT NULL,
/*
{
"case_count": 50,
"specialty": "cardiologia",
"difficulty_range": [3, 5],
"includes_video": true,
"estimated_hours": 10,
"prerequisites": ["Basic cardiology knowledge"],
"learning_objectives": ["Master ECG interpretation", "..."]
}
*/
-- Files/content
content_files JSONB, -- URLs to files in Supabase storage
preview_content JSONB, -- Free preview
-- Performance metrics
purchases_count INTEGER DEFAULT 0,
view_count INTEGER DEFAULT 0,
avg_rating NUMERIC(3, 2),
review_count INTEGER DEFAULT 0,
revenue_generated NUMERIC(10, 2) DEFAULT 0,
-- Quality control
is_verified BOOLEAN DEFAULT false, -- Verified by MedCards team
is_featured BOOLEAN DEFAULT false,
quality_score NUMERIC(3, 2), -- Internal quality metric
-- Status
status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'pending_review', 'published', 'unpublished')),
published_at TIMESTAMP WITH TIME ZONE,
-- SEO
tags TEXT[],
category TEXT
);
CREATE INDEX idx_premium_content_creator ON premium_content(creator_user_id);
CREATE INDEX idx_premium_content_published ON premium_content(status, published_at DESC) WHERE status = 'published';
CREATE INDEX idx_premium_content_featured ON premium_content(is_featured, avg_rating DESC) WHERE is_featured = true;
CREATE INDEX idx_premium_content_category ON premium_content(category, avg_rating DESC) WHERE status = 'published';
CREATE TABLE content_purchases (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
purchased_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content_id UUID NOT NULL REFERENCES premium_content(id) ON DELETE CASCADE,
-- Transaction
price_paid_credits INTEGER,
price_paid_reais NUMERIC(10, 2),
payment_method TEXT, -- 'credits', 'card', 'pix'
-- Access
access_expires_at TIMESTAMP WITH TIME ZONE, -- For subscriptions
-- Engagement
last_accessed_at TIMESTAMP WITH TIME ZONE,
completion_percentage NUMERIC(5, 2) DEFAULT 0,
-- Satisfaction
rated BOOLEAN DEFAULT false,
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
review_text TEXT,
UNIQUE(user_id, content_id) -- One purchase per user per content
);
CREATE INDEX idx_purchases_user ON content_purchases(user_id);
CREATE INDEX idx_purchases_content ON content_purchases(content_id);
CREATE INDEX idx_purchases_recent ON content_purchases(purchased_at DESC);
-- Creator profiles
CREATE TABLE creator_profiles (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Verification
is_verified_educator BOOLEAN DEFAULT false,
verified_at TIMESTAMP WITH TIME ZONE,
credentials TEXT, -- "Médico Residente R3 Cardiologia HC-USP"
credentials_verified BOOLEAN DEFAULT false,
-- Profile
display_name TEXT NOT NULL,
bio TEXT,
profile_image_url TEXT,
specialty TEXT,
institution TEXT,
-- Social
website_url TEXT,
twitter_handle TEXT,
linkedin_url TEXT,
-- Creator stats
total_content_created INTEGER DEFAULT 0,
total_revenue_earned NUMERIC(10, 2) DEFAULT 0,
total_students_reached INTEGER DEFAULT 0,
follower_count INTEGER DEFAULT 0,
avg_content_rating NUMERIC(3, 2),
-- Payout
payout_method TEXT CHECK (payout_method IN ('bank_transfer', 'pix', 'paypal')),
payout_details JSONB, -- Encrypted sensitive data
minimum_payout_threshold NUMERIC(10, 2) DEFAULT 100.00,
-- Status
is_active BOOLEAN DEFAULT true,
terms_accepted_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_creators_verified ON creator_profiles(is_verified_educator) WHERE is_verified_educator = true;
CREATE INDEX idx_creators_revenue ON creator_profiles(total_revenue_earned DESC);
CREATE TABLE creator_followers (
follower_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
creator_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
followed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
notifications_enabled BOOLEAN DEFAULT true,
PRIMARY KEY (follower_user_id, creator_user_id)
);
CREATE INDEX idx_followers_creator ON creator_followers(creator_user_id);
CREATE INDEX idx_followers_user ON creator_followers(follower_user_id);
-- Payout tracking
CREATE TABLE creator_payouts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
creator_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Payout details
amount NUMERIC(10, 2) NOT NULL,
currency TEXT DEFAULT 'BRL',
period_start TIMESTAMP WITH TIME ZONE NOT NULL,
period_end TIMESTAMP WITH TIME ZONE NOT NULL,
-- Transaction
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
payout_method TEXT NOT NULL,
transaction_id TEXT,
processed_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
-- Breakdown
revenue_breakdown JSONB -- Details of what generated this revenue
);
CREATE INDEX idx_payouts_creator ON creator_payouts(creator_user_id, created_at DESC);
CREATE INDEX idx_payouts_status ON creator_payouts(status) WHERE status IN ('pending', 'processing');
-- ============================================================================
-- COMMUNITY FORUM
-- ============================================================================
CREATE TABLE forum_categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT UNIQUE NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT,
icon_emoji TEXT,
sort_order INTEGER DEFAULT 0,
post_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true
);
INSERT INTO forum_categories (name, slug, description, icon_emoji, sort_order) VALUES
('Discussão de Casos', 'case-discussion', 'Discuta casos clínicos específicos', '🩺', 1),
('Dicas de Estudo', 'study-tips', 'Compartilhe estratégias e métodos de estudo', '📚', 2),
('Estratégias de Prova', 'exam-strategies', 'Táticas para diferentes provas de residência', '✍️', 3),
('Dúvidas Clínicas', 'clinical-questions', 'Tire dúvidas sobre medicina', '', 4),
('Motivação', 'motivation', 'Apoio e motivação durante a jornada', '💪', 5),
('Anúncios', 'announcements', 'Novidades da plataforma', '📢', 6);
CREATE TABLE forum_posts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
category_id UUID NOT NULL REFERENCES forum_categories(id),
-- Content
title TEXT NOT NULL,
content TEXT NOT NULL,
-- Context
related_case_id UUID REFERENCES clinical_cases(id),
related_specialty TEXT,
tags TEXT[],
-- Engagement
view_count INTEGER DEFAULT 0,
upvote_count INTEGER DEFAULT 0,
comment_count INTEGER DEFAULT 0,
-- Status
is_pinned BOOLEAN DEFAULT false,
is_locked BOOLEAN DEFAULT false,
is_solved BOOLEAN DEFAULT false, -- For questions
accepted_answer_id UUID, -- For questions
-- Moderation
is_flagged BOOLEAN DEFAULT false,
flag_count INTEGER DEFAULT 0
);
CREATE INDEX idx_posts_category ON forum_posts(category_id, created_at DESC);
CREATE INDEX idx_posts_user ON forum_posts(user_id);
CREATE INDEX idx_posts_popular ON forum_posts(upvote_count DESC, created_at DESC);
CREATE INDEX idx_posts_case ON forum_posts(related_case_id) WHERE related_case_id IS NOT NULL;
CREATE TABLE forum_comments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
post_id UUID NOT NULL REFERENCES forum_posts(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
parent_comment_id UUID REFERENCES forum_comments(id), -- For threaded replies
content TEXT NOT NULL,
-- Engagement
upvote_count INTEGER DEFAULT 0,
is_accepted_answer BOOLEAN DEFAULT false,
-- Quality signals
is_expert_answer BOOLEAN DEFAULT false, -- From verified educator/doctor
is_edited BOOLEAN DEFAULT false,
edited_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_comments_post ON forum_comments(post_id, created_at);
CREATE INDEX idx_comments_user ON forum_comments(user_id);
CREATE INDEX idx_comments_parent ON forum_comments(parent_comment_id) WHERE parent_comment_id IS NOT NULL;
CREATE TABLE forum_votes (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Polymorphic: can vote on posts or comments
votable_type TEXT NOT NULL CHECK (votable_type IN ('post', 'comment')),
votable_id UUID NOT NULL,
vote_value INTEGER NOT NULL CHECK (vote_value IN (-1, 1)), -- -1 downvote, 1 upvote
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (user_id, votable_type, votable_id)
);
CREATE INDEX idx_votes_votable ON forum_votes(votable_type, votable_id);
-- ============================================================================
-- FUNCTIONS & TRIGGERS FOR NETWORK EFFECTS
-- ============================================================================
-- Update group stats when member activity happens
CREATE OR REPLACE FUNCTION update_group_stats()
RETURNS TRIGGER AS $$
BEGIN
-- Update active member count
UPDATE study_groups
SET active_members_count = (
SELECT COUNT(*)
FROM study_group_members
WHERE group_id = NEW.group_id
AND last_active_at > NOW() - INTERVAL '7 days'
)
WHERE id = NEW.group_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_group_stats
AFTER INSERT OR UPDATE ON study_group_members
FOR EACH ROW
EXECUTE FUNCTION update_group_stats();
-- Update creator stats when content is purchased
CREATE OR REPLACE FUNCTION update_creator_stats()
RETURNS TRIGGER AS $$
BEGIN
UPDATE creator_profiles
SET
total_revenue_earned = total_revenue_earned + COALESCE(NEW.price_paid_reais, 0),
total_students_reached = (
SELECT COUNT(DISTINCT user_id)
FROM content_purchases
WHERE content_id IN (
SELECT id FROM premium_content WHERE creator_user_id = (
SELECT creator_user_id FROM premium_content WHERE id = NEW.content_id
)
)
)
WHERE user_id = (
SELECT creator_user_id FROM premium_content WHERE id = NEW.content_id
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_creator_stats
AFTER INSERT ON content_purchases
FOR EACH ROW
EXECUTE FUNCTION update_creator_stats();
-- Update forum post comment count
CREATE OR REPLACE FUNCTION update_post_comment_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE forum_posts
SET comment_count = comment_count + 1
WHERE id = NEW.post_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE forum_posts
SET comment_count = comment_count - 1
WHERE id = OLD.post_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_post_comment_count
AFTER INSERT OR DELETE ON forum_comments
FOR EACH ROW
EXECUTE FUNCTION update_post_comment_count();
-- ============================================================================
-- ROW LEVEL SECURITY POLICIES
-- ============================================================================
-- Community cases: Anyone can read approved, only creator can edit draft
ALTER TABLE community_cases ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view approved community cases" ON community_cases
FOR SELECT USING (status = 'approved' OR created_by_user_id = auth.uid());
CREATE POLICY "Users can create own community cases" ON community_cases
FOR INSERT WITH CHECK (created_by_user_id = auth.uid());
CREATE POLICY "Users can update own draft cases" ON community_cases
FOR UPDATE USING (created_by_user_id = auth.uid() AND status IN ('draft', 'needs_revision'));
-- Study groups: Members can view, admins can edit
ALTER TABLE study_groups ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view public groups" ON study_groups
FOR SELECT USING (
is_public = true
OR id IN (
SELECT group_id FROM study_group_members WHERE user_id = auth.uid()
)
);
CREATE POLICY "Members can view their groups" ON study_groups
FOR SELECT USING (
id IN (SELECT group_id FROM study_group_members WHERE user_id = auth.uid())
);
-- Marketplace: Buyers can see purchased content
ALTER TABLE premium_content ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view published premium content" ON premium_content
FOR SELECT USING (status = 'published' OR creator_user_id = auth.uid());
ALTER TABLE content_purchases ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own purchases" ON content_purchases
FOR SELECT USING (user_id = auth.uid());
-- Forum: Public read, authenticated write
ALTER TABLE forum_posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view forum posts" ON forum_posts
FOR SELECT USING (true);
CREATE POLICY "Authenticated users can create posts" ON forum_posts
FOR INSERT WITH CHECK (auth.role() = 'authenticated' AND user_id = auth.uid());
CREATE POLICY "Users can update own posts" ON forum_posts
FOR UPDATE USING (user_id = auth.uid());
ALTER TABLE forum_comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view comments" ON forum_comments
FOR SELECT USING (true);
CREATE POLICY "Authenticated users can comment" ON forum_comments
FOR INSERT WITH CHECK (auth.role() = 'authenticated' AND user_id = auth.uid());
-- ============================================================================
-- ANALYTICS VIEWS (Materialized for performance)
-- ============================================================================
-- Daily network effect metrics
CREATE MATERIALIZED VIEW network_metrics_daily AS
SELECT
DATE(created_at) as date,
COUNT(DISTINCT user_id) as daily_active_users,
COUNT(*) as total_interactions,
-- Social metrics
(SELECT COUNT(*) FROM study_group_members WHERE DATE(joined_at) = DATE(i.created_at)) as new_group_joins,
(SELECT COUNT(*) FROM peer_interactions WHERE DATE(created_at) = DATE(i.created_at)) as peer_interactions_count,
-- Content metrics
(SELECT COUNT(*) FROM community_cases WHERE DATE(submitted_at) = DATE(i.created_at)) as community_cases_submitted,
(SELECT COUNT(*) FROM content_purchases WHERE DATE(purchased_at) = DATE(i.created_at)) as marketplace_purchases,
-- Engagement depth
AVG(time_to_answer_seconds) as avg_time_per_case,
AVG(CASE WHEN is_correct THEN 1.0 ELSE 0.0 END) as platform_success_rate
FROM interactions i
GROUP BY DATE(created_at);
CREATE UNIQUE INDEX ON network_metrics_daily(date);
-- Refresh daily (run as cron job)
-- SELECT cron.schedule('refresh-network-metrics', '0 2 * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY network_metrics_daily');
-- ============================================================================
-- SAMPLE QUERIES FOR PRODUCT ANALYTICS
-- ============================================================================
COMMENT ON TABLE network_metrics_daily IS 'Sample query: SELECT * FROM network_metrics_daily WHERE date > NOW() - INTERVAL ''30 days'' ORDER BY date;';
COMMENT ON TABLE study_groups IS '
-- Find most active study groups
SELECT
sg.name,
sg.active_members_count,
sg.total_cases_solved,
sg.avg_group_success_rate
FROM study_groups sg
WHERE sg.is_archived = false
ORDER BY sg.total_cases_solved DESC
LIMIT 10;
';
COMMENT ON TABLE premium_content IS '
-- Top selling marketplace content
SELECT
pc.title,
cp.display_name as creator,
pc.purchases_count,
pc.avg_rating,
pc.revenue_generated
FROM premium_content pc
JOIN creator_profiles cp ON pc.creator_user_id = cp.user_id
WHERE pc.status = ''published''
ORDER BY pc.revenue_generated DESC
LIMIT 10;
';