Starter Project Anatomy¶
This document describes the structure and components of the CB Radio starter project, which serves as a template for building FastAPI + FastHTML web applications with DuckDB.
Overview¶
The starter project provides a complete, production-ready foundation with: - JWT authentication (admin/admin123 default user) - FastAPI backend with modular routing - FastHTML frontend with Jinja2 templates - DuckDB database - TailwindCSS + HTMX for UI - Systemd service files for deployment
Directory Structure¶
cbradio/
├── .env # Environment configuration
├── requirements.txt # Python dependencies
├── db/
│ └── cbradio.db # DuckDB database file
├── docs/
│ └── STARTER-PROJECT.md # This file
├── scripts/
│ ├── config.py # Shared configuration module
│ ├── create_schema.py # Database schema + seed data
│ └── services/ # Systemd service files
│ ├── ruralamfm-api.service
│ └── ruralamfm-app.service
└── src/
├── __init__.py
├── api/ # FastAPI Backend
│ ├── __init__.py
│ ├── main.py # FastAPI app entry point
│ ├── auth.py # JWT authentication utilities
│ ├── config.py # API-specific settings (Pydantic)
│ ├── database.py # DuckDB connection utilities
│ ├── models/ # Pydantic data models
│ │ ├── __init__.py
│ │ └── user.py # User, Token, TokenData models
│ └── routes/ # API route handlers
│ ├── __init__.py
│ └── auth.py # /api/auth/* endpoints
└── app/ # FastHTML Frontend
├── __init__.py
├── main.py # FastHTML app entry point
├── static/
│ ├── css/
│ │ └── styles.css
│ ├── images/
│ └── js/
└── templates/ # Jinja2 templates
├── layout.html # Base template with nav/sidebar
├── login.html # Login page
└── dashboard.html # Dashboard page
Configuration¶
Environment Variables (.env)¶
# Security
SECRET_KEY=dev-secret-key-change-in-production
API_KEYS=test-api-key-for-development
# Database
DB_PATH=db/cbradio.db
# Server Ports
API_PORT=32331
FRONTEND_PORT=32330
HOST=0.0.0.0
# URLs (for production)
FRONTEND_URL=https://ruralamfm.nominate.ai
BACKEND_API_URL=https://ruralamfm.nominate.ai/api
WS_BASE_URL=wss://ruralamfm.nominate.ai
# Branding
APP_NAME=Rural AM/FM
APP_NAME_SHORT=RAMFM
# Theme Colors
THEME_PRIMARY_COLOR=#0e173e
THEME_SECONDARY_COLOR=#f1c613
THEME_ACCENT_COLOR=#f1c613
THEME_TEXT_COLOR=#374151
THEME_SIDEBAR_COLOR=#f8fafc
scripts/config.py¶
Shared configuration module loaded by both API and frontend:
- Loads .env via python-dotenv
- Exports config dict with app settings
- Exports DB_PATH for database location
- Resolves relative paths from project root
src/api/config.py¶
Pydantic Settings class for type-safe API configuration:
- Settings class with defaults
- get_settings() cached function
- Session timeout settings
Backend (src/api/)¶
main.py - FastAPI Application¶
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="CB Radio API", version="0.1.0")
# CORS middleware for frontend
app.add_middleware(CORSMiddleware, ...)
# Include routers
app.include_router(auth.router, prefix="/api")
# Endpoints
GET /api # API info
GET /api/health # Health check
GET /api/protected # Example protected route
auth.py - Authentication¶
JWT-based authentication with:
- hash_password() / verify_password() - SHA256 hashing
- get_user(username) - Fetch user from DB
- authenticate_user(username, password) - Validate credentials
- create_access_token(data) - Generate JWT
- get_current_user() - FastAPI dependency
- get_current_active_user() - Require active user
database.py - DuckDB Utilities¶
@contextmanager
def get_connection():
"""Context manager for DB connections."""
def execute_query(query, params) -> list[dict]:
"""Execute query, return list of dicts."""
def execute_and_fetch_one(query, params) -> dict | None:
"""Execute query, return first row."""
def generate_id() -> str:
"""Generate UUID for new records."""
models/user.py - User Models¶
class UserBase(BaseModel): # username, email, first_name, last_name, role
class UserCreate(UserBase): # + password
class UserUpdate(BaseModel): # All optional fields
class User(UserBase): # + id, is_active, timestamps
class UserInDB(User): # + password_hash
class Token(BaseModel): # access_token, token_type
class TokenData(BaseModel): # id, username, role (JWT payload)
routes/auth.py - Auth Endpoints¶
Frontend (src/app/)¶
main.py - FastHTML Application¶
from fasthtml import FastHTML
from starlette.middleware.sessions import SessionMiddleware
app = FastHTML(debug=True)
app.add_middleware(SessionMiddleware, ...)
app.mount("/static", StaticFiles(...))
templates = Jinja2Templates(directory="templates")
# Routes
GET/POST /login # Login page and handler
GET /logout # Clear session, redirect
GET /dashboard # Protected dashboard
GET / # Redirect to login or dashboard
API Helper Function¶
async def api_request(method, endpoint, token=None, data=None, params=None):
"""Make authenticated API request to backend."""
# Returns JSON response or {"error": "message"}
# Handles SESSION_EXPIRED for auto-logout
Templates¶
layout.html - Base template with: - TailwindCSS (CDN) - Lucide Icons - HTMX - Navigation bar - Sidebar (when logged in) - Session timeout modal - Theme CSS variables
login.html - Login form extending layout
dashboard.html - Welcome dashboard with: - User greeting - Status cards - Tech stack display
Database Schema¶
user table¶
CREATE TABLE user (
id VARCHAR PRIMARY KEY,
username VARCHAR UNIQUE NOT NULL,
email VARCHAR,
password_hash VARCHAR NOT NULL,
first_name VARCHAR,
last_name VARCHAR,
role VARCHAR DEFAULT 'admin',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
Default User¶
- Username: admin
- Password: admin123
- Role: admin
Created automatically by scripts/create_schema.py.
Deployment¶
Systemd Services¶
ruralamfm-api.service - Backend API
ruralamfm-app.service - Frontend App
Service Commands¶
# Install services
sudo ln -sf /path/to/services/*.service /etc/systemd/system/
sudo systemctl daemon-reload
# Enable on boot
sudo systemctl enable ruralamfm-api ruralamfm-app
# Start/Stop/Restart
sudo systemctl start ruralamfm-api ruralamfm-app
sudo systemctl stop ruralamfm-api ruralamfm-app
sudo systemctl restart ruralamfm-api ruralamfm-app
# View logs
sudo journalctl -u ruralamfm-api -f
sudo journalctl -u ruralamfm-app -f
Nginx Configuration¶
Nginx routes requests to the appropriate service:
- /api/* and /ws → port 32331 (backend)
- / → port 32330 (frontend)
Adding New Features¶
New API Endpoint¶
-
Create model in
src/api/models/: -
Create route in
src/api/routes/: -
Register in
src/api/main.py:
New Frontend Page¶
-
Create template in
src/app/templates/: -
Add route in
src/app/main.py: -
Add to sidebar in
layout.html
New Database Table¶
-
Add to
scripts/create_schema.py: -
Run schema creation:
Tech Stack Reference¶
| Component | Technology | Version |
|---|---|---|
| Language | Python | 3.12.9 |
| Backend | FastAPI | latest |
| Frontend | FastHTML | latest |
| Database | DuckDB | latest |
| Templates | Jinja2 | latest |
| CSS | TailwindCSS | 2.2.19 (CDN) |
| Icons | Lucide | latest (CDN) |
| Interactivity | HTMX | 1.9.2 (CDN) |
| Auth | PyJWT | latest |
| HTTP Client | httpx | latest |
| Validation | Pydantic | latest |
Python Environment¶
# Activate virtualenv
source ~/.pyenv/versions/nominates/bin/activate
# Install dependencies
pip install -r requirements.txt
# Initialize database
python scripts/create_schema.py
# Run locally (development)
python -m uvicorn src.api.main:app --port 8000 --reload # API
python -m uvicorn src.app.main:app --port 8080 --reload # Frontend