Production & deployment
Minimal FastAPI deployment
import os
from fastapi import FastAPI
from chattice import Dispatcher
from chattice.integrations.fastapi import create_chat_router
from chattice.transports.http import GoogleTokenVerifier
dispatcher = Dispatcher()
# ...routers...
app = FastAPI()
app.include_router(
create_chat_router(dispatcher, GoogleTokenVerifier(audience="https://your.domain/"))
)
Run with any ASGI server:
uv run uvicorn app:app --host 0.0.0.0 --port 8000
VPS recipe (Caddy + systemd)
A complete deployment on a single VPS with a domain (chat.example.com),
TLS termination, and process supervision.
- Install:
sudo apt install -y python3.12-venv caddy
sudo adduser --system --home /opt/echo-bot --shell /usr/sbin/nologin echo-bot
sudo -u echo-bot -H python3.12 -m venv /opt/echo-bot/.venv
sudo -u echo-bot -H /opt/echo-bot/.venv/bin/pip install "chattice[fastapi]" uvicorn
- App (
/opt/echo-bot/app.py) — same shape as the Quickstart, plus a health check:
import os
from fastapi import FastAPI
from chattice import Dispatcher, Router
from chattice.events import MessageEvent
from chattice.integrations.fastapi import create_chat_router
from chattice.transports.http import GoogleTokenVerifier
router = Router()
@router.message()
async def echo(message: MessageEvent) -> str:
return f"You said: {message.text}"
dispatcher = Dispatcher()
dispatcher.include_router(router)
app = FastAPI()
app.include_router(
create_chat_router(
dispatcher,
GoogleTokenVerifier(audience=os.environ["CHATTICE_AUDIENCE"]),
)
)
@app.get("/healthz")
async def healthz():
return {"status": "ok"}
- systemd unit (
/etc/systemd/system/echo-bot.service):
[Unit]
Description=Chattice echo bot
After=network-online.target
[Service]
User=echo-bot
WorkingDirectory=/opt/echo-bot
Environment=CHATTICE_AUDIENCE=https://chat.example.com/
ExecStart=/opt/echo-bot/.venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now echo-bot
- TLS termination — Caddy (
/etc/caddy/Caddyfile):
chat.example.com {
reverse_proxy 127.0.0.1:8000
}
Caddy obtains and renews the Let's Encrypt certificate automatically; nginx + certbot works equally well.
- Verify:
curl https://chat.example.com/healthz→ 200, then set the app to Live mode in the Console and send a message from a test Space. Followjournalctl -u echo-bot -f; a 401 means an audience mismatch.
Keep CHATTICE_AUDIENCE EXACTLY equal to the endpoint URL configured in
the Chat API Console (trailing slash included).
VPS recipe — Pub/Sub pull (no domain, no TLS, no public IP)
With streaming pull Google does NOT call your server: events flow
Chat → Pub/Sub → your persistent subscriber. The VPS only needs outbound
access (works behind a VPN; no domain, no Caddy, no CHATTICE_AUDIENCE).
# app.py
import asyncio
import os
from chattice import Dispatcher, Router
from chattice.auth import ServiceAccountCredentialsProvider
from chattice.client import Bot
from chattice.events import MessageEvent
router = Router()
@router.message()
async def echo(message: MessageEvent) -> None:
await message.reply(f"You said: {message.text}") # no sync return on pull
async def main() -> None:
service_account_file = os.environ["CHATTICE_SERVICE_ACCOUNT_FILE"]
app_credentials = ServiceAccountCredentialsProvider.from_service_account_file(
service_account_file
)
pull_credentials = ServiceAccountCredentialsProvider.from_service_account_file(
service_account_file, scopes=["https://www.googleapis.com/auth/pubsub"]
)
async with Bot(app_credentials_provider=app_credentials) as bot:
dispatcher = Dispatcher(bot=bot)
dispatcher.include_router(router)
await dispatcher.run_pubsub(
os.environ["CHATTICE_SUBSCRIPTION"],
bot=bot,
credentials_provider=pull_credentials,
)
if __name__ == "__main__":
asyncio.run(main())
# /etc/systemd/system/echo-bot.service
[Unit]
Description=Chattice echo bot (Pub/Sub pull)
After=network-online.target
[Service]
User=echo-bot
WorkingDirectory=/opt/echo-bot
Environment=CHATTICE_SUBSCRIPTION=projects/<project>/subscriptions/<sub>
Environment=CHATTICE_SERVICE_ACCOUNT_FILE=/opt/echo-bot/sa.json
ExecStart=/opt/echo-bot/.venv/bin/python app.py
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
Trade-offs: no synchronous interaction responses, no Dialogs, no
App Home — handlers act through authenticated Bot calls.
Push deployments (Pub/Sub)
from chattice.integrations.fastapi import create_pubsub_router
from chattice.idempotency import RedisIdempotencyStorage
from chattice.transports.pubsub import GooglePubSubVerifier
app.include_router(
create_pubsub_router(
dispatcher,
verifier=GooglePubSubVerifier(
audience="https://your.domain/pubsub",
service_account_email=os.environ["PUBSUB_SERVICE_ACCOUNT_EMAIL"],
),
idempotency_storage=RedisIdempotencyStorage(), # from chattice.idempotency
)
)
Push constraints: ack-only (204/429/500), no dialogs, no sync card
updates — update cards via bot.app.messages.update(name, card=...)
afterwards.
Resource lifecycle
async with Bot(credentials_provider=...) as bot, dispatcher.lifespan(resource):
...
Bot.close() awaits the real transport closer; the lifespan closes
every resource even when one closer fails.
Configuration checklist
- Chat app in Live mode; app added to the target Spaces.
- App auth (service account,
chat.bot) for outbound sends; accessory widgets and private viewer are app-auth-only. - HTTP endpoint audience matches the verifier configuration.
- Push: OIDC service-account identity configured and verified.
- Durable state on Redis, never
MemoryStoragein production. - Handlers answer interactions within the 30 s deadline; long work via
Botcalls.
Multi-instance notes
- Pub/Sub dedupe keys are namespaced by subscription; the idempotency state machine (claim/complete/release, 429 on active) is safe across instances sharing one Redis.
- FSM records use compare-and-set — concurrent instances surface conflicts instead of losing updates.