PocketCodeIn Docs
// Service Usage Documentation Β· How to use each tool in the stack
πŸ“š
Service Documentation Hub
// Quick reference for using every service in the pocketcode.in stack

This site covers how to use each service after it's set up. For installation steps, see the setup guide. For source code and architecture, see the bundle README.

How this stack feels day-to-day β€” a few behaviours that are easy to miss until you bump into them:
  • Top bar on every service β€” each gated subdomain shows a thin top bar with the service icon (same as the home page card), a sync indicator, your signed-in user, and a Logout link. The actual service runs in an iframe below the bar so the bar stays visible on every navigation. terminal.pocketcode.in has its own tabbed variant.
  • Logout is cross-tab β€” click Logout on any service tab and every other open tab (home, other services) shows a Session expired overlay within a few seconds. Click Sign in on the overlay and you return to that tab's URL after login, not the home page.
  • Browser back button works within a service β€” back/forward retrace your navigation inside the iframe instead of jumping out of the wrapper. (Same-iframe SPA routing inside Sim.ai still doesn't update the address bar β€” that's an upstream limitation.)
  • www.* aliases β€” typing www.openclaw.pocketcode.in (or any other www. variant) lands on the bare hostname with the path preserved. HTTP requests also redirect to HTTPS automatically.
  • Service icons in the browser tab β€” each subdomain has its own favicon so you can tell tabs apart at a glance. Home/login use πŸš€.
  • Service Workers blocked β€” if a service registers a Service Worker (OpenClaw, Sim.ai PWAs), the wrapper unregisters it on load and Caddy 404s /sw.js. PWA install is sacrificed; reliable wrapper rendering is gained.
See the diagrams under Databases and Service Comms for the full topology.

All Services at a Glance

These cards are reference only β€” they describe each service. To launch any service, go to pocketcode.in.

πŸ’¬
Open WebUI
ChatGPT-style interface for local Ollama models. RAG, file uploads, multi-user. Start here for most chat needs.
chat.pocketcode.in
🎨
Sim.ai
Visual workflow canvas for AI agents. Drag-drop nodes, connect models to tools, run pipelines.
sim.pocketcode.in
βš™οΈ
n8n
Workflow automation. Triggers, schedules, 400+ integrations. Bridges AI and the rest of your stack.
n8n.pocketcode.in
🦞
OpenClaw
Multi-model AI agent dashboard. Coordinate Claude, GPT, Ollama models in one UI.
openclaw.pocketcode.in
⇆
OpenRouter (LiteLLM Proxy)
OpenAI-compatible API endpoint. Routes to GPT-4, Claude, Llama, etc. Use from any tool that speaks OpenAI's API.
openrouter.pocketcode.in
πŸ¦™
Ollama API
Local LLM runtime. Direct REST API. Used internally by Open WebUI, Sim.ai, and n8n.
ollama.pocketcode.in
β—†
Qdrant
Vector database for embeddings. Semantic search, RAG retrieval, similarity matching.
qdrant.pocketcode.in
🐘
pgAdmin
PostgreSQL admin GUI. Inspect databases, run queries, manage data across all services.
pgadmin.pocketcode.in
πŸ–₯️
Web Terminal
A command-line window to the server, opened in your browser. Full root, full Docker, runs anywhere.
terminal.pocketcode.in
πŸ“Ί
YouTube Downloader Β· v2
Save videos, audio, or transcripts from YouTube and 1000+ other sites β€” paste URL, search, or browse recent files. Real-time download progress.
ytdl.pocketcode.in
πŸŽ™οΈ
Voice Playground Β· v2
Talk to the AI agent through your browser (no phone needed) or have it call a real number. Per-call model + voice picker, live transcripts, pending callbacks board.
voice.pocketcode.in
πŸ’»
VS Code (code-server) Β· v2
A full VS Code editor that runs in your browser, with the Claude AI helper built into the sidebar. Integrated terminal has claude, docker, and ai-doctor on PATH.
code.pocketcode.in
πŸ“ž
Voice Agent (PSTN) Β· v2
The AI voice that answers and places real phone calls. Dial +1 (534) 231-0196 and "Adrian" picks up.
livekit-agent Β· outbound-only worker

Infrastructure Reference

πŸ—„οΈ
Databases
Shared PostgreSQL with pgvector. Six databases on sim-db-1: simstudio, n8n, openclaw, ailab, ollama_results, and voice_pg (v2 voice agent data plane).
Internal: sim-db-1:5432
πŸ”—
Service Comms
All services share the ai-stack Docker network. Each service is reachable by its container name.
Internal Docker DNS
πŸ“
Shared Files
Two cross-service file paths: a Docker volume at /shared and a bind-mount at /uploads.
Cross-container file exchange

How to Read This Documentation

Each service tab follows the same structure:

One login. Every service. Stays signed in. Sign in once at pocketcode.in. The gateway sets a .pocketcode.in cookie that protects every subdomain via Caddy's forward_auth. Auto-SSO goes further: the gateway also programmatically logs you into Sim.ai, Open WebUI, and n8n using stored credentials β€” click any card and you're already inside. A watchdog refreshes those service sessions every 2 minutes and on tab focus, so if you accidentally click a service's own "Logout" or its session expires, you're silently re-logged-in within seconds.
Logging out at pocketcode.in: only the gateway cookie is cleared. Service-internal sessions stay intact, but Caddy's forward_auth immediately blocks access to every *.pocketcode.in service β€” visiting any of them redirects to the pocketcode login page. After re-logging in, every service is instantly accessible again (since their own sessions never died).

TLS Health Quick Check

If you ever notice intermittent TLS errors in private browser windows or when curling from outside the server, this is the first command to run:

bash
for h in pocketcode.in sim.pocketcode.in chat.pocketcode.in n8n.pocketcode.in \
         openclaw.pocketcode.in pgadmin.pocketcode.in terminal.pocketcode.in \
         qdrant.pocketcode.in ollama.pocketcode.in openrouter.pocketcode.in; do
  cn=$(echo Q | timeout 5 openssl s_client -servername "$h" -connect 127.0.0.1:443 2>/dev/null \
       | openssl x509 -noout -subject 2>/dev/null | sed -E 's/.*CN ?= ?//')
  printf "  %-30s %s\n" "$h" "${cn:-NO CERT}"
done

Each hostname should report a CN β€” typically *.pocketcode.in (the wildcard cert) or the apex name. Any NO CERT line means Caddy isn't serving TLS for that hostname and the setup guide's Tab 18 (TLS Troubleshooting) has the full diagnostic + recovery playbook.

Single command that summarizes ongoing cert health:

bash
docker exec caddy find /data/caddy/certificates -name '*.crt' 2>/dev/null | sort

You should see a wildcard_.pocketcode.in/ entry and an apex entry. Old per-subdomain certs may linger for ~30 days β€” harmless leftovers.

What to watch for. Stale TXT records at _acme-challenge.pocketcode.in are the single most common cause of cert acquisition failures. Quick check: dig +short TXT _acme-challenge.pocketcode.in. If it returns more than one record, recovery is needed β€” setup guide Tab 18 Step 4.
πŸ’¬
Open WebUI
// ChatGPT-style interface for local Ollama models Β· chat.pocketcode.in

Quick Start

  1. Go to chat.pocketcode.in
  2. Sign in with your Open WebUI admin account (first signup = admin)
  3. Pick a model from the dropdown at the top center of the chat window
  4. Type a message β†’ press Enter

Pull a New Model

Models are pulled inside the Ollama container. You can do this from Open WebUI's admin panel OR via the web terminal:

via terminal.pocketcode.in
docker exec ollama ollama pull llama3.2:3b
docker exec ollama ollama pull qwen2.5:7b
docker exec ollama ollama pull nomic-embed-text   # for RAG embeddings

Or in Open WebUI: Settings β†’ Admin Panel β†’ Models β†’ Pull a model from Ollama.com. Type the model name (e.g. llama3.2:3b) and click pull.

Common Tasks

Upload a document for Q&A (RAG):

  1. Click the πŸ“Ž paperclip icon in the chat input
  2. Select a PDF, DOCX, MD, or TXT file
  3. Ask questions about it β€” the model reads the document and answers

Build a permanent knowledge base:

  1. Profile (top right) β†’ Workspace β†’ Knowledge β†’ + Create Knowledge
  2. Name it (e.g. "Company Docs") β†’ upload multiple files
  3. Open WebUI chunks the docs, embeds them with nomic-embed-text, stores in ChromaDB
  4. In any chat, reference the collection with #Company Docs

Create a custom model with a pinned system prompt:

  1. Workspace β†’ Models β†’ + Create a model
  2. Choose base model (e.g. llama3.2), add a system prompt, save
  3. Now appears in the chat model dropdown

Use as OpenAI-compatible API for other tools:

example β€” curl
curl https://chat.pocketcode.in/api/chat/completions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3.2:3b",
    "messages": [{"role":"user","content":"Hello"}]
  }'

API key is in Settings β†’ Account β†’ API Keys. Click "Create new secret key".

Useful Settings

SettingWhereWhat it does
Default modelSettings β†’ GeneralPicked when you start a new chat
Embedding modelAdmin β†’ Settings β†’ DocumentsSet to nomic-embed-text for RAG
System promptPer-chat βš™οΈ iconPin instructions for that conversation
TemperaturePer-chat βš™οΈ β†’ Advanced0 = deterministic, 1 = creative
User signupsAdmin β†’ Settings β†’ GeneralEnable/disable. Default is "pending approval"

This Setup's Quirks

OLLAMA_BASE_URL is preconfigured to http://ollama:11434 (internal Docker DNS). Open WebUI sees all your Ollama models automatically.
CPU mode means slower inference. On Hostinger KVM 8 (no GPU), expect 3B models at 8-15 tok/s, 7B at 3-6 tok/s. Use small models for chat, larger for one-off complex tasks.

Resources

🎨
Sim.ai
// Visual canvas for building AI agents and workflows Β· sim.pocketcode.in

Quick Start

  1. Go to sim.pocketcode.in
  2. Sign up (first time) or sign in
  3. Click + New Workflow
  4. Drag blocks from the left panel onto the canvas, connect them with lines
  5. Click Run in the top right to test

Block Types

BlockWhat it doesCommon use
AgentLLM call with optional toolsMost workflows start here
FunctionRun JavaScript codeData transformation between steps
APIHTTP request to any URLCall external services
ConditionIf/else branchingRoute based on agent output
LoopIterate over arrayProcess lists of items
ScheduleCron triggerRun workflow on schedule
WebhookHTTP endpoint triggerExternal app calls Sim.ai

Connecting to Models

Inside any Agent block:

Build Your First Workflow β€” Hello World

  1. Drag Agent block onto canvas
  2. Set model: llama3.2:3b
  3. Set system prompt: "You are a helpful assistant."
  4. Set user message: "What is 2+2?"
  5. Click Run β†’ see the output in the right panel

Add Tools to an Agent

Tools let agents take actions. Click an Agent block β†’ Tools tab β†’ add:

Common Tasks

Schedule a workflow to run daily:

  1. Add a Schedule block β†’ set cron (e.g. 0 9 * * * = daily 9am)
  2. Connect it to your first Agent block
  3. Click Deploy in top right
  4. Workflow now runs automatically

Expose workflow as a webhook (for external apps):

  1. Add a Webhook block as the trigger
  2. Deploy β†’ copy the webhook URL
  3. Any service can POST JSON to it β†’ triggers the workflow

This Setup's Quirks

PostgreSQL is shared. Sim.ai uses sim-db-1 (pgvector image) which also hosts databases for n8n, OpenClaw, and Open WebUI. Inspect via pgAdmin β€” connect to host sim-db-1, port 5432.
WebSocket realtime uses a separate subdomain. sim-realtime.pocketcode.in serves the live-collaboration WebSocket. If /workspace goes blank, check the subdomain's cert is valid and your browser cache is clear.

Resources

βš™οΈ
n8n
// Workflow automation with 400+ integrations Β· n8n.pocketcode.in

Quick Start

  1. Go to n8n.pocketcode.in
  2. Sign in with your owner account (first signup = owner)
  3. Click + Add workflow
  4. Click + in the canvas β†’ search for a trigger (e.g. "Manual")
  5. Add more nodes β†’ connect β†’ click Execute Workflow

Node Categories

CategoryExamplesUse for
TriggersManual, Webhook, Schedule, Email, SlackStarting events
AIOpenAI, Anthropic, Ollama, OpenRouterLLM calls
AppsSlack, Discord, Gmail, GitHub, NotionExternal service integration
DataPostgres, HTTP Request, RSS, CSVRead/write data
LogicIF, Switch, Loop, Merge, WaitFlow control
CodeFunction, Code (JS/Python)Custom transforms

Build Your First Workflow β€” Slack to Ollama

  1. Trigger: Slack Trigger (or Webhook for testing)
  2. Add Ollama Chat Model node β€” base URL http://ollama:11434, model llama3.2:3b
  3. Add AI Agent node β€” pass the Slack message as user input
  4. Add Slack Send Message node β€” post the AI response back
  5. Click Activate in top right

Common Tasks

Use Ollama directly in a workflow:

  1. Add Ollama Chat Model node
  2. Credentials β†’ New β†’ Base URL: http://ollama:11434
  3. Pick model from dropdown

Use cloud models (Claude/GPT) via OpenRouter:

  1. Add OpenAI Chat Model node (yes, even for Claude β€” OpenRouter is OpenAI-compatible)
  2. Credentials β†’ New β†’ Base URL: http://openrouter-proxy:4000, API key: any value (or your LiteLLM master key)
  3. Set model name to OpenRouter format (e.g. anthropic/claude-3.5-sonnet)

Query the shared Postgres:

  1. Add Postgres node
  2. Credentials: Host = sim-db-1, Port = 5432, DB = n8n, User/Pass from your .env
  3. Pick operation (Select, Insert, Update)

Receive webhooks from external apps:

  1. Use Webhook trigger node
  2. n8n shows you the test + production URLs
  3. For production (after Activate), URL is https://n8n.pocketcode.in/webhook/<your-path>

This Setup's Quirks

Encryption key persists in a volume. The n8n-data Docker volume holds the encryption key. All your stored credentials remain readable across container restarts. Back up this volume periodically.
WEBHOOK_URL must match the public domain. Check ~/ai-stack/n8n/run-n8n.sh β€” should have WEBHOOK_URL=https://n8n.pocketcode.in/ so webhook URLs n8n generates point to the right place.

Resources

🦞
OpenClaw
// Multi-model AI agent dashboard Β· openclaw.pocketcode.in

Quick Start

  1. Go to openclaw.pocketcode.in
  2. If first-time access, you may see a "Pair device" prompt β€” see "Device Pairing" below
  3. Type a message in the chat input β†’ send

Device Pairing

OpenClaw uses a device pairing system. New browsers/devices need to be approved from the CLI:

terminal.pocketcode.in or SSH
docker exec -it openclaw node /app/openclaw.mjs devices list
docker exec -it openclaw node /app/openclaw.mjs devices approve <UUID>

Common Tasks

Switch the active model:

  1. Settings (gear icon) β†’ Models
  2. Choose from local Ollama or configured cloud providers

Configure cloud model providers:

  1. Settings β†’ API Keys β†’ add Anthropic / OpenAI / OpenRouter keys
  2. Models become available in the chat selector

Start a new agent session:

  1. Click + New Session (sidebar)
  2. Pick a model + system prompt template (or custom)
  3. Sessions persist in the OpenClaw database

This Setup's Quirks

Ollama is pre-configured. OpenClaw is wired to use http://ollama:11434 via the ai-stack Docker network. Models pulled into Ollama appear automatically.
Browser plugin port 18791 not exposed in hosted edition. If you want browser-side relay features, they require additional setup not covered here.

Resources

✦
Claude Code
// Anthropic's CLI coding agent Β· Two ways to run it on this stack
Two flavours. This stack ships both: the official claude CLI installed directly on the host, and a containerised variant under claude-code that runs the same npm package inside a sandboxed image with ~/pocketcode-project bind-mounted. The host CLI is what you'll use most days; the container is useful when you want isolated env vars or to run the agent against a frozen working copy.

Quick Start β€” host CLI (recommended)

Installed in ~/.claude/bin/ on the VPS. Open the web terminal (or SSH in) and:

terminal
cd ~/pocketcode-project
claude

If this is the first time, you'll get a sign-in URL β€” open it in your local browser, paste the returned code. The CLI saves credentials under ~/.claude/; subsequent runs are instant.

Quick Start β€” containerised variant

Uses a long-running claude-code container with ~/pocketcode-project mounted at /workspace:

terminal
docker exec -it claude-code bash
claude

First run prompts for an Anthropic API key. Paste yours from console.anthropic.com β†’ API Keys (or sign in interactively, same as the host CLI).

Basic Usage

Working with the Stack

Host CLI: just cd into whichever directory matters and run claude. Everything in ~/pocketcode-project is directly accessible.

Containerised variant: ~/pocketcode-project is bind-mounted at /workspace inside the container.

example (works in both modes)
cd ~/pocketcode-project/services/sim          # or /workspace/services/sim in container
claude "review docker-compose.prod.yml and suggest improvements"

Common Tasks

Code review a file:

terminal
claude "review operations/manage/start-all.sh and find any race conditions"

Generate a new service config:

terminal
claude "create a docker-compose.yml for a Redis instance on the ai-stack network with persistence"

Debug a failing container:

terminal
docker logs sim-simstudio-1 --tail 100 > /tmp/logs.txt
claude "read /tmp/logs.txt and tell me why this container is restarting"

This Setup's Quirks

Costs money per token. Claude Code uses your Anthropic API key. Check /cost regularly. Sonnet 4 is ~$3/M input + $15/M output tokens.
Tab-completion for files works. When Claude asks for confirmation to edit a file, type the path with Tab. Files are mounted at /workspace.

Resources

⇆
OpenRouter (LiteLLM Proxy)
// OpenAI-compatible gateway to 300+ models Β· openrouter.pocketcode.in

What This Is

A LiteLLM proxy that exposes an OpenAI-compatible API on port 4000. You configure cloud providers (OpenAI, Anthropic, OpenRouter.ai, etc.) once, and any tool that speaks OpenAI's API can use them.

From Inside the Stack

Containers on the ai-stack network reach the proxy at:

internal URL
http://openrouter-proxy:4000

From Outside (via HTTPS)

public URL
https://openrouter.pocketcode.in

Configured Models

LiteLLM supports two configuration styles. Pick whichever matches your install:

A. Single-model CLI form β€” what the v1.0–v1.5 setup guide installs. Passes the model on the command line, no config file:

run-openrouter-proxy.sh β€” single-model form
docker run -d --name openrouter-proxy --network ai-stack \
  -e OPENROUTER_API_KEY="sk-or-v1-..." \
  -p 4000:4000 \
  ghcr.io/berriai/litellm:main-latest \
  --model openrouter/anthropic/claude-3.5-sonnet --port 4000

Any client calling /v1/chat/completions with any model name gets routed to the single configured model. Simple, good for a one-model lab.

B. Multi-model YAML form β€” if you want to expose several backends (Claude, GPT-4, Llama, etc.) through one proxy and have clients choose at request time. Mount a config file:

~/ai-stack/openrouter-proxy/litellm-config.yaml
model_list:
  - model_name: claude-sonnet-4
    litellm_params:
      model: anthropic/claude-sonnet-4-20250514
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: gpt-4o
    litellm_params:
      model: openai/gpt-4o
      api_key: os.environ/OPENAI_API_KEY
  - model_name: llama-3.3-70b
    litellm_params:
      model: openrouter/meta-llama/llama-3.3-70b-instruct
      api_key: os.environ/OPENROUTER_API_KEY

Then replace the trailing --model … CLI args in the run script with --config /app/config.yaml and mount the file with -v ~/ai-stack/openrouter-proxy/litellm-config.yaml:/app/config.yaml.

Common Tasks

Test from terminal:

terminal
curl https://openrouter.pocketcode.in/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "messages": [{"role":"user","content":"Hello"}]
  }'

Add a new model:

  1. Edit ~/ai-stack/openrouter-proxy/litellm-config.yaml β†’ add a new entry
  2. Restart the container: docker restart openrouter-proxy
  3. The new model_name is now usable everywhere

Use from Python:

python
from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.pocketcode.in/v1",
    api_key="any-string"  # disabled in default config
)

response = client.chat.completions.create(
    model="claude-sonnet-4",
    messages=[{"role": "user", "content": "Hello"}]
)
print(response.choices[0].message.content)

This Setup's Quirks

API keys live in env vars. Set in ~/ai-stack/openrouter-proxy/.env: ANTHROPIC_API_KEY=, OPENAI_API_KEY=, OPENROUTER_API_KEY=. Reference them in the YAML as os.environ/VAR_NAME.
Watch the costs. No native cost limits. Use LiteLLM's built-in budget feature in the config if needed. Check /spend endpoint for usage stats.

Resources

πŸ¦™
Ollama API
// Local LLM runtime Β· Direct REST API Β· ollama.pocketcode.in

What This Is

The raw Ollama API. Most users interact with Ollama via Open WebUI (recommended for chat) or via the SDK in code. This page covers direct API usage.

Endpoints

EndpointMethodPurpose
/api/tagsGETList installed models
/api/generatePOSTSingle-turn generation
/api/chatPOSTMulti-turn chat
/api/embeddingsPOSTGet vector embeddings
/api/pullPOSTDownload a new model
/api/showPOSTGet model details

Common Calls

List installed models:

curl
curl https://ollama.pocketcode.in/api/tags

Chat completion:

curl
curl https://ollama.pocketcode.in/api/chat -d '{
  "model": "llama3.2:3b",
  "messages": [{"role":"user","content":"Hello"}],
  "stream": false
}'

Generate embeddings (for RAG):

curl
curl https://ollama.pocketcode.in/api/embeddings -d '{
  "model": "nomic-embed-text",
  "prompt": "The text to embed"
}'

Pull a new model (from terminal, not API recommended):

terminal
docker exec ollama ollama pull llama3.2:3b
docker exec ollama ollama pull qwen2.5:7b
docker exec ollama ollama pull nomic-embed-text

Model Recommendations (CPU mode)

ModelSizeSpeed on KVM 8Use for
llama3.2:3b2.0 GB12-18 tok/sDefault chat, summaries
llama3.2:1b1.3 GB30-40 tok/sFast tasks, classification
qwen2.5:7b4.7 GB4-7 tok/sBetter reasoning
qwen2.5-coder:7b4.7 GB4-7 tok/sCode completion
nomic-embed-text274 MBEmbedding onlyRAG vector embeddings

This Setup's Quirks

Model data persists in a volume. Pulled models live in the ollama-data Docker volume. Survives container restarts. Inspect with docker exec ollama du -sh /root/.ollama.
CPU is the bottleneck. Concurrency is limited. Set OLLAMA_NUM_PARALLEL=2 in the run script if multiple users share the instance.

Resources

β—†
Qdrant
// Vector database for embeddings Β· qdrant.pocketcode.in

What This Is

A high-performance vector database for semantic search, RAG retrieval, and similarity matching. Stores embeddings (numerical representations of text, images, etc.) and finds nearest neighbors to a query vector. Bring your own embedding model β€” Ollama on this server, or any OpenAI-compatible cloud model via the OpenRouter proxy.

This Setup's Access Pattern

Qdrant is the only service that opens at its direct subdomain instead of through the launcher iframe wrapper used by the 8 other tools. The launch-pad card on pocketcode.in for Qdrant points straight to https://qdrant.pocketcode.in/dashboard.

Why direct, not iframe. Qdrant's Web UI makes cross-origin subresource fetches during sidebar mount (e.g. /dashboard/manifest.json) that fail when loaded inside the launcher iframe β€” the auth_gate redirects, the cross-origin redirect gets CORS-blocked, and the sidebar component never finishes rendering. Other services don't have this issue, so they stay in the iframe wrapper.

You still get full protection and feature parity with other services:

Web UI Layout

When you open qdrant.pocketcode.in/dashboard, the Web UI smart-routes:

Left sidebar items:

ItemWhat it's for
WelcomeOnboarding hero + getting-started cards. Shown when no collections exist.
ConsoleRun any REST API call interactively. Best place to learn the API.
CollectionsBrowse, create, inspect, snapshot, and query collections.
TutorialBuilt-in walkthroughs: Filtering (Beginner / Advanced / Full-Text), Multivector, Sparse, Hybrid, Multitenancy. Each tutorial creates a sample collection.
DatasetsImport sample data from remote snapshots β€” fastest way to populate a collection for experiments.
Access TokensQdrant's JWT-based per-key access control. Disabled here since we auth at Caddy.

Internal vs External Access

FromURLAuth
Another container on ai-stackhttp://qdrant:6333 (REST)
http://qdrant:6334 (gRPC)
None β€” internal network
Host shell on the VPShttp://localhost:6333
http://localhost:6334
None β€” host loopback
Your browser (Web UI)https://qdrant.pocketcode.in/dashboardMaster session cookie
External script via Caddyhttps://qdrant.pocketcode.in/<endpoint>Cookie header required
Security note. Qdrant ships with zero built-in authentication. Anything on the ai-stack Docker network can call any Qdrant endpoint without credentials. That's by design β€” it's a backend service. The Caddy auth_gate is the only thing standing between the public internet and your collections. Don't expose Qdrant's ports to the host network publicly, and keep your pocketcode_session cookie safe.

The v1.10 Query API

Since Qdrant v1.10, the unified Query API at POST /collections/{name}/points/query replaces the older endpoints (/points/search, /points/recommend, /points/scroll). The same endpoint handles:

The older endpoints still work for backward compatibility, but new code should use /points/query.

End-to-End: Create, Embed, Insert, Query

These examples run from the web terminal (or any host shell) using internal Docker networking β€” no auth needed. From outside the server, use https://qdrant.pocketcode.in with a valid session cookie.

1. Create a collection (768-dim vectors, cosine distance β€” matches nomic-embed-text):

terminal Β· curl from web terminal
curl -X PUT http://qdrant:6333/collections/my-docs \
  -H "Content-Type: application/json" \
  -d '{
    "vectors": {"size": 768, "distance": "Cosine"}
  }'

2. Create payload indexes BEFORE inserting data (recommended β€” see the callout below):

terminal
curl -X PUT http://qdrant:6333/collections/my-docs/index \
  -H "Content-Type: application/json" \
  -d '{"field_name": "category", "field_schema": "keyword"}'

curl -X PUT http://qdrant:6333/collections/my-docs/index \
  -H "Content-Type: application/json" \
  -d '{"field_name": "created_at", "field_schema": "datetime"}'

3. Get an embedding from Ollama, insert into Qdrant:

terminal
EMB=$(curl -s http://ollama:11434/api/embeddings \
  -d '{"model":"nomic-embed-text","prompt":"The quick brown fox"}' \
  | jq -c .embedding)

curl -X PUT http://qdrant:6333/collections/my-docs/points \
  -H "Content-Type: application/json" \
  -d "{
    \"points\": [{
      \"id\": 1,
      \"vector\": $EMB,
      \"payload\": {\"text\": \"The quick brown fox\", \"category\": \"animals\"}
    }]
  }"

4. Query (v1.10 Query API β€” /points/query):

terminal
QUERY_EMB=$(curl -s http://ollama:11434/api/embeddings \
  -d '{"model":"nomic-embed-text","prompt":"fast animal"}' \
  | jq -c .embedding)

curl -X POST http://qdrant:6333/collections/my-docs/points/query \
  -H "Content-Type: application/json" \
  -d "{
    \"query\": $QUERY_EMB,
    \"limit\": 5,
    \"with_payload\": true
  }"

Filtering β€” Where Most People Trip Up

Filters use three boolean groups combined with field conditions. The groups:

Condition types (most common):

ConditionMatches
match: { value }Exact equality on keyword / integer / bool field
match: { any: [...] }Field equals ANY value in list (OR within field)
match: { except: [...] }Field NOT in list
range: { gte, lte, gt, lt }Numeric range
match: { text: "phrase" } (v1.15+)Full-text phrase match (requires text payload index)
datetime_range: { gte, lte } (v1.8+)Datetime range queries
geo_bounding_box, geo_radius, geo_polygonGeographic queries
is_empty, is_null, has_idExistence / identity checks

Filter inside a query β€” find similar vectors but only in category animals, excluding inactive ones:

terminal
curl -X POST http://qdrant:6333/collections/my-docs/points/query \
  -H "Content-Type: application/json" \
  -d "{
    \"query\": $QUERY_EMB,
    \"limit\": 5,
    \"with_payload\": true,
    \"filter\": {
      \"must\":     [ {\"key\": \"category\", \"match\": {\"value\": \"animals\"}} ],
      \"must_not\": [ {\"key\": \"status\",   \"match\": {\"value\": \"inactive\"}} ]
    }
  }"

Nested payload fields use dot notation: {"key": "metadata.author", "match": {"value": "alice"}}.

Payload Indexes β€” The Performance Multiplier

If you filter on a payload field, you almost certainly want a payload index on it. Without one, Qdrant filters by scanning every point match β€” slow on large collections.

SchemaUse for
keywordTags, categories, string IDs β€” exact-match strings
integerNumeric IDs, counts
floatNumeric measurements
boolTrue/false flags
datetime (v1.8+)Timestamps for range queries
geoLat/long pairs
textFree-text fields (enables phrase + tokenized matching)
uuid (v1.11+)UUID-typed IDs
Create payload indexes BEFORE bulk-inserting data. Qdrant's HNSW vector index only registers filterable shortcuts for payload fields that have indexes when the points are first inserted. Adding the index after the fact still works (filtering will use it), but you miss the HNSW-level optimization unless you reindex. For best performance on large collections: create collection β†’ create payload indexes β†’ bulk insert.

Python Client (qdrant-client)

The official Python library is idiomatic and matches the REST API closely:

python Β· pip install qdrant-client
from qdrant_client import QdrantClient
from qdrant_client.models import (
    VectorParams, Distance, PointStruct,
    Filter, FieldCondition, MatchValue,
)

# From another container on ai-stack network:
client = QdrantClient(host="qdrant", port=6333)

# From host shell:
# client = QdrantClient(host="localhost", port=6333)

# 1. Create collection
client.create_collection(
    collection_name="my-docs",
    vectors_config=VectorParams(size=768, distance=Distance.COSINE),
)

# 2. Create payload index (before inserting)
client.create_payload_index(
    collection_name="my-docs",
    field_name="category",
    field_schema="keyword",
)

# 3. Upsert points
client.upsert(
    collection_name="my-docs",
    points=[
        PointStruct(
            id=1,
            vector=embedding_768,  # list of 768 floats from your embedding model
            payload={"text": "...", "category": "animals"},
        ),
    ],
)

# 4. Query (unified Query API)
result = client.query_points(
    collection_name="my-docs",
    query=query_embedding_768,
    limit=5,
    query_filter=Filter(must=[
        FieldCondition(key="category", match=MatchValue(value="animals")),
    ]),
).points

for hit in result:
    print(hit.id, hit.score, hit.payload["text"])

Common Tasks

List all collections:

terminal
curl http://qdrant:6333/collections

Collection info (size, indexed fields, vector params, point count):

terminal
curl http://qdrant:6333/collections/my-docs

Scroll through all points (paginated iteration):

terminal
curl -X POST http://qdrant:6333/collections/my-docs/points/scroll \
  -H "Content-Type: application/json" \
  -d '{"limit": 100, "with_payload": true, "with_vector": false}'

The response includes next_page_offset; pass it as offset on the next call to continue.

Delete a collection:

terminal
curl -X DELETE http://qdrant:6333/collections/my-docs

Snapshot a collection (for backup / portability):

terminal
curl -X POST http://qdrant:6333/collections/my-docs/snapshots

Embedding Models in This Stack

ModelVector sizeRun viaBest for
nomic-embed-text768OllamaGeneral-purpose, fast, on-server (default choice)
all-minilm384OllamaSmaller, faster, lower quality
mxbai-embed-large1024OllamaHigher quality, slower
text-embedding-3-small1536OpenRouter (cloud)Best quality / price ratio
text-embedding-3-large3072OpenRouter (cloud)Highest quality, costs more
FastEmbed (BM25, ColBERT)variesqdrant-client[fastembed]Sparse vectors for hybrid search
Vector size must match. If you create a collection at size: 768 and then try to insert a 1536-dim vector from OpenAI's text-embedding-3-small, Qdrant rejects the insert. Pick one model per collection, or use named-vector collections for multi-vector workflows.

Hybrid Search (Dense + Sparse)

Qdrant supports hybrid retrieval: combine a dense semantic vector with a sparse keyword/BM25 vector and fuse the rankings. The v1.10 Query API uses prefetch with reciprocal rank fusion or distance-based reranking. The built-in Hybrid Search tutorial in the dashboard (Tutorial β†’ Hybrid Search) walks through the full setup with sample data.

Multi-Tenancy

For SaaS-style use cases where one collection serves multiple isolated tenants, the recommended pattern:

  1. One large collection (not one per tenant β€” fewer indexes, better RAM use)
  2. Every point has a tenant_id in payload
  3. Create a keyword payload index on tenant_id
  4. Every query filters by tenant: filter: {"must": [{"key": "tenant_id", "match": {"value": "..."}}]}

Built-in tutorial: dashboard β†’ Tutorials β†’ Multitenancy.

Persistence

The qdrant container in this stack mounts a Docker volume at /qdrant/storage. Collections survive container restarts and reboots. To wipe everything (destructive β€” backs up nothing):

terminal Β· destructive
docker stop qdrant
docker rm qdrant
docker volume rm qdrant-data
bash ~/ai-stack/run-qdrant.sh
Note (2026-05-28) β€” the voice agent's voice_calls collection. Its indexing_threshold was lowered so even its small vector set builds a real HNSW index (Qdrant skips indexing tiny collections by default). With the index present, the dashboard's Graph / Visualize views now render for voice_calls instead of showing nothing.

Resources

🐘
pgAdmin
// PostgreSQL admin GUI Β· pgadmin.pocketcode.in

Quick Start

  1. Go to pgadmin.pocketcode.in
  2. Sign in with email/password from PGADMIN_DEFAULT_EMAIL / PGADMIN_DEFAULT_PASSWORD env vars
  3. Add a new server connection (first-time only)

Connect to the Shared Postgres

In pgAdmin: right-click Servers β†’ Register β†’ Server. Then:

TabFieldValue
GeneralNameai-stack-db (anything)
ConnectionHostsim-db-1
ConnectionPort5432
ConnectionMaintenance DBpostgres
ConnectionUsernameFrom your ~/ai-stack/sim/.env (POSTGRES_USER)
ConnectionPasswordFrom your ~/ai-stack/sim/.env (POSTGRES_PASSWORD)

What Databases Are There?

DatabaseUsed by
ailabGeneral-purpose (you can use freely)
simstudioSim.ai workflows & users
openclawOpenClaw sessions
n8nn8n workflows & credentials
ollama_resultsIf you stash Ollama outputs (optional)

Common Tasks

Run a query:

  1. Navigate to a database in the tree
  2. Right-click β†’ Query Tool
  3. Type SQL β†’ F5 to execute

Export a table to CSV:

  1. Right-click a table β†’ Import/Export Data
  2. Choose Export, set CSV format, browse for output location

Backup the whole stack DB:

terminal
docker exec sim-db-1 pg_dumpall -U postgres > ~/ai-stack/backups/all-$(date +%Y%m%d).sql

pgvector queries (Sim.ai uses this):

sql
-- Find vectors closest to a target
SELECT id, content, embedding <-> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY distance
LIMIT 5;

This Setup's Quirks

pgvector is pre-installed. The sim-db-1 container uses the pgvector/pgvector:pg17 image. CREATE EXTENSION vector; already done in each database.
Don't drop the postgres database. It's the maintenance DB. Use the per-service databases (ailab, simstudio, etc.) for your work.

Resources

πŸ–₯️
Web Terminal
// Real host bash shell in your browser Β· host-native ttyd via systemd Β· terminal.pocketcode.in

What This Is

A real bash shell on your VPS, running in your browser. The prompt shows your actual server hostname (root@srv1234:~#) β€” not a container ID β€” because ttyd runs as a host systemd service, not in Docker. Full root access: you can apt install packages, run systemctl, edit any file, and of course docker ps / docker exec any container. Perfect for quick admin from your phone, tablet, or any browser, without SSH client setup.

Quick Start

  1. Go to terminal.pocketcode.in
  2. If not logged in β†’ bounces you to pocketcode.in to log in
  3. Terminal loads β€” you see root@<your-vps-hostname>:~# prompt
  4. Type any command and press Enter

Keyboard Shortcuts

ShortcutWhat it does
Ctrl+Shift+CCopy selected text (browsers reserve plain Ctrl+C)
Ctrl+Shift+VPaste from clipboard
Right-clickContext menu with paste option
Ctrl+C in terminalInterrupt running process (e.g. stop a tail -f)
Ctrl+DExit current shell (reconnects automatically)

What You Can Do

Manage every container:

terminal
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
docker logs sim-simstudio-1 --tail 50
docker restart n8n
docker exec -it ollama bash

Edit any config file (no SSH needed):

terminal
nano ~/ai-stack/caddy/Caddyfile
nano ~/ai-stack/sim/.env
docker exec caddy caddy reload --config /etc/caddy/Caddyfile

Install host packages (you're root, no sudo needed):

terminal
apt install -y htop btop ncdu jq tmux tree
htop                          # live process viewer
ncdu /root/ai-stack           # disk usage explorer

Use stack aliases (already loaded β€” this is your real bash):

terminal
ai-start          # bring up all containers
ai-stop           # graceful shutdown
ai-doctor         # full diagnostic
ai-status         # quick container health snapshot
ai-logs n8n       # tail any service's logs
ai-reboot         # restart whole stack

Pull new Ollama models:

terminal
docker exec ollama ollama pull qwen2.5:7b
docker exec ollama ollama list

Run Claude Code from here:

terminal
docker exec -it claude-code bash
claude "review the start-all.sh script for race conditions"

Query the shared Postgres:

terminal
docker exec -it sim-db-1 psql -U postgres -d simstudio
\dt    -- list tables
\q     -- quit

Manage the ttyd service itself:

terminal Β· or from any SSH session
systemctl status ttyd       # is it running?
systemctl restart ttyd      # clears any stuck sessions
journalctl -u ttyd -f       # live log tail

How This Setup Works

You're on the host, not in a container. ttyd is installed as /usr/bin/ttyd via apt, supervised by systemd (/etc/systemd/system/ttyd.service). It binds to 0.0.0.0:7681 and Caddy reaches it via host.docker.internal:7681 β€” a name that resolves to the host's IP on the ai-stack Docker bridge.
Port 7681 is locked down externally. UFW rules allow connections from the two Docker bridge subnets (so Caddy can proxy) and from localhost, then deny all other inbound traffic on 7681. The public internet cannot reach ttyd directly β€” only through the gateway-protected https://terminal.pocketcode.in.
Anyone with the pocketcode.in login has full root. Trades isolation for convenience β€” fine for a personal lab where one person needs everything. If you ever share access or feel the threat model shift, consider switching back to a container-based ttyd or adding TOTP to the gateway.
WebSocket connection drops on flaky networks. If the terminal seems frozen, reload the page β€” the auth cookie keeps you signed in. ttyd auto-reconnects on minor blips.

Resources

πŸ—„οΈ
Databases
// Shared PostgreSQL + pgvector for the whole stack Β· sim-db-1:5432

Architecture

One PostgreSQL container β€” sim-db-1 running pgvector/pgvector:pg17 β€” hosts every database in the stack. Sim.ai brings it up (it's in the Sim.ai compose file), but pgAdmin, n8n, OpenClaw, and any custom workflow share the same instance. The container sits on two Docker networks simultaneously: sim_default (Sim.ai's own) and ai-stack (the rest of the stack), so anyone can reach it.

sim-db-1 Β· pgvector pg17 Β· 6 databases Β· port 5432

Services that need a database

Sim.ai Β· simstudio

n8n

OpenClaw

Open WebUI

livekit-agent Β· voice agent

voice-playground

mcp-voice-history

Your custom workflows

simstudio

n8n_db

openclaw_db

ailab

ollama_results

voice_pg
callers Β· calls Β· turns Β· tool_invocations
events Β· callbacks Β· airtable_outbox
kb_chunks 768-dim pgvector

One PostgreSQL container, six databases shared across services

The Six Databases

DatabaseOwner / Primary userWhat's inside
simstudioSim.aiWorkflows, agents, users, runs, knowledge base embeddings (pgvector)
n8nn8nWorkflows, credentials (encrypted), executions, webhooks
openclawOpenClawSessions, chat history, device pairings
ailabGeneral-purposeFree for your own use β€” custom tables, prototypes, scratch
ollama_resultsOptionalIf you stash inference outputs from cron jobs, n8n, etc.
voice_pg Β· v2livekit-agent + voice-playground + mcp-voice-historyOperational store for the voice agent: 8 tables β€” callers, calls, turns, tool_invocations, events, callbacks, airtable_outbox, and kb_chunks (768-dim pgvector embeddings of the product KB). Auto-bump trigger keeps callers.total_calls and last_seen in sync. Reads served read-only via mcp-voice-history for AI clients.

Connection Strings

FromHow to connect
Inside Docker (any service on ai-stack)postgres://USER:PASS@sim-db-1:5432/<db>
pgAdmin in browserHost = sim-db-1, Port = 5432 (Sim.ai's .env has the password)
Web terminal (CLI psql)docker exec -it sim-db-1 psql -U postgres -d <db>

pgvector Extension

Already installed in every database. Use it for semantic search inside Postgres without needing Qdrant for small datasets:

sql Β· create a vector table
-- nomic-embed-text returns 768-dim vectors
CREATE TABLE documents (
  id BIGSERIAL PRIMARY KEY,
  content TEXT,
  embedding vector(768),
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Index for fast similarity search
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);

-- Find nearest neighbors
SELECT id, content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY distance
LIMIT 5;

Common Tasks

Get a psql shell:

terminal
docker exec -it sim-db-1 psql -U postgres -d ailab

List all databases:

terminal
docker exec sim-db-1 psql -U postgres -l

Backup a single database:

terminal
docker exec sim-db-1 pg_dump -U postgres -Fc -d n8n > ~/backups/n8n-$(date +%Y%m%d).pgcustom

Backup everything (pg_dumpall):

terminal
docker exec sim-db-1 pg_dumpall -U postgres > ~/backups/all-$(date +%Y%m%d).sql

Restore from backup:

terminal
cat ~/backups/all-20260516.sql | docker exec -i sim-db-1 psql -U postgres

Create a new database for your project:

sql
-- via pgAdmin Query Tool or psql shell
CREATE DATABASE my_project;
\c my_project
CREATE EXTENSION vector;

Connection Pool Limits

Default Postgres config allows ~100 concurrent connections. Sim.ai + n8n + OpenClaw typically use ~30. If you start running into "too many connections" errors, increase the limit:

terminal
docker exec sim-db-1 psql -U postgres -c "ALTER SYSTEM SET max_connections=200;"
docker restart sim-db-1

This Setup's Quirks

sim-db-1 lives in the Sim.ai compose file. If you docker compose down from ~/ai-stack/sim/, the database goes away. ai-stop handles this gracefully (stops dependent services first), but if you ever manually run compose commands, be aware.
Dual network membership. The container is connected to both sim_default AND ai-stack. After a Sim.ai recompose, it sometimes drops the ai-stack attachment β€” ai-start reconnects it automatically. If you see "host sim-db-1 not found" errors from n8n or OpenClaw, run docker network connect ai-stack sim-db-1 manually.
n8n's encryption key is in a Docker volume (n8n-data), not in Postgres. Back up that volume separately if you care about preserving stored credentials across rebuilds.

Resources

πŸ”—
Service Communication
// How services talk to each other inside Docker Β· ai-stack network Β· internal DNS

The Network Architecture

Every container in the stack is on the ai-stack Docker network. Docker provides automatic DNS resolution between containers using their container names. Sim.ai's compose stack has its own internal network sim_default, and sim-db-1 straddles both so services on either network can reach it.

ai-stack network

Applications

HTTPS 443

host.docker.internal

host.docker.internal

WSS

bridge

bridge

sim_default network

sim-simstudio

sim-realtime

sim-redis

MCP layer Β· L6

mcp-postgres

mcp-airtable

mcp-web-search

mcp-yt-dlp

mcp-sim

mcp-n8n

mcp-voice

mcp-voice-history

Core services

auth-gateway

open-webui

openclaw

pgadmin

n8n

qdrant

openrouter-proxy

ollama

Browser

caddy
:443

yt-dlp-ui

voice-playground

livekit-agent

sim-db-1 Β· pgvector pg17
6 DBs including voice_pg

ttyd Β· host

code-server Β· host

LiveKit Cloud Β· Twilio Β· Deepgram Β· Anthropic

Docker network layout β€” ai-stack, sim_default, the bridge container

Internal Hostnames

Inside any container on ai-stack, these hostnames resolve automatically:

HostnamePortWhat it isCommon use
ollama11434Ollama APILLM inference
open-webui8080Open WebUIβ€”
auth-gateway7000Gateway serviceCaddy forward_auth
sim-db-15432PostgreSQLAny service's database
sim-simstudio-13000Sim.ai web appCaddy upstream
sim-realtime-13002Sim.ai WebSocketWorkspace live collab
sim-redis-16379Redis (Sim.ai)Sim.ai queues, sessions
openclaw8080OpenClawβ€”
n8n5678n8nβ€”
openrouter-proxy4000LiteLLM proxyCloud model gateway
qdrant6333Qdrant RESTVector search
pgadmin80pgAdminβ€”
terminal7681Web terminal (ttyd)β€”
caddy80, 443Reverse proxyβ€”

Why Use Internal Hostnames?

Common Patterns

n8n calling Ollama: In n8n's Ollama Chat Model credentials, set Base URL to:

n8n credential value
http://ollama:11434

n8n calling OpenRouter (cloud models) via the proxy:

n8n OpenAI Chat Model credentials
Base URL: http://openrouter-proxy:4000
API Key:  any-non-empty-string

Sim.ai calling Ollama: already wired via Sim.ai's OLLAMA_URL=http://ollama:11434 env var.

Custom script calling everything from a terminal:

terminal Β· inside any ai-stack container
curl http://ollama:11434/api/tags
curl http://qdrant:6333/collections
curl http://openrouter-proxy:4000/v1/models

Connecting from a brand-new container you create: Add it to the ai-stack network:

terminal
docker run -d --name my-app \
  --network ai-stack \
  -e DB_URL=postgres://postgres:PASS@sim-db-1:5432/ailab \
  -e LLM_URL=http://ollama:11434 \
  my-image:latest

Adding a Service to ai-stack

  1. Run with --network ai-stack flag (or networks: [ai-stack] in compose)
  2. Once started, it can reach every other container by name
  3. If it needs to be public via HTTPS, add a Caddyfile block per Tab 13

Auto-SSO Across Services

The gateway at pocketcode.in ships with optional auto-SSO: when you sign in at the apex, hidden iframes silently log you into Sim.ai, Open WebUI, and n8n using credentials stored in ~/ai-stack/auth-gateway/.env. Three things are happening:

PhaseWhat runsHow it works
1. Master loginGateway sets pocketcode_session cookie scoped to .pocketcode.inOne cookie sent to every subdomain β€” Caddy's forward_auth sees it and lets you through
2. Bootstraphome.html creates hidden iframes pointing at https://<svc>.pocketcode.in/_sso_initCaddy proxies /_sso_init to the gateway; gateway POSTs to the service's login API internally, forwards Set-Cookie back through Caddy β†’ cookie scoped to <svc>.pocketcode.in
3. Watchdogiframes re-load every 2 min and on tab focusIf you got logged out of any service in the meantime, the next refresh logs you back in. No way to "stay out" of a service while the master session is alive.

Which services auto-SSO works for

ServiceSSO?Why / why not
Sim.aiβœ“BetterAuth's POST /api/auth/sign-in/email accepts JSON, returns a session cookie
Open WebUIβœ“POST /api/v1/auths/signin JSON + cookie-based auth
n8nβœ“POST /rest/login JSON + n8n-auth session cookie
OpenClawβ€”Device pairing only, no credential-based login
pgAdminβ€”Requires CSRF token (two-step login); skipped for simplicity. Sessions persist 30+ days though, so a one-time login.
Ollama / OpenRouterβ€”No login β€” gated by Caddy's auth_gate directly
Qdrantβ€”No login β€” gated by auth_gate AND has a session-poll script injected into its HTML via Caddy's replace-response plugin (v1.6). See docs-page Qdrant tab for the full access pattern.

The /_sso_init endpoint

Each SSO-enabled subdomain has this Caddyfile block:

caddyfile Β· sim.pocketcode.in pattern
sim.pocketcode.in {
    import auth_gate
    handle /_sso_init {
        reverse_proxy auth-gateway:7000 {
            rewrite /sso-init/sim
        }
    }
    reverse_proxy sim-simstudio-1:3000
}

Browser loads /_sso_init in a hidden iframe β†’ Caddy proxies to gateway β†’ gateway logs into Sim.ai server-side β†’ response carries Set-Cookie for the user β†’ iframe posts a status message back to home.html.

What logging out does

Troubleshooting

"Host not found" / "getaddrinfo ENOTFOUND":

"Connection refused" but container is up:

n8n shows "Failed to load model catalog" briefly at start:

Resources

πŸ“
Shared Files
// Two cross-service file paths Β· Docker volume + bind mount Β· /shared and /uploads

What's Mounted Where

Two file paths are mounted inside every relevant service container, so data can flow between them without HTTP roundtrips:

Path inside containersBacked byTypeUse for
/sharedai-shared-data Docker volumeManaged volumeCross-service intermediate data, persistent across restarts
/uploads/root/ai-stack/uploads bind mountHost bind mountFiles dropped from your Mac via scp, visible to all services

When to Use Which

Use /shared for service-to-service data. One service writes, another reads. Stays inside Docker. No host pollution. Survives reboots. Examples: n8n stashes a CSV, Sim.ai picks it up; Ollama caches an embedding, custom script reads it.
Use /uploads for files coming from you (the human). scp from your Mac, drop into ~/ai-stack/uploads/, every service sees it instantly at /uploads. Examples: PDFs for Open WebUI to ingest, CSVs for n8n to process, datasets for Qdrant to embed.

How to Use From Each Service

From the web terminal:

terminal.pocketcode.in
ls /shared          # Inside the terminal container
ls /uploads
ls ~/ai-stack/uploads   # Same as /uploads β€” terminal's /root is mounted

From n8n (Read/Write Files node): point at /shared/output.csv or /uploads/input.csv directly. n8n already has both mounts.

From Sim.ai workflows: use the Function block with Node.js fs:

javascript Β· in a Sim.ai Function block
const fs = require('fs');
const content = fs.readFileSync('/uploads/data.json', 'utf-8');
const data = JSON.parse(content);
return { items: data };

From Open WebUI: upload files through the UI (paperclip icon) β€” they're stored in the open-webui-data volume separately. To make a host file available, copy it to /uploads first, then upload via the UI.

From OpenClaw and custom scripts: Same β€” read/write to /shared or /uploads within their containers.

Common Workflows

Drop a file from your Mac, process it in n8n:

bash Β· on Mac
scp ~/Documents/data.csv root@YOUR_SERVER_IP:~/ai-stack/uploads/data.csv

In n8n: Read Binary File node β†’ /uploads/data.csv β†’ process. n8n sees the file immediately, no restart needed.

n8n writes a result, Sim.ai picks it up:

Build a dataset for Qdrant:

  1. scp folder of PDFs to ~/ai-stack/uploads/pdfs/
  2. From terminal: docker exec -it <some-python-container> python ingest.py /uploads/pdfs
  3. Script chunks files, embeds via Ollama at http://ollama:11434, posts to Qdrant at http://qdrant:6333

Inspecting Each From the Host

The bind-mount (/uploads) is trivially visible on the host:

terminal Β· on host
ls -la ~/ai-stack/uploads/
du -sh ~/ai-stack/uploads/

The Docker volume (/shared) lives under Docker's internal storage:

terminal Β· on host
docker volume inspect ai-shared-data
# shows: Mountpoint /var/lib/docker/volumes/ai-shared-data/_data
ls -la /var/lib/docker/volumes/ai-shared-data/_data/

Backup & Restore

Backup /uploads (host bind-mount β€” just tar it):

terminal
tar czf ~/backups/uploads-$(date +%Y%m%d).tar.gz -C ~/ai-stack uploads/

Backup /shared (Docker volume β€” needs a helper container):

terminal
docker run --rm \
  -v ai-shared-data:/data \
  -v ~/backups:/backup \
  alpine tar czf /backup/shared-$(date +%Y%m%d).tar.gz -C /data .

Restore the volume:

terminal
docker run --rm \
  -v ai-shared-data:/data \
  -v ~/backups:/backup \
  alpine sh -c "cd /data && tar xzf /backup/shared-20260516.tar.gz"

Permissions Note

Containers may run as different users. Open WebUI runs as a non-root UID; n8n runs as node (UID 1000); pgAdmin runs as pgadmin (UID 5050). Files created by one service may be owned by an unfamiliar UID. If you hit permission errors, chmod -R a+rw on the affected directory from the terminal usually fixes it (these mounts aren't security boundaries, just convenience).

This Setup's Quirks

The volume isn't auto-cleaned. /shared grows over time. Periodically check with du -sh /var/lib/docker/volumes/ai-shared-data/_data/ from the host and clean up old files.
Not all services have both mounts. Cross-check by inspecting: docker inspect <container> --format='{{range .Mounts}}{{.Destination}} {{end}}'. If your service needs them and they're missing, edit the run script to add -v ai-shared-data:/shared -v /root/ai-stack/uploads:/uploads and recreate.

Resources

πŸ”§
Integration Recipes
// Cross-service patterns and common workflows

Recipe 1: Slack-to-Ollama Chatbot (via n8n)

Listen for Slack messages, route through Ollama, respond.

  1. In n8n: Slack Trigger β†’ AI Agent (Ollama, model llama3.2:3b) β†’ Slack Send Message
  2. Use the message text as user input to the agent
  3. Pass the agent output back to Slack channel

Recipe 2: Document Q&A System (Open WebUI + Ollama)

  1. Pull embedding model: docker exec ollama ollama pull nomic-embed-text
  2. In Open WebUI: Settings β†’ Documents β†’ Embedding Model = nomic-embed-text
  3. Workspace β†’ Knowledge β†’ New Collection β†’ upload PDFs
  4. In chat, reference with #collection-name

Recipe 3: Scheduled Web Scraping (n8n + OpenRouter)

  1. n8n: Schedule trigger (daily 9am) β†’ HTTP Request (fetch URL) β†’ AI Agent (summarize via OpenRouter Claude) β†’ Send Email/Slack
  2. Claude does the summarization since it's better at structured output
  3. Costs ~$0.01-0.05 per run depending on page size

Recipe 4: Custom RAG Pipeline (Ollama + Qdrant + Sim.ai)

  1. Create a Qdrant collection (size 768 for nomic-embed-text)
  2. In Sim.ai: build a workflow that takes a query, embeds via Ollama, searches Qdrant, passes top-K results + query to an Agent block
  3. Agent uses retrieved context + query to produce grounded answer

Recipe 5: Multi-Model Comparison (LiteLLM)

Compare outputs from local Ollama vs cloud Claude vs GPT side-by-side.

terminal
PROMPT="Explain quantum computing in 2 sentences"

# Local Ollama
curl -s http://localhost:11434/api/generate \
  -d "{\"model\":\"llama3.2:3b\",\"prompt\":\"$PROMPT\",\"stream\":false}" \
  | jq -r .response

# Cloud Claude via OpenRouter proxy
curl -s http://localhost:4000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d "{\"model\":\"claude-sonnet-4\",\"messages\":[{\"role\":\"user\",\"content\":\"$PROMPT\"}]}" \
  | jq -r '.choices[0].message.content'

Recipe 6: Voice Pipeline (Open WebUI + Whisper)

Open WebUI supports voice input via Whisper. To enable:

  1. Settings β†’ Audio β†’ STT Engine: select "Local Whisper"
  2. Pick a Whisper model size (base / small / medium)
  3. Click 🎀 icon in chat input β†’ speak β†’ transcribed and sent

Recipe 7: Backup & Restore the Stack

Backup (run daily as a cron job or n8n workflow):

bash
#!/bin/bash
DATE=$(date +%Y%m%d-%H%M)
mkdir -p ~/backups/$DATE

# Postgres dump
docker exec sim-db-1 pg_dumpall -U postgres > ~/backups/$DATE/postgres.sql

# Critical volumes (n8n encryption key, Open WebUI chats, etc.)
docker run --rm -v n8n-data:/data -v ~/backups/$DATE:/backup \
  alpine tar czf /backup/n8n-data.tar.gz -C /data .

docker run --rm -v open-webui-data:/data -v ~/backups/$DATE:/backup \
  alpine tar czf /backup/open-webui-data.tar.gz -C /data .

# Caddy data (certs)
docker run --rm -v caddy_data:/data -v ~/backups/$DATE:/backup \
  alpine tar czf /backup/caddy_data.tar.gz -C /data .

# Compress everything
tar czf ~/backups/$DATE.tar.gz -C ~/backups $DATE
rm -rf ~/backups/$DATE

Recipe 8: Manage Stack from the Web Terminal

Everything in this guide can be done from terminal.pocketcode.in in your browser:

useful aliases
ai-start          # bring up all containers
ai-stop           # shut down cleanly
ai-doctor         # diagnostic scan
ai-logs SERVICE   # tail any service's logs
ai-status         # quick health check
Going deeper? The web terminal has full Docker socket access. docker ps, docker exec, docker logs all work. Edit any config file under ~/pocketcode-project/.
βš™
Claude Code MCPs (v2 layer Β· added 2026-05-20)
// HTTP MCP servers β€” same URL works from VPS Claude Code, Mac Claude Desktop, sim agents, future LiveKit voice agent

The PocketCodeIn stack runs HTTP MCP servers behind Caddy on mcp-*.pocketcode.in subdomains. They are NOT browser-interactive β€” they're API endpoints consumed by LLM clients. Every MCP uses the same authentication: Authorization: Bearer <MCP_BEARER_TOKEN>, where the token lives in ~/pocketcode-project/secrets/secrets.json under mcp.bearer_token.

Why HTTP MCPs? Single endpoint, multiple consumers. Claude Code in a tmux session on the VPS, Claude Desktop on your Mac 5,000 km away, and a sim.ai agent in a sibling container all hit the same URL with the same bearer token. One source of tool definitions, one auth path.

mcp-postgres

URL: https://mcp-postgres.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-postgres/
Connects as: postgres superuser β†’ sim-db-1 (5 databases)

Tools exposed

ToolPurpose
list_databases()Returns the 5 db names: simstudio, n8n, openclaw, ailab, ollama_results
list_tables(database)Returns [{schema, table}, …] across all non-system schemas
describe_table(database, table, schema='public')Returns [{column, type, nullable, default}, …]
query(sql, database='simstudio')Executes SELECT/INSERT/UPDATE/DELETE/DDL. Capped at 1000 rows. 30-second statement timeout.

Using from Claude Code (on the VPS)

Already wired up via ~/pocketcode-project/.mcp.json. To launch a Claude Code session that picks it up:

bash Β· in a tmux session on the VPS
tmux new -s claude
cd ~/pocketcode-project
source secrets/secrets.env    # exports MCP_BEARER_TOKEN
claude                        # reads .mcp.json, connects to mcp-postgres over HTTPS

In the Claude Code session, the MCP tools show up under /mcp. Ask Claude things like "list all tables in the n8n database" or "how many workflows are saved in sim?"

Using from Claude Desktop (on your Mac)

Add to your Claude Desktop MCP config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

json Β· macOS Claude Desktop config
{
  "mcpServers": {
    "pocketcode-postgres": {
      "type": "http",
      "url": "https://mcp-postgres.pocketcode.in/mcp",
      "headers": {
        "Authorization": "Bearer <PASTE MCP_BEARER_TOKEN HERE>"
      }
    }
  }
}

Get the token value with jq -r .mcp.bearer_token ~/pocketcode-project/secrets/secrets.json on the VPS, then paste into the Mac config. Restart Claude Desktop.

Quick verification (manual)

bash Β· from anywhere
TOKEN="<your MCP_BEARER_TOKEN>"

# Health check β€” no auth needed
curl https://mcp-postgres.pocketcode.in/health
# β†’ OK

# List databases β€” proper MCP handshake then tool call
INIT=$(curl -s -D /tmp/h https://mcp-postgres.pocketcode.in/mcp \
  -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"curl","version":"0"}}}')

SESSION=$(grep -i '^mcp-session-id:' /tmp/h | awk '{print $2}' | tr -d '\r')

curl -s -X POST https://mcp-postgres.pocketcode.in/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

curl -s -X POST https://mcp-postgres.pocketcode.in/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_databases","arguments":{}}}'
Security note: mcp-postgres connects as postgres superuser (full DDL). Any prompt-injection attack on a connected LLM could in principle execute destructive SQL. This is a deliberate project trade-off (consistency with v1.0 services) β€” see CLAUDE.md. If you ever expose this MCP to less-trusted callers, add a read-only role and switch the URL.

mcp-airtable

URL: https://mcp-airtable.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-airtable/
Default base: the call_logs base from secrets.json β†’ airtable.base_call_logs. Override per-tool with the base_id argument.

Tools (full CRUD)

ToolPurpose
list_bases()All bases the PAT can access
list_tables(base_id='')Tables + field schemas in a base
describe_table(table, base_id='')Schema of one table by ID or name
list_records(table, base_id='', view='', max_records=100, filter_formula='')Records with optional view + filter; cap 1000
get_record(table, record_id, base_id='')Single record by ID
create_record(table, fields, base_id='')Insert; typecast=true so strings auto-coerce
update_record(table, record_id, fields, base_id='')PATCH β€” only listed fields change

Example: log a call from the voice agent (Phase 4 use case)

Claude tool call (conceptual)
create_record(
  table="Call Logs",
  fields={
    "caller_id": "+15551234567",
    "timestamp": "2026-05-20T11:23:45Z",
    "duration_sec": 142,
    "transcript": "Caller: Hi, I need help with...",
    "summary": "Customer asked about pricing tier; agent quoted KVM 4 plan."
  }
)
Airtable setup note: the default Airtable starter table is "Table 1" with generic fields (Name, Notes, Assignee, Status, …). To use this MCP for call logs, either rename "Table 1" β†’ "Call Logs" and add the fields above, or pass any other table name that exists in your base.

URL: https://mcp-web-search.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-web-search/
Backend: Brave Search API. Free tier 2000 queries/month. Check quota at api.search.brave.com/app/dashboard.

Tool

ParamTypeNotes
querystr (required)The search terms
countint = 101-20 (Brave per-page max). Defaults to 10.
freshnessstr = ''Empty = any time. Or day / week / month / year
countrystr = ''ISO-2 like IN, US, GB. Empty = Brave's auto-detect.
safesearchstr = 'moderate'off / moderate / strict

Returns {query, total_results, results: [{title, url, description, age, language, is_source_local}]}. On rate-limit (429), returns {error: 'Brave Search rate limit hit', hint: ...} so the LLM can react gracefully.

Example queries

Claude tool calls (conceptual)
web_search(query="latest news on Anthropic")
web_search(query="Hostinger KVM 8 pricing", country="IN")
web_search(query="MCP protocol spec", freshness="month", count=20)
web_search(query="open source TTS comparison", safesearch="strict")

mcp-sim

URL: https://mcp-sim.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-sim/
What it is: the architectural replacement for the dropped mcp-n8n. Wraps sim.ai's REST API for workflow CRUD + execution β€” the flagship "Claude Code builds workflows" use case.

Tools (9)

ToolPurpose
list_workspaces()Workspaces the operator can access (start here)
list_workflows(workspace_id='', scope='')Workflows, optionally filtered by workspace or scope
get_workflow(workflow_id)Full payload (definition + metadata)
get_workflow_state(workflow_id)Live state snapshot (nodes, edges, runtime)
create_workflow(name, workspace_id, color?, folder_id?, description?)New (empty) workflow β€” fill in nodes/edges via UI or future state endpoint
update_workflow(workflow_id, name?, color?, description?, folder_id?)PATCH metadata
delete_workflow(workflow_id)Remove
execute_workflow(workflow_id, input?)Trigger run; returns result or execution_id
get_workflow_logs(workflow_id, limit=20)Recent execution logs

Auth β€” why service-account login (not API key)

Sim has two auth paths: personal API keys and BetterAuth session cookies. API keys are restricted to per-workflow execution endpoints β€” they cannot list, create, update, or delete workflows. CRUD requires a real user session. So mcp-sim uses session cookies via auto-login with the operator's credentials (same ones auth-gateway uses for browser SSO). 30-day session TTL; auto-refreshes on 401. SIM_SSO_EMAIL + SIM_SSO_PASSWORD come from secrets.json via regenerate-env.sh.

Example flow β€” "Claude Code builds a workflow"

Conceptual tool calls in a Claude Code session
# 1. Discover available workspaces
list_workspaces()
# β†’ {workspaces: [{id: "4a5acb0a...", name: "Nishant's Workspace", role: "owner"}]}

# 2. List what's already there
list_workflows(workspace_id="4a5acb0a...")
# β†’ {workflows: [...]}

# 3. Create a new workflow shell
create_workflow(name="YouTube Summarizer", workspace_id="4a5acb0a...",
                description="Take a YouTube URL, return a one-paragraph summary")
# β†’ {id: "wf_xyz...", name: "YouTube Summarizer", ...}

# 4. (Use sim's UI or the state endpoint to populate nodes/edges β€”
#    this is the part that's still better done visually for now)

# 5. Execute it
execute_workflow(workflow_id="wf_xyz...", input={"url": "https://youtube.com/watch?v=..."})
# β†’ {success: true, output: "...", execution_id: "..."}

# 6. Inspect logs
get_workflow_logs(workflow_id="wf_xyz...")
Current limitation: create_workflow creates an EMPTY workflow. Populating nodes/edges programmatically still requires the /api/workflows/{id}/state endpoint, which this MCP doesn't yet wrap. For now the pattern is: Claude creates the shell + describes the workflow β†’ you fill nodes visually in sim's UI. A future iteration could add a set_workflow_state tool to close that gap.

mcp-n8n

URL: https://mcp-n8n.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-n8n/
What it is: companion to mcp-sim β€” wraps n8n's public REST API for workflow CRUD + execution. Lives alongside mcp-sim; pick whichever workflow engine fits the task.

Tools (12)

ToolPurpose
list_workflows(active?, tags?, limit=100)Filtered list
get_workflow(workflow_id)Full nodes/connections/settings
create_workflow(name, nodes?, connections?, settings?)New (minimum: just name)
update_workflow(workflow_id, ...)PUT-semantics; fetches current, merges, sends
delete_workflow(workflow_id)Remove
activate_workflow(workflow_id)Enable triggers
deactivate_workflow(workflow_id)Disable triggers
execute_workflow(workflow_id, input?)One-shot run (fallback guidance if API version doesn't support)
list_executions(workflow_id?, status?, limit=50)Recent runs
get_execution(execution_id, include_data=false)Single run; include_data for per-node I/O
list_credentials()Schema metadata (no values)
list_tags()Workflow tags

Auth

Uses n8n's public API key (header X-N8N-API-KEY). Minted programmatically via n8n's /rest/api-keys endpoint with all 68 scopes granted. Stored at secrets.json β†’ services.n8n.api_key, exported as N8N_API_KEY by regenerate-env.sh.

vs mcp-sim: both MCPs are live. mcp-sim wraps sim.ai (visual agent flows, BetterAuth); mcp-n8n wraps n8n (huge community node ecosystem, X-API-KEY auth). Use whichever fits the integration you need.

yt-dlp-ui (browser UI for downloads/transcripts)

URL: https://ytdl.pocketcode.in (master-login gated)
Source: ~/pocketcode-project/services/yt-dlp-ui/
What it is: a 3-tab single-page web app for using yt-dlp from the browser β€” with real-time progress bars on downloads. Companion to mcp-yt-dlp (which serves LLM agents). Same dependencies + shared data/downloads/ directory.

Three tabs

  1. πŸ“Ž Paste URL β€” paste any yt-dlp-supported URL, click "Get info" to fetch metadata (title, channel, duration, views, thumbnail). Then options panel: resolution dropdown (240β†’2160 or "max"), audio format/quality dropdowns, optional trim (HH:MM:SS start/end). Three action buttons: Download Video, Extract Audio, Get Transcript.
  2. πŸ”Ž Search YouTube β€” search query + limit β†’ grid of result cards (title, channel, duration, view count). Clicking a card loads it into the URL tab.
  3. πŸ“ Recent files β€” lists everything in data/downloads/ with one-click "Download" buttons that stream the file to the user's computer.

Real-time progress

When you click Download Video or Extract Audio, yt-dlp-ui runs yt-dlp directly as a subprocess with --newline --progress-template. Each progress line is parsed and emitted as a Server-Sent Event to the browser. The on-screen progress bar fills in real time with downloaded/total MB, current speed, and ETA. Post-processing stages (ExtractAudio, Merger) are shown as status text. On completion, you see a "Done" indicator with a direct "Download to your computer" link.

How it relates to mcp-yt-dlp

Aspectmcp-yt-dlpyt-dlp-ui
AudienceLLM agents (Claude Code, sim, voice)Humans in a browser
TransportHTTP+SSE MCP protocolHTTP (FastAPI) + SSE for downloads
AuthBearer token (shared MCP token)Master JWT cookie via Caddy auth_gate
Subdomainmcp-yt-dlp.pocketcode.inytdl.pocketcode.in
Downloads dir/downloads (host: data/downloads/)Same β€” shared bind-mount
Cookies.txtAuto-detected at /downloads/cookies.txtSame β€” shared

Calling yt-dlp-ui for search/metadata internally proxies to mcp-yt-dlp via HTTP+bearer, so there's one canonical implementation of those tools. Downloads run yt-dlp directly in yt-dlp-ui because real-time progress streaming needs subprocess management that doesn't fit MCP's request/response shape.

Browser-side bearer token: the bearer NEVER touches the browser. yt-dlp-ui reads it from secrets.env server-side and uses it only when calling mcp-yt-dlp. The browser sees only the cookie-authenticated UI surface (/api/search, /api/download/*, etc.).

mcp-yt-dlp

URL: https://mcp-yt-dlp.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-yt-dlp/
Design lineage: tool surface inspired by kevinwatt/yt-dlp-mcp (MIT) β€” reimplemented natively in Python to match this project's existing FastMCP + subprocess pattern. No Node.js / Deno runtime needed.

Tools (8)

ToolPurpose
ytdlp_search_videos(query, limit=10)YouTube search; capped at 50 results
ytdlp_list_subtitle_languages(url)Manual + auto-generated subtitle language codes available
ytdlp_download_video_subtitles(url, language='en')VTT subtitle text β€” full content returned
ytdlp_download_transcript(url, language='en')Plain text transcript (timing stripped) β€” fastest "what's said" extraction
ytdlp_download_video(url, resolution='720', start='', end='')Download video file; optional time-range trim. Resolutions: 240/360/480/720/1080/1440/2160/max
ytdlp_download_audio(url, format='mp3', quality='192')Extract audio. format: mp3/m4a/opus/wav/flac. quality: 128/192/256/320 kbps
ytdlp_get_video_metadata(url)Full yt-dlp JSON metadata (id, title, formats, thumbnails, etc.)
ytdlp_get_video_metadata_summary(url)Compact LLM-friendly summary (title, channel, duration, view count, description preview)

Downloads location

Files written by download_video and download_audio land in ~/pocketcode-project/data/downloads/ on the host. The tool response includes both container path (/downloads/<file>) and host path so other services can reference them.

YouTube anti-bot on cloud IPs: per-video calls (metadata, subtitles, downloads) may fail with "Sign in to confirm you're not a bot" from VPS IPs. Search works regardless (uses a different YouTube endpoint).

Fix: export a YouTube cookies.txt from your logged-in browser (use the "Get cookies.txt LOCALLY" extension), then:
scp ~/Downloads/cookies.txt root@VPS:/root/pocketcode-project/data/downloads/
The MCP picks it up automatically on the next call. No container restart needed.

Example tool calls

Claude tool calls (conceptual)
ytdlp_search_videos(query="livekit voice agent tutorial", limit=5)

ytdlp_get_video_metadata_summary(url="https://youtube.com/watch?v=...")
# β†’ {title, channel, duration_str, view_count, description_preview, ...}

ytdlp_download_transcript(url="https://youtube.com/watch?v=...", language="en")
# β†’ {url, language, text}   (timing-stripped, ready to summarize)

ytdlp_download_audio(url="...", format="mp3", quality="192")
# β†’ file lands at ~/pocketcode-project/data/downloads/<id>_<title>.mp3

voice-playground β€” browser harness + outbound dialer

URL: https://voice.pocketcode.in (master-login gated)
Source: ~/pocketcode-project/services/voice-playground/
What it is: a single-page app with two tabs β€” Audio test with agent (talk to Adrian from your browser) and Outbound AI call (dial a real phone, listen + watch live transcript, schedule callbacks, view call history).

Architecture (end-to-end)

The diagram below traces every path the playground supports β€” browser-only test calls (left side), outbound PSTN dial-outs (middle), the agent's runtime pipeline (LLM/STT/TTS + tools), and the operational data plane (voice_pg + Qdrant + Airtable outbox + scheduled-callback auto-dialer).

Operational data plane

livekit-agent worker

LiveKit Cloud + Twilio

voice-playground

kb_search embed

cosine search

caller_memory

schedule_callback

transfer_to_supervisor

due rows

create_room + metadata

CreateSIPParticipant

finalize Β· turns + summary + outbox

upsert summary vector

Browser Β· voice.pocketcode.in

Audio test tab

Outbound AI call

Pending callbacks

Recent call logs

/api/token

/api/dial

/api/voices /models /preview

/api/calls /callers /callbacks

outbox worker Β· 8s

callback dialer Β· 30s

LiveKit room
+ metadata

LiveKit SIP gateway

Twilio Trunk +15342310196

PSTN caller

Deepgram STT

Claude Haiku / Ollama

Deepgram Aura / ElevenLabs

function_tools

voice_pg Β· sim-db-1
8 tables incl. kb_chunks

Qdrant Β· voice_calls

Ollama nomic-embed-text

Airtable Β· Call Logs
+ Call Memories

Voice Playground β€” full request and data flow

Flow narrative

  1. Audio Test path. Browser POSTs /api/token → voice-playground mints a LiveKit token, pre-creates the room with metadata (llm, tts, agent_name, agent_gender, product_kb="audio_test_mode.md") → browser joins room and publishes mic → the livekit-agent worker auto-dispatches into the room → STT→LLM→TTS pipeline → audio streams back to the browser.
  2. Outbound path. Browser POSTs /api/dial with to_number, optional supervisor_number, prompt, model, voice β†’ voice-playground calls LiveKit's CreateSIPParticipant with wait_until_answered=True β†’ LiveKit SIP gateway dials Twilio β†’ Twilio rings the destination β†’ on pickup, the SIP participant joins the room and the agent joins too. The same call returns an observer LiveKit token the browser uses to listen in + render the WhatsApp-style live transcript.
  3. Per-turn pipeline (preemptive). Deepgram emits interim transcripts β†’ LLM starts generating BEFORE end-of-turn confirmation (saves ~300–600 ms first-token) β†’ tokens stream into the TTS plugin β†’ audio frames flow to LiveKit and out to the caller. The ML turn detector decides when the user really IS done, instead of relying on pure VAD silence.
  4. Tools at runtime. The LLM may call kb_search (embed via Ollama nomic-embed-text β†’ cosine search over voice_pg.kb_chunks), caller_memory (Qdrant voice_calls filtered by caller_number), web_search (Brave Search via mcp-web-search), lookup_caller (mcp-postgres), transfer_to_supervisor (CreateSIPParticipant for the supervisor number, with hold music + 3-attempt retry), schedule_callback (INSERT into voice_pg.callbacks with scheduled_for, validated against the 48-hour window), or hang_up (DeleteRoom).
  5. End-of-call writes. Agent finalises the calls row in voice_pg (outcome, duration, summary), pushes the summary vector into Qdrant for future-caller lookup, and enqueues an Airtable write in voice_pg.airtable_outbox. The outbox worker drains pending rows to Airtable every 8 s β€” Airtable outage no longer drops calls.
  6. Auto-dialed callbacks. The scheduled-callback worker polls voice_pg.callbacks every 30 s with a 60 s look-ahead. Due rows trigger another CreateSIPParticipant β†’ LiveKit β†’ Twilio β†’ PSTN. Failed dials push scheduled_for back 5 minutes. The UI's Pending callbacks card surfaces each row with scheduled time + buttons to βœ• Cancel or βœ“ Mark done.

Architecture

Browser
  β”‚  (master-login JWT via auth_gate)
  β–Ό
voice-playground  (FastAPI, this service)
  β”œβ”€β”€ GET /          static index.html + livekit-client SDK
  └── POST /api/token  mints a short-lived LiveKit access token
                       (api_secret stays server-side)
  β”‚
  β–Ό  (browser uses the token to join)
LiveKit Cloud (Germany 2 region)
  β”‚
  β–Ό  (Dispatch Rule SDR_xxts7WxxddQj β€” same one used for PSTN)
livekit-agent  β†’  Silero VAD β†’ Deepgram STT β†’ Claude β†’ ElevenLabs TTS

UI features

vs the PSTN path

AspectPSTN (call +1 534-231-0196)voice-playground
IngressTwilio DID β†’ SIP trunk β†’ LiveKit SIP gatewayBrowser β†’ LiveKit WebRTC (same project)
Cost~$0.013/min inbound TwilioFree (LiveKit free tier 50 GB egress/mo)
Latency+SIP processing overheadJust WebRTC RTT to LiveKit Germany 2
PipelineSame agent, same STT/LLM/TTSSame agent, same STT/LLM/TTS
Use caseProduction / real callersDev, testing, demos
Same agent dispatch as PSTN: the LiveKit Dispatch Rule (SDR_xxts7WxxddQj) auto-dispatches the registered livekit-agent worker into any new room β€” whether the room was created by an inbound SIP call or by the playground's token-minting flow. One agent, two ingress paths.

livekit-agent β€” Adrian, the voice agent

NOT a browser-visible service. Python worker that registers with LiveKit Cloud and waits for dispatched calls. Source: ~/pocketcode-project/services/livekit-agent/.

Persona

The agent is Adrian, a representative from Pocket Code dot IN. Greets every call with "Hi, this is Adrian, a representative from Pocket Code dot IN, and this is a test call. How are you doing today?" Never reveals it's an AI. Configurable via AGENT_NAME / AGENT_ROLE / AGENT_GREETING env vars. Listens for the caller calling it by name ("Adrian") as an attention signal.

Persona name + gender pronouns follow the selected voice. Pick Orpheus (male) β†’ agent introduces as Orpheus, refers to itself with he/him; Stella (female) β†’ Stella, she/her; neutral β†’ they/them. Wired by the voice-playground server from each voice's catalog entry into the room metadata, then injected as a "Self-reference" block into the system prompt at call time. The chosen agent_gender is also persisted on voice_pg.calls.agent_gender for analytics.

Supervisor number is optional. With one set, transfer_to_supervisor() dials with hold music + 3-attempt retry. Without one, it apologizes, promises a 24-hour callback (logged in voice_pg.callbacks with scheduled_for = now() + 24h), and hangs up cleanly.

Pipeline

Twilio DID (+1 534-231-0196)
   β”‚  SIP trunk
   β–Ό
LiveKit Cloud SIP gateway
   β”‚  WebRTC room (one per call)
   β–Ό
livekit-agent container
   β”œβ”€β”€ VAD     : Silero (in-process ONNX, tuned for phone audio)
   β”œβ”€β”€ STT     : Deepgram Nova-3 (streaming, en, endpointing=200ms)
   β”œβ”€β”€ LLM     : Anthropic Claude Haiku 4.5 (streaming; Sonnet via env override)
   β”œβ”€β”€ TTS     : Deepgram Aura aura-2-orpheus-en (young male, English)
   β”‚             β€” OR ElevenLabs with a cloned voice id (USE_ELEVENLABS_TTS=1)
   └── Tools   : mcp-postgres.query (caller lookup)
                 mcp-web-search.web_search (mid-call queries)
                 hang_up (LLM calls when caller says bye β†’ deletes room)
                 mcp-airtable.create_record (end-of-call β†’ call_logs table)

Latency knobs (all streamable end-to-end):
   min_endpointing_delay=0.4s, max_endpointing_delay=2.0s, allow_interruptions=True
   barge-in: caller talking over Adrian instantly cuts his TTS

Tools available to Claude during the call (14 function tools)

As of Phase 5.7q the agent exposes 14 function tools. The LLM calls them as needed mid-conversation; results stream back into the next turn.

ToolWhat it does
lookup_caller(phone)Looks up the caller's profile + history in voice_pg (via mcp-postgres) for returning callers.
web_search(query)Top Brave Search results (via mcp-web-search), returned as compact text.
kb_search(query)pgvector cosine search over voice_pg.kb_chunks β€” top-4 product-KB chunks only.
caller_memory(query)Qdrant voice_calls lookup filtered by caller_number β€” cross-call memory.
transfer_to_supervisor(reason)Dials the supervisor number (hold music + 3-attempt retry); falls back to a 24-hour callback if none is set.
schedule_callback(when, reason)INSERTs into voice_pg.callbacks (validated against the look-ahead window) for the auto-dialer to pick up.
note_signal(kind, value)Records a structured signal (interest, objection, budget, …) into voice_pg.signals.
set_qualification(status)Marks the lead's qualification state on the call row.
send_followup_email(to, subject, body)Sends a branded follow-up via mcp-gmail's send_email.
find_demo_slots(...)Asks mcp-gcal for 2-3 open business-hours slots.
book_demo(slot, attendee)Creates a Google Calendar event with an auto-Meet link via mcp-gcal.
disqualify_lead(reason)Records that the caller isn't a fit and closes the qualification loop.
get_current_time()Returns the current time in the caller's timezone β€” anchors all scheduling.
hang_up(reason)The LLM calls this when the caller signals they're done. Adrian says a brief farewell, then the LiveKit room is deleted 1.5 s later (audio flush time).
Current agent internals (Phase 5.7m–q). A few things worth knowing about the worker as it stands today:
  • Canonical 10-section brain (5.7q). The agent's system prompt was rewritten from an ad-hoc blob into a fixed 10-section structure (identity, persona, knowledge, qualification, tools, scheduling, objection handling, compliance, closing, self-reference). Easier to reason about and to edit one behaviour without disturbing the rest.
  • LLM failover chain (5.7p). A Claude β†’ OpenRouter β†’ Ollama fallback prevents stalls when a provider is overloaded β€” if the primary returns an overload/5xx the worker transparently retries on the next provider so the caller never hits dead air.
  • Branded HTML emails (5.7m). send_followup_email now sends a branded HTML template (not plain text) with Home / Setup / Docs links in the footer.

End-of-call logging (Airtable call_logs)

On any disconnect, the agent writes one row. The write is wrapped in try/except so call teardown cannot crash on an Airtable failure.

Cost protection

CALL_MAX_DURATION_SEC (default 600 s) starts a watchdog at call connect. If the call runs past the limit (stuck conversation, looping LLM, dead audio), the watchdog forcibly deletes the room. Set to 0 to disable.

What's needed before a real call works

The agent registers as a LiveKit worker on startup. Both PSTN directions are configured via API (no dashboard clicking) β€” see the project CHANGELOG for the exact recipes. High-level:

  1. Twilio β€” buy a DID, create an Elastic SIP Trunk, add an Origination URL pointing at the LiveKit SIP gateway, associate the DID. For outbound, set a Termination Domain + attach a Credential List for SIP digest auth.
  2. LiveKit Cloud β€” create an Inbound Trunk + a Dispatch Rule (auto-creates rooms with prefix call-). For outbound, create an Outbound Trunk pointing at the Twilio termination domain with those creds.
  3. Airtable β€” create a base + call_logs table with the four fields above. Mint a PAT with data.records:read, data.records:write, schema.bases:read. Store the base ID under secrets.json β†’ airtable.base_call_logs.

Testing without a real phone

Open voice.pocketcode.in. Click the πŸ“ž button to talk to Adrian directly from the browser (LiveKit mints a publisher token, browser publishes mic, agent auto-joins). Or use the dial-out card to call any phone β€” the browser joins the same room as an observer so you can listen + watch live transcripts.

Logs from the running worker

bash Β· on VPS
docker logs livekit-agent --tail 30 -f
Sibling variant β€” Voice Playground 2 (VP2). voice2.pocketcode.in hosts a sim-backbone rebuild of the same voice agent: instead of the inline-Python tool implementations in VP1's ~1820-LOC agent.py monolith, its function tools delegate to sim workflows, fronted by a thin ~400-LOC worker. Same personas / LLM-TTS pipeline, different tool plumbing. Use VP1 (this page) for the canonical agent; VP2 when you want tools editable as visual sim flows.

mcp-voice β€” outbound PSTN calling

URL: https://mcp-voice.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-voice/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}

Thin MCP wrapper over LiveKit's CreateSIPParticipant Twirp API. Lets any agent (or the dial-out card on voice-playground) place outbound calls to a real phone number. The voice agent worker auto-joins the room and talks to the callee.

Tools

ToolArgumentsNotes
dial_phoneto_number (E.164), prompt?, agent_name?, room_name?Creates the room, dials. Prompt is forwarded as a participant attribute so the agent can read it on join.
list_active_callsβ€”Lists LiveKit rooms hosting a phone-* participant.
end_callroom_nameDeletes the LiveKit room (hangs up both legs).

How outbound is wired (one-time API setup)

Example dial from curl

bash Β· MCP tools/call
TOKEN=$(jq -r .mcp.bearer_token ~/pocketcode-project/secrets/secrets.json)
# (initialize + notifications/initialized first β€” see services/mcp-voice/README.md)
curl -sk -X POST https://mcp-voice.pocketcode.in/mcp \
  -H "Authorization: Bearer $TOKEN" -H "Mcp-Session-Id: $SID" \
  -H "Accept: application/json, text/event-stream" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"dial_phone","arguments":{"to_number":"+15551234567","prompt":"Confirm Tuesday 3pm dentist."}}}'

mcp-voice-history β€” read-only call history MCP (8th)

URL: https://mcp-voice-history.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-voice-history/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}

Read-only HTTP MCP over the voice agent's operational store on the existing sim-db-1 Postgres (database voice_pg). Every call writes through here, so this MCP is the canonical entry point for any "show me what happened" question from any AI client.

Tools

ToolArgumentsReturns
list_callerslimit=50Recent unique callers, most-recently-seen first.
get_callercaller_numberOne caller's profile + their 10 most-recent calls.
list_callslimit=20, caller_number?, outcome?Recent calls. Optional filters.
get_callcall_idFull detail: row + all turns + all tool_invocations.
search_calls_by_summaryquery, limit=10ILIKE search over summaries (pgvector per-call search wiring is a follow-up).
list_callbacksstatus='pending'Callbacks driven by failed supervisor transfers.
resolve_callbackcallback_id, status, notes?Mark a callback done/cancelled after dial-back.

Operational data plane (voice_pg + Qdrant)

The agent and the voice-playground share a Postgres database voice_pg with 8 tables β€” callers, calls, turns, tool_invocations, events, callbacks, airtable_outbox, kb_chunks (pgvector). Plus a Qdrant collection voice_calls for cross-call memory. None of this is a new container β€” it's all on the existing sim-db-1 and Qdrant instances.


mcp-gmail β€” Gmail send / draft / read / search / labels (Phase 4, 9th MCP)

URL: https://mcp-gmail.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-gmail/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}

HTTP+Bearer MCP wrapping Google's Gmail API. Five tools so your AI helpers β€” and the voice agent's send_followup_email tool β€” can write emails on your behalf. Single Google Cloud Console OAuth Desktop client covers both Gmail and Calendar APIs; the refresh token lives at secrets.json.services.google.oauth_refresh_token (single source of truth after the Phase 6b consolidation).

Tools

ToolArgumentsReturns
send_emailto, subject, body, cc?, bcc?, body_html?Outbound email via your Gmail account.
draft_emailSame as send_emailSaves to Drafts only.
read_emailmessage_idOne message's content + metadata.
search_emailsquery, max_results?Gmail-search-syntax lookup (e.g. from:nishu after:2026/05/01).
list_labelsβ€”System + user labels. Good as a smoke test.

mcp-gcal β€” Calendar availability + Meet booking (Phase 4, 10th MCP)

URL: https://mcp-gcal.pocketcode.in/mcp
Source: ~/pocketcode-project/services/mcp-gcal/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}

HTTP+Bearer MCP wrapping Google's Calendar API. Seven tools for availability checks, slot finding, and event creation with auto-generated Google Meet links. Shares the OAuth refresh token with mcp-gmail β€” one Desktop client covers both APIs. The voice agent's find_demo_slots + book_demo tools call this MCP.

Tools

ToolArgumentsReturns
list_calendarsβ€”All calendars you can read/write. Primary + shared.
check_availabilitystart_iso, end_isoBusy/free spans in the window.
find_free_slotsduration_minutes, days_ahead, prefer_afternoon2-3 ISO datetimes of open slots (business-hours filter).
create_eventsummary, start_iso, duration_minutes, attendees[], add_meet_link, description?New event with auto-Meet link + invites sent.
list_eventsstart_iso, end_isoEvents in the window.
get_eventevent_idFull event detail.
delete_eventevent_idCancels the event + notifies attendees.

mcp-hostinger β€” manage the VPS / DNS / domains / billing (11th MCP)

URL: https://mcp-hostinger.pocketcode.in/  (note: the MCP endpoint is the root path /, NOT /mcp like every other server)
Source: ~/pocketcode-project/services/mcp-hostinger/
Auth: Authorization: Bearer ${MCP_BEARER_TOKEN}

Wraps Hostinger's official API MCP (hostinger-api-mcp) β€” 118 tools spanning the whole Hostinger control panel: VPS lifecycle, DNS zones, domain registration/transfer, billing, shared hosting, and email. Lets your AI helpers inspect and operate the very VPS this stack runs on. Bearer-gated through Caddy like the other MCPs.

Tool surface (118 tools, by area)

AreaWhat it covers
VPSList/inspect virtual machines, metrics, snapshots/backups, start/stop/restart, OS templates β€” and rebuild / delete.
DNSRead and edit DNS zones + records (A / AAAA / CNAME / MX / TXT …) for your domains.
DomainsAvailability checks, registration, transfers, nameserver + WHOIS management.
BillingCatalog, orders, subscriptions, payment methods.
Hosting / EmailShared-hosting accounts and mailbox management.
Use with care β€” this MCP includes destructive operations. The toolset can rebuild or delete the VPS (i.e. the host this entire stack runs on), edit live DNS, and place billing orders. There is no read-only mode. Treat any LLM with this MCP attached as having root over your Hostinger account, and never expose the bearer token to untrusted callers. For routine inspection prefer the read-only "list/get" tools and keep the write/delete tools for deliberate, supervised actions.
Endpoint gotcha. The Mac Claude Desktop / .mcp.json entry for this server must point at the bare host https://mcp-hostinger.pocketcode.in/ β€” not .../mcp. Copying the postgres example and swapping the subdomain will 404; remember to drop the /mcp suffix.

code-server β€” browser VS Code

URL: https://code.pocketcode.in
Source (unit): ~/pocketcode-project/operations/host/systemd/code-server.service
Runs as: host systemd service β€” NOT a container. Same posture as ttyd.

Browser-accessible VS Code IDE β€” the same thing your Mac VS Code is, but running on the VPS. Lets you edit any file in ~/pocketcode-project/, control Docker, and run claude from the integrated terminal β€” all without a separate Mac install. Auth is via Caddy auth_gate (the master login cookie); code-server itself runs --auth none so there's no double-login.

What's pre-wired

Claude IDE extension

Install once via the IDE's Extensions panel (Ctrl+Shift+X) β€” search "Claude Code", install anthropic.claude-code. It reads your ANTHROPIC_API_KEY from the env or you can use the Pro/Max account OAuth flow. Open VSX rate-limits aggressively, so we don't auto-install at boot; manual install through the UI handles retries naturally.

Network posture

Restart it

bash Β· on VPS
systemctl restart code-server
systemctl status code-server --no-pager | head -5