C
Chatty

Self-Hosting & Production Deployment

Chatty's supported self-hosting path moves only the application containers to your infrastructure. Supabase remains the managed system of record for Auth, Postgres, Storage, Realtime, pgvector, and row-level security. This preserves the current data and authentication boundary while allowing the same release to run on Docker/VPS, Railway, Render, or a Heroku-style container host.

The deployment contract is two services: a Next.js frontend and a FastAPI API. The default root Compose file does not create a second database, Redis, or object store.

1. Prerequisites and security boundary

  • A Supabase project and database password.
  • A GitHub checkout of the Chatty repository.
  • A Gemini key or another configured LLM/BYOK provider.
  • One public HTTPS URL for the frontend and one for the API.
  • Docker Engine and the Compose plugin for local testing or a VPS.

Use the Supabase publishable key only in the browser and the secret key only on the API. Supabase explains this split in its API key guide. Never put a secret key, database password, OAuth secret, webhook token, FUNCTION_SECRET, or BYOK encryption key in the frontend or a committed file.

The provider-neutral self_host profile is a separate advanced deployment. Do not enable it when you want to keep the current Supabase project.

2. Create Supabase and apply migrations

  1. Create a project at supabase.com, choose a region close to the API, and store the database password in a password manager.
  2. In Project Settings → API, copy the project URL, publishable key, and server-only secret key.
  3. In Project Settings → Database → Connection string, copy the direct or session-pooler details. Do not use the transaction pooler for DDL. See database connections.
  4. In Authentication → URL Configuration, set the final frontend Site URL and add the OAuth callback URLs.
  5. Apply the schema once, before production traffic:
git clone https://github.com/PersonaliAI/chatty.git
cd chatty/backend
python -m pip install psycopg2-binary
python scripts/apply_migrations.py "postgresql://postgres:DB_PASSWORD@DB_HOST:5432/postgres"

3. Generate secrets and configure variables

python -c "import secrets; print(secrets.token_urlsafe(32))"  # FUNCTION_SECRET
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"  # BYOK_ENCRYPTION_KEY

API variables:

VariableValue
DEPLOYMENT_PROFILEmanaged_supabase
SUPABASE_URLproject URL
SUPABASE_SECRET_KEYserver-only secret key
SUPABASE_DB_HOSTdirect/session database host
SUPABASE_DB_PASSWORDdatabase password
FUNCTION_SECRETgenerated random secret
BYOK_ENCRYPTION_KEYgenerated Fernet key
GEMINI_API_KEYGoogle AI Studio key
ALLOWED_ORIGINSexact frontend HTTPS origin
FRONTEND_URLfrontend HTTPS origin
CHATTY_FRONTEND_URLfrontend HTTPS origin
CHATTY_BACKEND_URLAPI HTTPS origin
PORTplatform-provided value

Frontend variables:

VariableValue
NEXT_PUBLIC_SUPABASE_URLproject URL
NEXT_PUBLIC_SUPABASE_ANON_KEYpublishable key
NEXT_PUBLIC_BACKEND_URLAPI HTTPS origin
NEXT_PUBLIC_DEPLOYMENT_PROFILEmanaged_supabase
NEXT_PUBLIC_LEMON_PORTAL_URLoptional public billing URL

Copy optional channel variables from backend/.env.example.

4. Local smoke test

docker compose up --build -d backend frontend
docker compose ps
curl http://localhost:8000/readyz

Wait for a ready response, then test sign-up, sign-in, bot creation, knowledge upload, widget chat, and each integration you enabled. Stop with docker compose down. The managed Compose file does not create a second Postgres, Redis, or object store.

5. Docker Compose on a VPS

  1. Provision Linux, Docker, a firewall, automatic security updates, and a non-root deployment user.
  2. Clone the repository. Create backend/.env, frontend/.env, and root .env from the examples and restrict them:
chmod 600 backend/.env frontend/.env .env
  1. Start the application services:
docker compose up --build -d backend frontend
docker compose ps
curl http://127.0.0.1:8000/readyz
  1. Put Caddy, nginx, or Traefik in front. Route the frontend domain to port 3000 and API domain to port 8000; terminate TLS at the proxy.
  2. Set ALLOWED_ORIGINS, FRONTEND_URL, CHATTY_FRONTEND_URL, CHATTY_BACKEND_URL, and NEXT_PUBLIC_BACKEND_URL to final origins.
  3. Monitor /readyz and logs. Supabase remains responsible for database and storage durability; do not add local database volumes to this profile.

6. Railway

Railway maps each Compose service to a separate service. It does not run the root Compose file as one application. Follow the official Compose deployment guide.

  1. Create an Empty project and add a service from the GitHub repository.
  2. Select main, set Root Directory to backend, and Dockerfile path to Dockerfile. Add API variables in Variables → Raw Editor.
  3. Generate api.example.com and set CHATTY_BACKEND_URL. Configure /readyz as the healthcheck; Railway injects PORT and waits for a 2xx response. See Dockerfiles and healthchecks.
  4. Add a second service from the same repository. Set Root Directory to frontend and Dockerfile path to Dockerfile.
  5. Add NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_BACKEND_URL, and public billing values. These are build-time values; changing one rebuilds the frontend. See frontend variables.
  6. Generate app.example.com and update API CHATTY_FRONTEND_URL, FRONTEND_URL, and ALLOWED_ORIGINS. Verify logs, login, bot creation, and widget chat. Configure DNS in Railway Networking.

7. Render

The root render.yaml Blueprint creates chatty-api and chatty-frontend. Render supports Docker services, HTTP health checks, and sync: false secret prompts (Blueprint reference).

  1. Choose New → Blueprint, connect the repository, and select main.
  2. Confirm the two services. Enter all sync: false API secrets and final URLs. Never commit secret values to render.yaml; see Infrastructure as Code.
  3. Enter frontend NEXT_PUBLIC_* build values and apply the Blueprint.
  4. Wait for chatty-api /readyz to pass. Render routes traffic only after a healthy 2xx/3xx probe (health checks).
  5. Attach api.example.com and app.example.com. Update API CORS and Supabase Auth redirects.
  6. Without Blueprints, create two Web Services manually with runtime Docker, root directories backend and frontend, and the same variables. Never put sensitive values in Docker build args or image layers (Render Docker).

8. Heroku-style container hosts

Heroku uses one web process per app. Use two apps and keep Supabase external. The official flow is documented in Container Registry & Runtime.

heroku login
heroku container:login
 
heroku create chatty-api --stack container
heroku config:set DEPLOYMENT_PROFILE=managed_supabase SUPABASE_URL="..." SUPABASE_SECRET_KEY="..." SUPABASE_DB_HOST="..." SUPABASE_DB_PASSWORD="..." FUNCTION_SECRET="..." BYOK_ENCRYPTION_KEY="..." GEMINI_API_KEY="..." ALLOWED_ORIGINS="https://app.example.com" FRONTEND_URL="https://app.example.com" CHATTY_FRONTEND_URL="https://app.example.com" CHATTY_BACKEND_URL="https://api.example.com" --app chatty-api
cd backend
heroku container:push web --app chatty-api
heroku container:release web --app chatty-api
cd ..
heroku domains:add api.example.com --app chatty-api
 
heroku create chatty-frontend --stack container
heroku config:set NODE_ENV=production NEXT_PUBLIC_SUPABASE_URL="..." NEXT_PUBLIC_SUPABASE_ANON_KEY="sb_publishable_..." NEXT_PUBLIC_BACKEND_URL="https://api.example.com" NEXT_PUBLIC_DEPLOYMENT_PROFILE=managed_supabase --app chatty-frontend
cd frontend
heroku container:push web --app chatty-frontend
heroku container:release web --app chatty-frontend
cd ..
heroku domains:add app.example.com --app chatty-frontend

Use config vars, not Dockerfile ENV, for credentials. Heroku's filesystem is ephemeral and Compose networking is unavailable. Read the official config vars and custom domains guides.

9. Verify, update, and roll back safely

  1. GET https://api.example.com/readyz returns HTTP 200 and status ready.
  2. The frontend loads over HTTPS without mixed-content errors.
  3. Supabase sign-up, sign-in, sign-out, password reset, bot creation, knowledge upload, and widget chat work.
  4. OAuth callbacks, WhatsApp webhook, and Slack request URLs use the API domain.
  5. Browser bundles contain only the publishable key; logs contain no secrets.
  6. Enable platform alerts, log retention, Supabase backups, and a rollback plan.

Deploy an immutable Git commit. Apply migrations in a controlled release, then deploy API and frontend. If a container fails health checks, roll back to the last healthy platform release; do not reset or alter the live Supabase database. Rotate leaked Supabase keys, FUNCTION_SECRET, BYOK_ENCRYPTION_KEY, OAuth credentials, and channel tokens through the platform secret manager.

Troubleshooting

SymptomCheck
API exits during startupSUPABASE_URL or SUPABASE_SECRET_KEY is missing or a placeholder.
/readyz failsThe process is not listening on injected PORT, or Supabase is unreachable.
Browser CORS errorALLOWED_ORIGINS must contain the exact frontend origin.
Login redirects to the old siteUpdate Supabase Auth redirect URLs and CHATTY_FRONTEND_URL.
Widget calls the old APIRebuild after changing NEXT_PUBLIC_BACKEND_URL.
Migration cannot connectUse direct/session connection, not the transaction pooler.
Render never becomes liveConfirm Dockerfile CMD and /readyz response.
Railway browser failureBoth services need public domains and matching CORS/build values.
Heroku app exitsUse platform PORT; do not expect Compose networking or local volumes.