This commit is contained in:
2026-02-03 21:58:25 +01:00
parent 3019bcaf1a
commit 959dad3fec
14 changed files with 1477 additions and 1 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Dependencies
node_modules/
__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
env/
.env
# Build outputs
.next/
out/
build/
dist/
*.egg-info/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Environment files
.env
.env.local
.env.*.local
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Testing
coverage/
.coverage
htmlcov/
# Misc
*.pem

268
README.md
View File

@@ -1,2 +1,268 @@
# hack-nation template # 🚀 Hack Nation 2026 - Full-Stack Template
A comprehensive hackathon template with **Next.js**, **FastAPI**, **Supabase**, and **AI integration**.
## 📁 Project Structure
```
hack-nation/
├── frontend/ # Next.js 14 + React
│ ├── src/
│ │ ├── app/ # App Router pages
│ │ │ ├── layout.tsx # Root layout
│ │ │ ├── page.tsx # Home page
│ │ │ ├── login/ # Auth page
│ │ │ ├── dashboard/ # Protected page
│ │ │ └── demo/ # Feature demos
│ │ ├── components/ # React components
│ │ │ ├── ui/ # Shadcn/ui components
│ │ │ └── ... # Custom components
│ │ └── lib/ # Utilities & services
│ │ ├── supabase/ # Supabase client & provider
│ │ └── api.ts # Backend API client
│ └── package.json
├── backend/ # FastAPI (Python)
│ ├── main.py # FastAPI app entry
│ ├── config.py # Environment configuration
│ ├── routers/ # API route handlers
│ │ ├── ai.py # AI generation endpoints
│ │ ├── projects.py # CRUD example
│ │ └── health.py # Health checks
│ └── services/ # Business logic
│ ├── ai_service.py # AI provider integration
│ └── database.py # Supabase helpers
└── README.md
```
## 🛠 Tech Stack
| Layer | Technology | Why? |
|-------|------------|------|
| **Frontend** | Next.js 14 | React framework with App Router, SSR, and great DX |
| **UI Components** | Shadcn/ui | Beautiful, accessible, customizable components |
| **Styling** | Tailwind CSS | Utility-first CSS, rapid development |
| **Backend** | FastAPI | Fast Python API, auto docs, async support |
| **Database** | Supabase | Postgres + Auth + Realtime, generous free tier |
| **AI** | OpenAI/Anthropic | GPT-4 or Claude for AI features |
## 🚀 Quick Start
### 1. Clone & Install
```bash
# Clone the repo (or use this template)
cd hack-nation
# Install frontend dependencies
cd frontend
npm install
# Install backend dependencies
cd ../backend
pip install -r requirements.txt
```
### 2. Configure Environment
```bash
# Frontend
cd frontend
cp .env.local.example .env.local
# Edit .env.local with your Supabase keys
# Backend
cd ../backend
cp .env.example .env
# Edit .env with your API keys
```
### 3. Run Development Servers
```bash
# Terminal 1: Frontend (localhost:3000)
cd frontend
npm run dev
# Terminal 2: Backend (localhost:8000)
cd backend
python run.py
```
### 4. View Your App
- **Frontend**: http://localhost:3000
- **Backend API**: http://localhost:8000
- **API Docs**: http://localhost:8000/docs
## ⚙️ Configuration
### Supabase Setup
1. Create a project at [supabase.com](https://supabase.com)
2. Go to Settings → API
3. Copy the **URL** and **anon key** to frontend `.env.local`
4. Copy the **URL** and **service role key** to backend `.env`
### AI Setup
1. Get an API key from [OpenAI](https://platform.openai.com) or [Anthropic](https://anthropic.com)
2. Add to backend `.env`:
```
OPENAI_API_KEY=sk-your-key-here
AI_PROVIDER=openai
AI_MODEL=gpt-3.5-turbo
```
## 📚 Key Concepts
### Frontend Architecture
```
┌─────────────────────────────────────────┐
│ layout.tsx │
│ ┌────────────────────────────────────┐ │
│ │ Providers │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Navigation │ │ │
│ │ ├──────────────────────────────┤ │ │
│ │ │ page.tsx │ │ │
│ │ │ (Route-specific content) │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
- **layout.tsx**: Wraps all pages, contains providers
- **page.tsx**: Individual page content
- **Providers**: Context for auth, theme, etc.
### Backend Architecture
```
Request → Router → Service → Response
│ │
│ └── AI Service / Database
└── Validates input with Pydantic
```
- **Routers**: Handle HTTP requests, validate input
- **Services**: Business logic, external integrations
- **Config**: All settings from environment variables
### Data Flow
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Frontend │────▶│ Backend │────▶│ AI │
│ (Next.js)│◀────│ (FastAPI)│◀────│ Provider │
└──────────┘ └──────────┘ └──────────┘
│ │
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ Supabase │
│ (Database, Auth, Realtime, Storage) │
└─────────────────────────────────────────┘
```
## 🎨 Adding Shadcn/ui Components
```bash
# Initialize shadcn (first time only)
cd frontend
npx shadcn-ui@latest init
# Add components as needed
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
```
## 🔌 API Endpoints
### AI Endpoints
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/ai/generate` | Generate text from prompt |
| POST | `/api/ai/chat` | Chat with message history |
| POST | `/api/ai/stream` | Stream AI response |
| GET | `/api/ai/models` | List available models |
### Project Endpoints (Example CRUD)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/projects` | List all projects |
| POST | `/api/projects` | Create project |
| GET | `/api/projects/:id` | Get project |
| PATCH | `/api/projects/:id` | Update project |
| DELETE | `/api/projects/:id` | Delete project |
## 🗄 Database Schema (Supabase)
Run these SQL commands in Supabase SQL Editor:
```sql
-- Projects table
CREATE TABLE projects (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Enable Row Level Security
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Users can only access their own projects
CREATE POLICY "Users can CRUD own projects"
ON projects FOR ALL
USING (auth.uid() = user_id);
```
## 🚢 Deployment
### Frontend (Vercel)
1. Push to GitHub
2. Import project in [Vercel](https://vercel.com)
3. Add environment variables
4. Deploy!
### Backend (Railway/Render)
1. Push to GitHub
2. Import in [Railway](https://railway.app) or [Render](https://render.com)
3. Set environment variables
4. Deploy!
### Supabase
Already hosted! Just keep your project running.
## 💡 Hackathon Tips
1. **Start with the AI demo** - It already works!
2. **Use Shadcn/ui components** - Don't build from scratch
3. **Leverage Supabase** - Auth and DB are ready
4. **Keep it simple** - One cool feature > many half-done ones
5. **Test the happy path** - Edge cases can wait
## 📖 Learn More
- [Next.js Docs](https://nextjs.org/docs)
- [FastAPI Docs](https://fastapi.tiangolo.com)
- [Supabase Docs](https://supabase.com/docs)
- [Shadcn/ui Components](https://ui.shadcn.com)
- [OpenAI API](https://platform.openai.com/docs)
---
Built for **Hack Nation 2026** 🏆 template

43
backend/.env.example Normal file
View File

@@ -0,0 +1,43 @@
# =============================================================================
# ENVIRONMENT VARIABLES
# =============================================================================
# Copy this file to .env and fill in your values
# NEVER commit .env to version control!
# -----------------------------------------------------------------------------
# SERVER CONFIGURATION
# -----------------------------------------------------------------------------
PORT=8000
ENVIRONMENT=development
# -----------------------------------------------------------------------------
# SUPABASE CONFIGURATION
# -----------------------------------------------------------------------------
# Get these from: https://app.supabase.com/project/_/settings/api
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your-service-role-key-here
# Note: Use SERVICE KEY (not anon key) for backend - has admin access
# -----------------------------------------------------------------------------
# AI PROVIDER CONFIGURATION
# -----------------------------------------------------------------------------
# Choose your AI provider and add the appropriate key
# OpenAI (GPT-4, GPT-3.5)
OPENAI_API_KEY=sk-your-openai-key-here
# Anthropic (Claude)
# ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
# Default AI provider to use: "openai" or "anthropic"
AI_PROVIDER=openai
# Default model to use
AI_MODEL=gpt-3.5-turbo
# Other options: gpt-4, gpt-4-turbo, claude-3-opus-20240229, claude-3-sonnet-20240229
# -----------------------------------------------------------------------------
# CORS CONFIGURATION
# -----------------------------------------------------------------------------
# Comma-separated list of allowed origins
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000

83
backend/config.py Normal file
View File

@@ -0,0 +1,83 @@
"""
=============================================================================
CONFIGURATION MODULE
=============================================================================
This module handles all configuration using Pydantic Settings.
Benefits of using Pydantic Settings:
- Type validation for environment variables
- Auto-loading from .env files
- IDE autocomplete and type hints
- Clear documentation of all required config
Usage:
from config import settings
print(settings.OPENAI_API_KEY)
"""
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
"""
Application settings loaded from environment variables.
All settings can be overridden via:
1. Environment variables
2. .env file in the backend directory
"""
# -------------------------------------------------------------------------
# Server Configuration
# -------------------------------------------------------------------------
PORT: int = 8000
ENVIRONMENT: str = "development" # "development", "staging", "production"
# -------------------------------------------------------------------------
# Supabase Configuration
# -------------------------------------------------------------------------
SUPABASE_URL: str = ""
SUPABASE_SERVICE_KEY: str = "" # Service key for admin access
# -------------------------------------------------------------------------
# AI Configuration
# -------------------------------------------------------------------------
OPENAI_API_KEY: str = ""
ANTHROPIC_API_KEY: str = ""
AI_PROVIDER: str = "openai" # "openai" or "anthropic"
AI_MODEL: str = "gpt-3.5-turbo"
# -------------------------------------------------------------------------
# CORS Configuration
# -------------------------------------------------------------------------
CORS_ORIGINS: str = "http://localhost:3000" # Comma-separated list
@property
def cors_origins_list(self) -> list[str]:
"""Parse CORS_ORIGINS string into a list."""
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
class Config:
# Load from .env file
env_file = ".env"
env_file_encoding = "utf-8"
# Make field names case-insensitive
case_sensitive = False
# Use lru_cache to create a singleton settings instance
@lru_cache
def get_settings() -> Settings:
"""
Get cached settings instance.
Using lru_cache ensures we only read the .env file once,
improving performance.
"""
return Settings()
# Export settings instance for easy import
settings = get_settings()

130
backend/main.py Normal file
View File

@@ -0,0 +1,130 @@
"""
=============================================================================
FASTAPI MAIN APPLICATION
=============================================================================
This is the entry point for the FastAPI backend.
FastAPI Features:
- Automatic API documentation (Swagger UI at /docs)
- Request validation with Pydantic
- Async support for high performance
- Easy dependency injection
To run the server:
uvicorn main:app --reload --port 8000
Or use the provided run script:
python run.py
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
# Import configuration
from config import settings
# Import routers (API endpoints grouped by feature)
from routers import ai, projects, health
# =============================================================================
# APPLICATION LIFECYCLE
# =============================================================================
@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Manage application lifecycle events.
This runs code on startup (before yield) and shutdown (after yield).
Great for:
- Initializing database connections
- Loading ML models
- Setting up caches
"""
# Startup
print("🚀 Starting Hack Nation Backend...")
print(f"📍 Environment: {settings.ENVIRONMENT}")
print(f"🤖 AI Provider: {settings.AI_PROVIDER} ({settings.AI_MODEL})")
yield # Application runs here
# Shutdown
print("👋 Shutting down...")
# =============================================================================
# CREATE APPLICATION
# =============================================================================
app = FastAPI(
title="Hack Nation API",
description="""
Backend API for Hack Nation hackathon template.
Features:
- AI text generation (OpenAI / Anthropic)
- Supabase database integration
- User authentication
""",
version="1.0.0",
lifespan=lifespan,
)
# =============================================================================
# MIDDLEWARE
# =============================================================================
# CORS - Allow frontend to call API
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list, # Origins allowed to call API
allow_credentials=True, # Allow cookies/auth headers
allow_methods=["*"], # Allow all HTTP methods
allow_headers=["*"], # Allow all headers
)
# =============================================================================
# ROUTERS
# =============================================================================
# Include routers with prefixes
# Each router handles a specific feature area
app.include_router(
health.router,
prefix="/api",
tags=["Health"],
)
app.include_router(
ai.router,
prefix="/api/ai",
tags=["AI"],
)
app.include_router(
projects.router,
prefix="/api/projects",
tags=["Projects"],
)
# =============================================================================
# ROOT ENDPOINT
# =============================================================================
@app.get("/")
async def root():
"""
Root endpoint - useful for health checks and API info.
"""
return {
"name": "Hack Nation API",
"version": "1.0.0",
"docs": "/docs",
"health": "/api/health",
}

22
backend/requirements.txt Normal file
View File

@@ -0,0 +1,22 @@
# =============================================================================
# FASTAPI BACKEND - REQUIREMENTS
# =============================================================================
# Install with: pip install -r requirements.txt
# Or: pip install fastapi uvicorn openai python-dotenv supabase pydantic
# Web Framework
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
# AI Integration
openai>=1.50.0 # OpenAI API client
anthropic>=0.39.0 # Anthropic Claude API (alternative)
# Database (Supabase)
supabase>=2.10.0 # Supabase Python client
# Utilities
python-dotenv>=1.0.0 # Load .env files
pydantic>=2.9.0 # Data validation
pydantic-settings>=2.5.0 # Settings management
httpx>=0.27.0 # Async HTTP client

View File

@@ -0,0 +1,10 @@
"""
Routers Package
This package contains all API route handlers organized by feature.
Each router is responsible for a specific domain of the application.
"""
from . import ai, projects, health
__all__ = ["ai", "projects", "health"]

171
backend/routers/ai.py Normal file
View File

@@ -0,0 +1,171 @@
"""
=============================================================================
AI ROUTER
=============================================================================
This router handles all AI-related endpoints.
Features:
- Text generation with OpenAI or Anthropic
- Streaming responses for real-time output
- Multiple AI models support
Architecture:
- Endpoints are thin - they just validate input and return output
- Business logic is in the services module
- This separation makes testing easier
"""
from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from typing import Optional
# Import our AI service
from services.ai_service import generate_text, stream_text, chat_completion
router = APIRouter()
# =============================================================================
# REQUEST/RESPONSE MODELS
# =============================================================================
class GenerateRequest(BaseModel):
"""Request body for text generation."""
prompt: str = Field(..., min_length=1, max_length=10000)
max_tokens: Optional[int] = Field(default=500, ge=1, le=4000)
temperature: Optional[float] = Field(default=0.7, ge=0, le=2)
class GenerateResponse(BaseModel):
"""Response from text generation."""
text: str
model: str
usage: Optional[dict] = None
class ChatMessage(BaseModel):
"""A single message in a chat conversation."""
role: str = Field(..., pattern="^(user|assistant|system)$")
content: str = Field(..., min_length=1)
class ChatRequest(BaseModel):
"""Request body for chat completion."""
messages: list[ChatMessage] = Field(..., min_items=1)
max_tokens: Optional[int] = Field(default=500, ge=1, le=4000)
temperature: Optional[float] = Field(default=0.7, ge=0, le=2)
class ChatResponse(BaseModel):
"""Response from chat completion."""
message: ChatMessage
model: str
# =============================================================================
# ENDPOINTS
# =============================================================================
@router.post("/generate", response_model=GenerateResponse)
async def generate_endpoint(request: GenerateRequest):
"""
Generate text from a prompt.
This is a simple completion endpoint - give it a prompt,
get back generated text.
Example:
POST /api/ai/generate
{"prompt": "Write a haiku about coding"}
"""
try:
result = await generate_text(
prompt=request.prompt,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
return GenerateResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/chat", response_model=ChatResponse)
async def chat_endpoint(request: ChatRequest):
"""
Chat completion with message history.
Send a list of messages (conversation history) and get
the assistant's response.
Example:
POST /api/ai/chat
{
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
]
}
"""
try:
# Convert Pydantic models to dicts
messages = [m.model_dump() for m in request.messages]
result = await chat_completion(
messages=messages,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
return ChatResponse(
message=ChatMessage(**result["message"]),
model=result["model"],
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/stream")
async def stream_endpoint(request: GenerateRequest):
"""
Stream generated text in real-time.
Uses Server-Sent Events (SSE) to stream the response
token by token. Great for chat interfaces!
The frontend can read this with:
const response = await fetch("/api/ai/stream", {...});
const reader = response.body.getReader();
"""
try:
return StreamingResponse(
stream_text(
prompt=request.prompt,
max_tokens=request.max_tokens,
temperature=request.temperature,
),
media_type="text/event-stream",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/models")
async def list_models():
"""
List available AI models.
Useful for letting users choose which model to use.
"""
return {
"openai": [
{"id": "gpt-4-turbo", "name": "GPT-4 Turbo", "description": "Most capable model"},
{"id": "gpt-4", "name": "GPT-4", "description": "High quality responses"},
{"id": "gpt-3.5-turbo", "name": "GPT-3.5 Turbo", "description": "Fast and efficient"},
],
"anthropic": [
{"id": "claude-3-opus-20240229", "name": "Claude 3 Opus", "description": "Most capable"},
{"id": "claude-3-sonnet-20240229", "name": "Claude 3 Sonnet", "description": "Balanced"},
{"id": "claude-3-haiku-20240307", "name": "Claude 3 Haiku", "description": "Fast"},
],
}

62
backend/routers/health.py Normal file
View File

@@ -0,0 +1,62 @@
"""
=============================================================================
HEALTH CHECK ROUTER
=============================================================================
Simple health check endpoints for monitoring and deployment.
These are useful for:
- Load balancer health checks
- Kubernetes readiness probes
- Monitoring systems
"""
from fastapi import APIRouter
from config import settings
router = APIRouter()
@router.get("/health")
async def health_check():
"""
Basic health check endpoint.
Returns 200 if the server is running.
"""
return {
"status": "healthy",
"environment": settings.ENVIRONMENT,
}
@router.get("/health/detailed")
async def detailed_health_check():
"""
Detailed health check with service status.
Useful for debugging connection issues.
"""
# Check AI provider configuration
ai_configured = bool(
settings.OPENAI_API_KEY or settings.ANTHROPIC_API_KEY
)
# Check Supabase configuration
supabase_configured = bool(
settings.SUPABASE_URL and settings.SUPABASE_SERVICE_KEY
)
return {
"status": "healthy",
"environment": settings.ENVIRONMENT,
"services": {
"ai": {
"configured": ai_configured,
"provider": settings.AI_PROVIDER,
"model": settings.AI_MODEL,
},
"database": {
"configured": supabase_configured,
"provider": "supabase",
},
},
}

191
backend/routers/projects.py Normal file
View File

@@ -0,0 +1,191 @@
"""
=============================================================================
PROJECTS ROUTER
=============================================================================
Example CRUD endpoints for managing projects.
This demonstrates:
- Supabase database integration
- CRUD operations (Create, Read, Update, Delete)
- Request validation with Pydantic
- Error handling patterns
You can adapt this pattern for any resource in your app.
"""
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# Import Supabase client
from services.database import get_supabase_client
router = APIRouter()
# =============================================================================
# REQUEST/RESPONSE MODELS
# =============================================================================
class ProjectCreate(BaseModel):
"""Data required to create a new project."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = Field(default="", max_length=1000)
class ProjectUpdate(BaseModel):
"""Data that can be updated on a project."""
name: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = Field(None, max_length=1000)
class Project(BaseModel):
"""Full project model (database representation)."""
id: str
name: str
description: str
user_id: str
created_at: datetime
# =============================================================================
# ENDPOINTS
# =============================================================================
@router.get("")
async def list_projects(
authorization: str = Header(None, description="Bearer token from Supabase")
):
"""
List all projects for the authenticated user.
Requires authentication - pass the Supabase access token
in the Authorization header.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
# Get Supabase client
supabase = get_supabase_client()
# Extract user from token (Supabase handles this)
# In production, you'd verify the JWT properly
# Query projects
response = supabase.table("projects").select("*").execute()
return response.data
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("", response_model=Project)
async def create_project(
project: ProjectCreate,
authorization: str = Header(None),
):
"""
Create a new project.
Example:
POST /api/projects
{"name": "My AI App", "description": "An awesome AI project"}
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
supabase = get_supabase_client()
# In production, extract user_id from JWT
# For demo, we'll use a placeholder
user_id = "demo-user-id"
response = supabase.table("projects").insert({
"name": project.name,
"description": project.description or "",
"user_id": user_id,
}).execute()
return response.data[0]
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{project_id}", response_model=Project)
async def get_project(project_id: str):
"""
Get a specific project by ID.
"""
try:
supabase = get_supabase_client()
response = supabase.table("projects").select("*").eq("id", project_id).single().execute()
if not response.data:
raise HTTPException(status_code=404, detail="Project not found")
return response.data
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/{project_id}", response_model=Project)
async def update_project(
project_id: str,
project: ProjectUpdate,
authorization: str = Header(None),
):
"""
Update a project.
Only send the fields you want to update.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
supabase = get_supabase_client()
# Build update dict with only provided fields
update_data = project.model_dump(exclude_unset=True)
if not update_data:
raise HTTPException(status_code=400, detail="No fields to update")
response = supabase.table("projects").update(update_data).eq("id", project_id).execute()
if not response.data:
raise HTTPException(status_code=404, detail="Project not found")
return response.data[0]
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{project_id}")
async def delete_project(
project_id: str,
authorization: str = Header(None),
):
"""
Delete a project.
"""
if not authorization:
raise HTTPException(status_code=401, detail="Authorization required")
try:
supabase = get_supabase_client()
response = supabase.table("projects").delete().eq("id", project_id).execute()
return {"message": "Project deleted", "id": project_id}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

23
backend/run.py Normal file
View File

@@ -0,0 +1,23 @@
"""
=============================================================================
RUN SCRIPT
=============================================================================
Simple script to run the FastAPI server.
Useful for development and debugging.
Usage:
python run.py
"""
import uvicorn
from config import settings
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=settings.PORT,
reload=settings.ENVIRONMENT == "development", # Auto-reload in dev
log_level="info",
)

View File

@@ -0,0 +1,10 @@
"""
Services Package
This package contains business logic and external service integrations.
Keeping business logic separate from routes makes the code more testable.
"""
from . import ai_service, database
__all__ = ["ai_service", "database"]

View File

@@ -0,0 +1,250 @@
"""
=============================================================================
AI SERVICE
=============================================================================
This module handles all AI provider integrations.
Supported Providers:
- OpenAI (GPT-4, GPT-3.5)
- Anthropic (Claude)
Architecture:
- Provider-agnostic interface (same API regardless of provider)
- Easy to add new providers
- Streaming support for real-time responses
The actual API keys and configuration come from config.py
"""
from typing import AsyncGenerator, Optional
import openai
from config import settings
# =============================================================================
# INITIALIZE CLIENTS
# =============================================================================
# Initialize OpenAI client
openai_client = openai.AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
# For Anthropic, you would:
# import anthropic
# anthropic_client = anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
# =============================================================================
# TEXT GENERATION
# =============================================================================
async def generate_text(
prompt: str,
max_tokens: int = 500,
temperature: float = 0.7,
model: Optional[str] = None,
) -> dict:
"""
Generate text from a prompt using the configured AI provider.
Args:
prompt: The input text to generate from
max_tokens: Maximum tokens in the response
temperature: Creativity level (0 = deterministic, 2 = very creative)
model: Override the default model
Returns:
dict with keys: text, model, usage
Example:
result = await generate_text("Write a haiku about Python")
print(result["text"])
"""
model = model or settings.AI_MODEL
if settings.AI_PROVIDER == "openai":
return await _generate_openai(prompt, max_tokens, temperature, model)
elif settings.AI_PROVIDER == "anthropic":
return await _generate_anthropic(prompt, max_tokens, temperature, model)
else:
raise ValueError(f"Unknown AI provider: {settings.AI_PROVIDER}")
async def _generate_openai(
prompt: str,
max_tokens: int,
temperature: float,
model: str,
) -> dict:
"""Generate text using OpenAI."""
response = await openai_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=temperature,
)
return {
"text": response.choices[0].message.content,
"model": model,
"usage": {
"prompt_tokens": response.usage.prompt_tokens,
"completion_tokens": response.usage.completion_tokens,
},
}
async def _generate_anthropic(
prompt: str,
max_tokens: int,
temperature: float,
model: str,
) -> dict:
"""
Generate text using Anthropic Claude.
NOTE: Uncomment and install anthropic package to use.
"""
# import anthropic
# client = anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
#
# response = await client.messages.create(
# model=model,
# max_tokens=max_tokens,
# messages=[{"role": "user", "content": prompt}],
# )
#
# return {
# "text": response.content[0].text,
# "model": model,
# "usage": {
# "prompt_tokens": response.usage.input_tokens,
# "completion_tokens": response.usage.output_tokens,
# },
# }
raise NotImplementedError("Anthropic provider not configured")
# =============================================================================
# CHAT COMPLETION
# =============================================================================
async def chat_completion(
messages: list[dict],
max_tokens: int = 500,
temperature: float = 0.7,
model: Optional[str] = None,
) -> dict:
"""
Generate a chat response from message history.
Args:
messages: List of message dicts with 'role' and 'content'
max_tokens: Maximum tokens in the response
temperature: Creativity level
model: Override the default model
Returns:
dict with keys: message, model
Example:
result = await chat_completion([
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
])
print(result["message"]["content"])
"""
model = model or settings.AI_MODEL
response = await openai_client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens,
temperature=temperature,
)
return {
"message": {
"role": "assistant",
"content": response.choices[0].message.content,
},
"model": model,
}
# =============================================================================
# STREAMING
# =============================================================================
async def stream_text(
prompt: str,
max_tokens: int = 500,
temperature: float = 0.7,
model: Optional[str] = None,
) -> AsyncGenerator[str, None]:
"""
Stream generated text token by token.
This is an async generator that yields text chunks as they're generated.
Use this for real-time chat interfaces.
Example usage in FastAPI:
@app.post("/stream")
async def stream_endpoint(request: Request):
return StreamingResponse(
stream_text(request.prompt),
media_type="text/event-stream",
)
"""
model = model or settings.AI_MODEL
response = await openai_client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=temperature,
stream=True, # Enable streaming
)
# Yield each chunk as it arrives
async for chunk in response:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
async def count_tokens(text: str, model: str = "gpt-3.5-turbo") -> int:
"""
Count the number of tokens in a text string.
Useful for:
- Estimating costs before making API calls
- Ensuring prompts don't exceed model limits
Note: This is an approximation. For exact counts, use tiktoken library.
"""
# Rough approximation: ~4 characters per token for English
return len(text) // 4
def get_model_context_limit(model: str) -> int:
"""
Get the context window size for a model.
Useful for knowing how much text you can send/receive.
"""
limits = {
# OpenAI
"gpt-4-turbo": 128000,
"gpt-4": 8192,
"gpt-3.5-turbo": 16385,
# Anthropic
"claude-3-opus-20240229": 200000,
"claude-3-sonnet-20240229": 200000,
"claude-3-haiku-20240307": 200000,
}
return limits.get(model, 4096)

View File

@@ -0,0 +1,172 @@
"""
=============================================================================
DATABASE SERVICE (SUPABASE)
=============================================================================
This module provides Supabase database integration.
Supabase Features Used:
- Database: PostgreSQL with auto-generated REST API
- Auth: User authentication (handled in frontend mostly)
- Real-time: Websocket subscriptions (used in frontend)
- Storage: File uploads (not shown here, but easy to add)
Why Supabase?
- Free tier is generous (great for hackathons!)
- Built-in auth that works with frontend
- Real-time subscriptions out of the box
- Full PostgreSQL (can run raw SQL)
- Great documentation
"""
from supabase import create_client, Client
from config import settings
from functools import lru_cache
@lru_cache
def get_supabase_client() -> Client:
"""
Get a cached Supabase client instance.
Using lru_cache ensures we reuse the same client,
avoiding connection overhead.
Note: We use the SERVICE KEY here (not anon key) because
the backend needs admin access to the database.
The frontend uses the anon key with Row Level Security.
"""
if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_KEY:
raise ValueError(
"Supabase not configured. "
"Set SUPABASE_URL and SUPABASE_SERVICE_KEY in .env"
)
return create_client(
settings.SUPABASE_URL,
settings.SUPABASE_SERVICE_KEY,
)
# =============================================================================
# DATABASE HELPERS
# =============================================================================
async def get_user_by_id(user_id: str) -> dict | None:
"""
Get a user by their ID.
Example:
user = await get_user_by_id("123-456-789")
print(user["email"])
"""
supabase = get_supabase_client()
response = supabase.auth.admin.get_user_by_id(user_id)
return response.user if response else None
async def verify_jwt_token(token: str) -> dict | None:
"""
Verify a JWT token and get the user.
Use this to authenticate API requests that include
the Supabase access token.
Example:
@app.get("/protected")
async def protected_route(authorization: str = Header()):
token = authorization.replace("Bearer ", "")
user = await verify_jwt_token(token)
if not user:
raise HTTPException(status_code=401)
"""
supabase = get_supabase_client()
try:
response = supabase.auth.get_user(token)
return response.user if response else None
except Exception:
return None
# =============================================================================
# GENERIC CRUD HELPERS
# =============================================================================
class DatabaseTable:
"""
Helper class for common database operations.
Example:
projects = DatabaseTable("projects")
all_projects = await projects.get_all()
project = await projects.get_by_id("123")
new_project = await projects.create({"name": "My Project"})
"""
def __init__(self, table_name: str):
self.table_name = table_name
self.client = get_supabase_client()
async def get_all(self, filters: dict = None) -> list[dict]:
"""Get all records, optionally filtered."""
query = self.client.table(self.table_name).select("*")
if filters:
for key, value in filters.items():
query = query.eq(key, value)
response = query.execute()
return response.data
async def get_by_id(self, record_id: str) -> dict | None:
"""Get a single record by ID."""
response = (
self.client.table(self.table_name)
.select("*")
.eq("id", record_id)
.single()
.execute()
)
return response.data
async def create(self, data: dict) -> dict:
"""Create a new record."""
response = (
self.client.table(self.table_name)
.insert(data)
.execute()
)
return response.data[0]
async def update(self, record_id: str, data: dict) -> dict:
"""Update an existing record."""
response = (
self.client.table(self.table_name)
.update(data)
.eq("id", record_id)
.execute()
)
return response.data[0] if response.data else None
async def delete(self, record_id: str) -> bool:
"""Delete a record."""
self.client.table(self.table_name).delete().eq("id", record_id).execute()
return True
# =============================================================================
# EXAMPLE: Projects Table Helper
# =============================================================================
# Create a helper instance for the projects table
projects_db = DatabaseTable("projects")
# You can now use:
# await projects_db.get_all()
# await projects_db.get_by_id("123")
# await projects_db.create({"name": "Test", "user_id": "abc"})
# await projects_db.update("123", {"name": "Updated"})
# await projects_db.delete("123")