Building a Claude Agent on Railway + Supabase in 20 Minutes
Agent Guides

Building a Claude Agent on Railway + Supabase in 20 Minutes

DomAIn Labs Team
September 5, 2025
15 min read

Building a Claude Agent on Railway + Supabase in 20 Minutes

Goal: Deploy a production Claude agent that can:

  • Answer customer questions
  • Look up data from Supabase
  • Handle conversations with memory
  • Scale automatically

Time: 20 minutes Cost: ~$5/month for light usage Stack: Python + Claude + Railway + Supabase

Let's build it.

Prerequisites

You need:

  1. Anthropic API key (get free credits at console.anthropic.com)
  2. GitHub account
  3. Railway account (railway.app - free tier available)
  4. Supabase account (supabase.com - free tier available)

Optional but helpful:

  • Basic Python knowledge
  • Familiarity with REST APIs

Step 1: Set Up Supabase Database (3 minutes)

Create Project

  1. Go to supabase.com
  2. Click "New Project"
  3. Name: claude-agent-demo
  4. Database password: (generate strong password)
  5. Region: Choose closest to you
  6. Click "Create new project"

Wait ~2 minutes for provisioning.

Create Table

In Supabase dashboard:

  1. Go to "Table Editor"
  2. Click "New Table"
  3. Name: products
  4. Columns:
    id (int8, primary key, auto-increment)
    name (text)
    price (float8)
    stock (int4)
    description (text)
    created_at (timestamptz, default: now())
    
  5. Click "Save"

Insert Sample Data

In SQL Editor:

INSERT INTO products (name, price, stock, description) VALUES
('Laptop Pro', 1299.99, 15, 'High-performance laptop with 16GB RAM'),
('Wireless Mouse', 29.99, 50, 'Ergonomic wireless mouse with USB receiver'),
('USB-C Cable', 12.99, 100, '6ft braided USB-C charging cable'),
('Desk Lamp', 39.99, 25, 'Adjustable LED desk lamp with touch control'),
('Webcam HD', 79.99, 30, '1080p webcam with built-in microphone');

Get API Credentials

  1. Go to "Settings" → "API"
  2. Copy:
    • Project URL (something like https://xxx.supabase.co)
    • anon/public key

Save these for later.

Step 2: Create Claude Agent (5 minutes)

Project Structure

Create new directory:

mkdir claude-agent
cd claude-agent

Install Dependencies

Create requirements.txt:

anthropic==0.18.1
supabase==2.3.4
flask==3.0.2
python-dotenv==1.0.1

Create Agent Code

Create agent.py:

import os
from anthropic import Anthropic
from supabase import create_client, Client

class ClaudeAgent:
    def __init__(self):
        # Initialize Claude
        self.anthropic = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

        # Initialize Supabase
        supabase_url = os.getenv("SUPABASE_URL")
        supabase_key = os.getenv("SUPABASE_KEY")
        self.supabase: Client = create_client(supabase_url, supabase_key)

        # Conversation history (in-memory for demo, use DB for production)
        self.conversations = {}

    def search_products(self, query: str):
        """Search products in database"""
        try:
            response = self.supabase.table("products").select("*").ilike("name", f"%{query}%").execute()
            return response.data
        except Exception as e:
            return {"error": str(e)}

    def get_product_details(self, product_id: int):
        """Get specific product details"""
        try:
            response = self.supabase.table("products").select("*").eq("id", product_id).single().execute()
            return response.data
        except Exception as e:
            return {"error": str(e)}

    def check_stock(self, product_id: int):
        """Check product stock level"""
        try:
            response = self.supabase.table("products").select("name, stock").eq("id", product_id).single().execute()
            return response.data
        except Exception as e:
            return {"error": str(e)}

    def get_tools(self):
        """Define available tools for Claude"""
        return [
            {
                "name": "search_products",
                "description": "Search for products by name. Returns list of matching products with id, name, price, stock, and description.",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "Search query (product name)"
                        }
                    },
                    "required": ["query"]
                }
            },
            {
                "name": "get_product_details",
                "description": "Get detailed information about a specific product by ID.",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "product_id": {
                            "type": "integer",
                            "description": "Product ID"
                        }
                    },
                    "required": ["product_id"]
                }
            },
            {
                "name": "check_stock",
                "description": "Check current stock level for a product.",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "product_id": {
                            "type": "integer",
                            "description": "Product ID"
                        }
                    },
                    "required": ["product_id"]
                }
            }
        ]

    def execute_tool(self, tool_name: str, tool_input: dict):
        """Execute a tool and return result"""
        if tool_name == "search_products":
            return self.search_products(tool_input["query"])
        elif tool_name == "get_product_details":
            return self.get_product_details(tool_input["product_id"])
        elif tool_name == "check_stock":
            return self.check_stock(tool_input["product_id"])
        else:
            return {"error": f"Unknown tool: {tool_name}"}

    def chat(self, user_message: str, conversation_id: str = "default"):
        """Main chat function with tool use"""

        # Get or create conversation history
        if conversation_id not in self.conversations:
            self.conversations[conversation_id] = []

        # Add user message
        self.conversations[conversation_id].append({
            "role": "user",
            "content": user_message
        })

        # Keep conversation history lean (last 10 messages)
        messages = self.conversations[conversation_id][-10:]

        # Initial API call
        response = self.anthropic.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            tools=self.get_tools(),
            messages=messages
        )

        # Tool use loop
        while response.stop_reason == "tool_use":
            # Extract tool use
            tool_use = next(block for block in response.content if block.type == "tool_use")

            # Execute tool
            tool_result = self.execute_tool(tool_use.name, tool_use.input)

            # Add assistant response and tool result to history
            self.conversations[conversation_id].append({
                "role": "assistant",
                "content": response.content
            })

            self.conversations[conversation_id].append({
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_use_id": tool_use.id,
                        "content": str(tool_result)
                    }
                ]
            })

            # Continue conversation
            messages = self.conversations[conversation_id][-10:]
            response = self.anthropic.messages.create(
                model="claude-3-5-sonnet-20241022",
                max_tokens=1024,
                tools=self.get_tools(),
                messages=messages
            )

        # Extract final text response
        final_text = next(
            (block.text for block in response.content if hasattr(block, "text")),
            "I couldn't generate a response."
        )

        # Add assistant response to history
        self.conversations[conversation_id].append({
            "role": "assistant",
            "content": final_text
        })

        return final_text

Create API Server

Create app.py:

import os
from flask import Flask, request, jsonify
from dotenv import load_dotenv
from agent import ClaudeAgent

# Load environment variables
load_dotenv()

# Initialize Flask
app = Flask(__name__)

# Initialize agent
agent = ClaudeAgent()

@app.route("/")
def home():
    return {"status": "Claude Agent API is running"}

@app.route("/health")
def health():
    return {"status": "healthy"}

@app.route("/chat", methods=["POST"])
def chat():
    """Chat endpoint"""
    try:
        data = request.get_json()

        user_message = data.get("message")
        conversation_id = data.get("conversation_id", "default")

        if not user_message:
            return jsonify({"error": "Message required"}), 400

        # Get response from agent
        response = agent.chat(user_message, conversation_id)

        return jsonify({
            "response": response,
            "conversation_id": conversation_id
        })

    except Exception as e:
        return jsonify({"error": str(e)}), 500

if __name__ == "__main__":
    port = int(os.getenv("PORT", 8080))
    app.run(host="0.0.0.0", port=port)

Create Environment File

Create .env:

ANTHROPIC_API_KEY=your_anthropic_key_here
SUPABASE_URL=your_supabase_url_here
SUPABASE_KEY=your_supabase_anon_key_here
PORT=8080

Step 3: Test Locally (2 minutes)

Install Dependencies

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install -r requirements.txt

Run Server

python app.py

Server runs on http://localhost:8080

Test with cURL

curl -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Do you have any laptops in stock?"}'

Expected response:

{
  "response": "Yes! We have the Laptop Pro in stock. It's a high-performance laptop with 16GB RAM, priced at $1299.99. We currently have 15 units available.",
  "conversation_id": "default"
}

Works? Great! Let's deploy.

Step 4: Deploy to Railway (5 minutes)

Prepare for Deployment

Create Procfile:

web: python app.py

Create runtime.txt:

python-3.11.6

Create .gitignore:

venv/
__pycache__/
.env
*.pyc

Initialize Git

git init
git add .
git commit -m "Initial commit: Claude agent"

Push to GitHub

  1. Create new repo on GitHub
  2. Push code:
git remote add origin https://github.com/yourusername/claude-agent.git
git branch -M main
git push -u origin main

Deploy on Railway

  1. Go to railway.app
  2. Click "New Project"
  3. Select "Deploy from GitHub repo"
  4. Choose your claude-agent repo
  5. Railway auto-detects Python and deploys

Add Environment Variables

In Railway dashboard:

  1. Go to your project
  2. Click "Variables"
  3. Add:
    ANTHROPIC_API_KEY=your_key
    SUPABASE_URL=your_url
    SUPABASE_KEY=your_key
    PORT=8080
    
  4. Click "Deploy"

Wait ~2 minutes for deployment.

Get Public URL

  1. In Railway, go to "Settings"
  2. Click "Generate Domain"
  3. Copy domain (something like claude-agent-production.up.railway.app)

Step 5: Test Production (2 minutes)

Test Health Endpoint

curl https://your-railway-domain.up.railway.app/health

Should return:

{"status": "healthy"}

Test Chat Endpoint

curl -X POST https://your-railway-domain.up.railway.app/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "What products do you have under $50?",
    "conversation_id": "user_123"
  }'

Step 6: Add Conversation Persistence (Bonus)

Right now, conversations are in-memory. Let's persist them in Supabase.

Create Conversations Table

In Supabase SQL Editor:

CREATE TABLE conversations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    conversation_id TEXT NOT NULL,
    messages JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_conversation_id ON conversations(conversation_id);

Update Agent Code

In agent.py, replace the __init__ and chat methods:

def __init__(self):
    # ... existing code ...

    # Remove in-memory storage
    # self.conversations = {}

def load_conversation(self, conversation_id: str):
    """Load conversation from Supabase"""
    try:
        response = self.supabase.table("conversations").select("messages").eq("conversation_id", conversation_id).single().execute()
        return response.data["messages"]
    except:
        return []

def save_conversation(self, conversation_id: str, messages: list):
    """Save conversation to Supabase"""
    try:
        # Check if conversation exists
        existing = self.supabase.table("conversations").select("id").eq("conversation_id", conversation_id).execute()

        if len(existing.data) > 0:
            # Update
            self.supabase.table("conversations").update({
                "messages": messages,
                "updated_at": "now()"
            }).eq("conversation_id", conversation_id).execute()
        else:
            # Insert
            self.supabase.table("conversations").insert({
                "conversation_id": conversation_id,
                "messages": messages
            }).execute()
    except Exception as e:
        print(f"Error saving conversation: {e}")

def chat(self, user_message: str, conversation_id: str = "default"):
    """Main chat function with persistent conversation history"""

    # Load conversation history from database
    messages = self.load_conversation(conversation_id)

    # Add user message
    messages.append({
        "role": "user",
        "content": user_message
    })

    # ... rest of existing chat logic ...

    # After getting final response, save conversation
    self.save_conversation(conversation_id, messages)

    return final_text

Commit and push:

git add .
git commit -m "Add conversation persistence"
git push

Railway auto-deploys the update.

What You Built

A production Claude agent that:

  • ✅ Uses Claude 3.5 Sonnet with tool use
  • ✅ Queries Supabase database
  • ✅ Maintains conversation history
  • ✅ Scales automatically on Railway
  • ✅ Costs ~$5/month for light usage
  • ✅ Has persistent conversations

Next Steps

Add Authentication

from functools import wraps

def require_api_key(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        api_key = request.headers.get("X-API-Key")
        if api_key != os.getenv("API_KEY"):
            return jsonify({"error": "Unauthorized"}), 401
        return f(*args, **kwargs)
    return decorated

@app.route("/chat", methods=["POST"])
@require_api_key  # Add this
def chat():
    # ... existing code

Add Rate Limiting

pip install flask-limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["100 per hour"]
)

@app.route("/chat", methods=["POST"])
@limiter.limit("10 per minute")  # Add this
def chat():
    # ... existing code

Add Monitoring

Railway provides basic metrics. For more, add:

pip install sentry-sdk
import sentry_sdk
sentry_sdk.init(dsn=os.getenv("SENTRY_DSN"))

Scale Up

Railway auto-scales, but you can optimize:

  1. Use connection pooling for Supabase
  2. Add caching for common queries (Redis)
  3. Implement async endpoints (FastAPI instead of Flask)

Cost Breakdown

Monthly estimates (1,000 requests):

ServiceCost
Railway (Hobby plan)$5
Supabase (Free tier)$0
Claude API (1K requests, 500K tokens)$1.50
Total~$6.50/month

For 10,000 requests/month: ~$20-30

Common Issues

Issue #1: Railway Deployment Fails

Fix: Check runtime.txt has correct Python version

Issue #2: Agent Can't Connect to Supabase

Fix: Verify environment variables are set in Railway dashboard

Issue #3: Conversation Not Persisting

Fix: Check Supabase table exists and agent has correct permissions

Issue #4: Slow Responses

Fix:

  • Check Railway logs for errors
  • Optimize Supabase queries (add indexes)
  • Reduce conversation history length

The Bottom Line

You just built a production Claude agent that:

  • Runs in the cloud
  • Uses real database
  • Scales automatically
  • Costs < $10/month

Total time: 20 minutes Total cost: ~$6.50/month Production ready: Yes (with auth + monitoring)

Getting Started

Clone starter code:

git clone https://github.com/domainlabs/claude-agent-starter
cd claude-agent-starter
# Follow README for setup

Need help customizing this for your use case? We've deployed dozens of Claude agents.

Get deployment help →


Related reading:

Tags:TutorialClaudeRailwaySupabaseDeployment

About the Author

DomAIn Labs Team

The DomAIn Labs team consists of AI engineers, strategists, and educators passionate about demystifying AI for small businesses.