{"slug": "building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase", "title": "Building a RAG System from Scratch — Cloud Deployment with Render and Supabase", "summary": "A developer built and deployed a Retrieval-Augmented Generation (RAG) system to the cloud using Render and Supabase. The system uses pgvector for vector search and is accessible via an MCP server hosted on Render with a Supabase PostgreSQL database. The deployment involved migrating data from a local Docker setup to Supabase and configuring the server for cloud hosting.", "body_md": "In the [previous article](https://dev.to/hiroki-kameyama/building-a-rag-system-from-scratch-mcp-exposing-pgvector-as-a-reusable-tool-server-2onc), we built an MCP server that any LLM client can connect to locally. In this article, we'll deploy it to the cloud — making it accessible from anywhere.\n\n```\nBefore: localhost:8000 → pgvector (Docker)\nAfter:  https://your-app.onrender.com/mcp → Supabase (pgvector)\n```\n\nBoth services are free to start with no credit card required.\n\n| Service | Role | Free tier |\n|---|---|---|\nRender |\nHost the MCP HTTP server | Persistent web service (sleeps after 15 min) |\nSupabase |\nManaged PostgreSQL + pgvector | 500MB, persistent |\n\nOpen **SQL Editor** in the Supabase dashboard and run:\n\n```\nCREATE EXTENSION IF NOT EXISTS vector;\n\nCREATE TABLE IF NOT EXISTS documents (\n    id          SERIAL PRIMARY KEY,\n    title       TEXT NOT NULL,\n    body        TEXT NOT NULL,\n    category    TEXT,\n    created_at  TIMESTAMP DEFAULT NOW(),\n    embedding   vector(768)\n);\n\nCREATE INDEX IF NOT EXISTS docs_embedding_idx\nON documents\nUSING hnsw (embedding vector_cosine_ops)\nWITH (m = 16, ef_construction = 64);\n\nCREATE INDEX ON documents (category);\n```\n\nClick the **Connect** button at the top of the dashboard (not Settings → Database — the UI has changed).\n\nSelect the **Connection pooling** tab and copy the **Transaction** mode URI. It looks like:\n\n```\npostgresql://postgres.xxxx:password@aws-0-ap-northeast-1.pooler.supabase.com:6543/postgres\n```\n\nWhy port 6543?The standard port 5432 uses IPv6, which Render doesn't support. The Connection Pooler (port 6543) uses IPv4 and is the correct choice for cloud-to-cloud connections.\n\nAdd the Supabase URL to your `.env`\n\n:\n\n```\nDATABASE_URL=postgresql://postgres.xxxx:password@aws-0-ap-northeast-1.pooler.supabase.com:6543/postgres\n```\n\nThen run the migration:\n\n``` python\n# migrate_to_supabase.py\nimport psycopg2\nfrom dotenv import load_dotenv\nimport os\n\nload_dotenv()\n\nlocal_conn = psycopg2.connect(\n    host=os.getenv(\"DB_HOST\"), port=os.getenv(\"DB_PORT\"),\n    dbname=os.getenv(\"DB_NAME\"), user=os.getenv(\"DB_USER\"),\n    password=os.getenv(\"DB_PASSWORD\"),\n)\nlocal_cur = local_conn.cursor()\n\nsupa_conn = psycopg2.connect(os.getenv(\"DATABASE_URL\"), sslmode=\"require\")\nsupa_cur = supa_conn.cursor()\n\nlocal_cur.execute(\"SELECT title, body, category, embedding FROM documents;\")\nrows = local_cur.fetchall()\nprint(f\"Migrating {len(rows)} documents...\")\n\nfor row in rows:\n    title, body, category, embedding = row\n    supa_cur.execute(\"\"\"\n        INSERT INTO documents (title, body, category, embedding)\n        VALUES (%s, %s, %s, %s) ON CONFLICT DO NOTHING;\n    \"\"\", (title, body, category, embedding))\n\nsupa_conn.commit()\n\nsupa_cur.execute(\"SELECT COUNT(*) FROM documents;\")\ncount = supa_cur.fetchone()[0]\nprint(f\"Done. Documents in Supabase: {count}\")\n\nlocal_conn.close()\nsupa_conn.close()\npython migrate_to_supabase.py\n# Migrating 5 documents...\n# Done. Documents in Supabase: 5\n```\n\nCreate `mcp_server/server_render.py`\n\n. The only differences from `server.py`\n\n:\n\n`DATABASE_URL`\n\nenv var`PORT`\n\nenv var (Render sets this automatically)`streamable-http`\n\ninstead of stdio\n\n``` python\n# mcp_server/server_render.py\nimport psycopg2\nfrom google import genai\nfrom google.genai import types as genai_types\nfrom fastmcp import FastMCP\nfrom dotenv import load_dotenv\nimport os\n\nload_dotenv()\n\nmcp = FastMCP(\n    name=\"pgvector-search\",\n    instructions=\"Document search server using pgvector.\",\n)\n\ngemini_client = genai.Client(api_key=os.getenv(\"GEMINI_API_KEY\"))\n\n# Use DATABASE_URL if available (Render/Supabase), fall back to individual vars\nDATABASE_URL = os.getenv(\"DATABASE_URL\")\nif DATABASE_URL:\n    conn = psycopg2.connect(DATABASE_URL, sslmode=\"require\")\nelse:\n    conn = psycopg2.connect(\n        host=os.getenv(\"DB_HOST\", \"localhost\"),\n        port=os.getenv(\"DB_PORT\", \"5432\"),\n        dbname=os.getenv(\"DB_NAME\", \"vectordb\"),\n        user=os.getenv(\"DB_USER\", \"postgres\"),\n        password=os.getenv(\"DB_PASSWORD\", \"password\"),\n    )\n\ncur = conn.cursor()\n\ndef get_embedding(text: str) -> list[float]:\n    result = gemini_client.models.embed_content(\n        model=\"gemini-embedding-001\",\n        contents=text,\n        config=genai_types.EmbedContentConfig(\n            task_type=\"RETRIEVAL_QUERY\",\n            output_dimensionality=768,\n        ),\n    )\n    return result.embeddings[0].values\n\n@mcp.tool\ndef search_documents(query: str, top_k: int = 3) -> list[dict]:\n    \"\"\"Search all document categories for a given query.\"\"\"\n    q = get_embedding(query)\n    cur.execute(\"\"\"\n        SELECT title, body, category,\n               1 - (embedding <=> %s::vector) AS similarity\n        FROM documents ORDER BY embedding <=> %s::vector LIMIT %s;\n    \"\"\", (q, q, top_k))\n    return [\n        {\"title\": r[0], \"body\": r[1], \"category\": r[2], \"similarity\": round(r[3], 4)}\n        for r in cur.fetchall()\n    ]\n\n@mcp.tool\ndef search_by_category(query: str, category: str, top_k: int = 3) -> list[dict]:\n    \"\"\"Search within a specific category (ML, Python, or Cloud).\"\"\"\n    q = get_embedding(query)\n    cur.execute(\"\"\"\n        SELECT title, body, category,\n               1 - (embedding <=> %s::vector) AS similarity\n        FROM documents WHERE category = %s\n        ORDER BY embedding <=> %s::vector LIMIT %s;\n    \"\"\", (q, category, q, top_k))\n    return [\n        {\"title\": r[0], \"body\": r[1], \"category\": r[2], \"similarity\": round(r[3], 4)}\n        for r in cur.fetchall()\n    ]\n\n@mcp.tool\ndef list_categories() -> list[dict]:\n    \"\"\"Return all available categories and document counts.\"\"\"\n    cur.execute(\"\"\"\n        SELECT category, COUNT(*) as count\n        FROM documents GROUP BY category ORDER BY count DESC;\n    \"\"\")\n    return [{\"category\": r[0], \"count\": r[1]} for r in cur.fetchall()]\n\nif __name__ == \"__main__\":\n    port = int(os.getenv(\"PORT\", 8000))  # Render sets PORT automatically\n    mcp.run(\n        transport=\"streamable-http\",\n        host=\"0.0.0.0\",\n        port=port,\n    )\n```\n\nPush to GitHub:\n\n```\ngit add .\ngit commit -m \"feat: add Render deployment server\"\ngit push origin main\n```\n\n| Field | Value |\n|---|---|\n| Name | `pgvector-mcp-server` |\n| Runtime | Python 3 |\n| Build Command | `pip install -r requirements.txt` |\n| Start Command | `python mcp_server/server_render.py` |\n| Instance Type | Free |\n\n| Key | Value |\n|---|---|\n`GEMINI_API_KEY` |\n`AIza...` |\n`DATABASE_URL` |\nThe Connection Pooler URI from Supabase (port 6543) |\n\nOnce deployed, visit:\n\n```\nhttps://pgvector-mcp-server.onrender.com/mcp\n```\n\nYou'll see:\n\n```\n{\"jsonrpc\":\"2.0\",\"id\":\"server-error\",\"error\":{\"code\":-32600,\"message\":\"Not Acceptable: Client must accept text/event-stream\"}}\n```\n\nThis is correct — it means the server is running. The error appears because browsers aren't MCP clients. Your agent will connect without issues.\n\nFirst request is slow:Render's free tier sleeps after 15 minutes of inactivity. The first request after sleep takes 30–60 seconds to wake up. Use[UptimeRobot](free) to ping every 5 minutes and prevent sleep.\n\nUpdate `13_mcp_http_agent.py`\n\nwith the Render URL:\n\n```\n# 13_mcp_http_agent.py\nMCP_SERVER_URL = \"https://pgvector-mcp-server.onrender.com/mcp\"\n\nasync def run_agent(task: str):\n    async with Client(MCP_SERVER_URL) as mcp_client:  # URL instead of file path\n        mcp_tools = await mcp_client.list_tools()\n        print(f\"Loaded {len(mcp_tools)} tools from remote MCP server\")\n        # ... rest of the agentic loop is identical ...\npython 13_mcp_http_agent.py\n# Loaded 3 tools from remote MCP server\n# [Step 1]\n#   → list_categories({})\n# [Step 2]\n#   → search_by_category({'query': 'evaluation metrics', 'category': 'ML'})\n# [Done in 3 steps]\n```\n\nThe agent is now querying pgvector on Supabase through an MCP server running on Render — entirely in the cloud.\n\n| Error | Cause | Fix |\n|---|---|---|\n`Network is unreachable` (IPv6) |\nUsing port 5432 | Use Connection Pooler URL (port 6543) |\n`SSL connection required` |\nMissing sslmode | Add `sslmode=\"require\"`\n|\n`ModuleNotFoundError` |\nMissing package | Run `pip freeze > requirements.txt` and push |\n| Slow first response | Render sleep | Use UptimeRobot to keep alive |\n`Not Found` at `/`\n|\nWrong URL | Add `/mcp` to the URL |\n\n```\nLocal development:\n  Python agent → stdio → mcp_server/server.py → Docker pgvector\n\nCloud deployment:\n  Python agent → HTTPS → Render (server_render.py) → Supabase pgvector\n```\n\nThe codebase is identical. The infrastructure changed around it.\n\nIn the final article, we'll wrap up the series with a summary of all design decisions and point to Vol.2 — where we cover Evals, Observability, Security, MLOps, Fine-tuning, Multi-Agent, and Governance.\n\n*Full source code: github.com/qameqame/pgvector-tutorial*", "url": "https://wpnews.pro/news/building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase", "canonical_source": "https://dev.to/hiroki-kameyama/building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase-2fd9", "published_at": "2026-06-27 22:20:09+00:00", "updated_at": "2026-06-27 23:03:37.158844+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools", "ai-infrastructure"], "entities": ["Render", "Supabase", "pgvector", "MCP", "FastMCP", "Gemini", "PostgreSQL", "Docker"], "alternates": {"html": "https://wpnews.pro/news/building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase", "markdown": "https://wpnews.pro/news/building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase.md", "text": "https://wpnews.pro/news/building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase.txt", "jsonld": "https://wpnews.pro/news/building-a-rag-system-from-scratch-cloud-deployment-with-render-and-supabase.jsonld"}}