My AI Development Stack: Claude, FastAPI, Supabase, and More
Why Stack Choices Matter
The tools you choose for AI development have a larger impact than in traditional software engineering. AI applications have unique requirements: variable response times, high API costs, complex data flows, and the need for robust error handling around non-deterministic outputs. The wrong stack choice can make these challenges significantly harder.
Here is a detailed look at my production stack and the reasoning behind each choice.
Claude (Anthropic) as Primary LLM
Claude is my default model for any task that requires strong reasoning, accurate analysis, or reliable structured output. I use the API via the official Python SDK:
from anthropic import Anthropic
client = Anthropic()
def analyze_document(content: str) -> dict:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{
"role": "user",
"content": f"Analyze this document: {content}"
}]
)
return parse_response(response.content[0].text)
Why Claude over alternatives? In my experience, Claude produces more consistent structured output, handles nuanced instructions better, and is less likely to hallucinate on factual questions. For production systems where reliability matters more than novelty, consistency is the most important quality in a model.
FastAPI for API Services
Every AI application I build exposes its functionality through a FastAPI service. The framework is ideal for AI applications because of:
- Async support: LLM API calls are I/O-bound. FastAPI's native async support lets me handle many concurrent requests efficiently
- Type hints and Pydantic: Automatic request/response validation catches malformed data before it hits the pipeline
- Auto-generated docs: Swagger UI makes API testing and documentation effortless
- Middleware support: Easy to add authentication, logging, and rate limiting
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="Content Scorer API")
class ScoreRequest(BaseModel):
content: str
rubric: str = "default"
class ScoreResponse(BaseModel):
overall: float
dimensions: dict
pass_threshold: bool
@app.post("/score", response_model=ScoreResponse)
async def score_content(request: ScoreRequest):
result = await scoring_pipeline.run(request.content, request.rubric)
return ScoreResponse(**result)
Supabase for Data and Auth
Supabase is the backbone of my data layer. Here is why it replaced my previous setup of separate PostgreSQL, auth, and storage services:
- PostgreSQL under the hood: Full SQL support, triggers, functions, and all the reliability of Postgres
- pgvector extension: Vector similarity search without a separate vector database
- Row-level security: Authorization logic lives in the database, not scattered through application code
- Built-in auth: JWT-based authentication with minimal setup
- Real-time subscriptions: Useful for dashboards and monitoring
- Storage: File storage with access control for document processing pipelines
Having all of these in one service dramatically simplifies operations. One dashboard, one set of credentials, one backup strategy.
Python as the Language
Python is the obvious choice for AI engineering, but it is worth stating why explicitly:
- Every major AI SDK has first-class Python support
- The data processing ecosystem (pandas, numpy) is unmatched
- FastAPI and Pydantic make Python viable for production APIs
- Type hints and modern Python features keep code maintainable
Supporting Tools
Pydantic for Validation
Every data model in my pipelines is a Pydantic model. This means type checking, validation, and serialization are handled consistently across the entire stack.
httpx for HTTP Clients
I use httpx instead of requests because it supports async operations natively. When a pipeline needs to make multiple API calls, they can run concurrently.
PM2 and Nginx for Deployment
PM2 manages all my Python processes in production, and Nginx handles routing, SSL, and static files. This combination is simple, reliable, and free.
Git for Everything
Code, configuration, and even some data files are version-controlled with Git. This gives me history, rollback capability, and collaboration support.
What I Deliberately Avoided
Choosing what not to use is as important as choosing what to use:
- LangChain: Too much abstraction. I want direct control over my LLM calls
- Docker: Unnecessary complexity for my deployment model
- Kubernetes: Massively over-engineered for my scale
- Multiple database vendors: Supabase handles everything I need
A good tech stack is not the one with the most impressive names. It is the one where every component earns its place by solving a real problem better than the alternatives.
Evolving the Stack
This stack is not static. I evaluate new tools regularly and make changes when the benefits clearly outweigh the migration cost. But the bar for adding a new tool is high: it must solve a problem that my current stack cannot, and it must not add significant operational complexity. So far, this approach has served me well.