Cookbook: Connect cantus to Google Chat (over Cloud Pub/Sub, no public HTTPS)
This walkthrough takes a Google Chat app from zero to a working echo bot on cantus serve: a member says something in a space, and the bot replies. Google Chat asks for more GCP setup than LINE, Telegram, or Discord, but it drops the requirement those three share: because cantus pulls from Cloud Pub/Sub, your laptop never needs a public HTTPS endpoint, so there is no Cloudflare Tunnel to run. A service account with Pub/Sub Subscriber access is enough.
Design tradeoff: Google Chat also offers an HTTPS webhook mode with RS256 JWT signing, but cantus does not support that path. RS256 plus JWKS public-key rotation would pull
pyjwtandcryptographyintocantus[serve], and it would still need a public endpoint. Pub/Sub pull authenticates with an IAM service account, which fits the student scenario (a laptop behind NAT) cleanly.
0. What you'll need
- A Google Cloud account (Google Workspace or a regular Gmail account both work).
- A Google Chat space you can administer, to use as the test target.
cantus-agent[serve]>=0.4.7installed.- A laptop on any OS.
google-cloud-pubsuband its transitive dependencygrpcioship prebuilt wheels for Linux x86_64, macOS arm64 and x86_64, and Windows AMD64. - No
cloudflared,ngrok, or any tunnel.
1. Create a GCP project and enable APIs in the Google Cloud Console
- Open the Google Cloud Console, then Select a project -> New Project. Give it a name, for example
cantus-chatbot. - In that project, go to APIs & Services -> Library and enable these APIs:
- Google Chat API
- Cloud Pub/Sub API
- Google Workspace Events API
- Note the Project ID (the string-form ID, not the Project Number), for example
cantus-chatbot-2026.
2. Create a service account and download its JSON key
- Go to IAM & Admin -> Service Accounts -> Create Service Account. Give it a name, for example
cantus-chatbot-sa. - Grant it the role:
- Pub/Sub Subscriber (to read the Pub/Sub topic)
- On the service account detail page, open the Keys tab -> Add Key -> Create new key -> JSON -> download.
- Put the downloaded JSON file somewhere safe on your machine (do not commit it to git), for example
~/.cantus/sa.json, and runchmod 600 ~/.cantus/sa.json. - In the Google Chat admin / API console, add this service account's email as the Chat app's service account.
3. Create a Pub/Sub topic and subscription
gcloud config set project YOUR_PROJECT_ID
gcloud pubsub topics create cantus-chat-events
gcloud pubsub subscriptions create cantus-chat-sub --topic=cantus-chat-eventsTwo strings you'll use later:
- Topic:
projects/YOUR_PROJECT_ID/topics/cantus-chat-events - Subscription:
projects/YOUR_PROJECT_ID/subscriptions/cantus-chat-sub
Let the Chat-events publisher (an internal Google service account) publish to your topic:
gcloud pubsub topics add-iam-policy-binding cantus-chat-events \
--member='serviceAccount:chat-api-push@system.gserviceaccount.com' \
--role='roles/pubsub.publisher'4. Subscribe to Chat events through the Google Workspace Events API
curl -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
https://workspaceevents.googleapis.com/v1/subscriptions \
-d '{
"target_resource": "//chat.googleapis.com/spaces/YOUR_SPACE_ID",
"event_types": ["google.workspace.chat.message.v1.created"],
"notification_endpoint": {
"pubsub_topic": "projects/YOUR_PROJECT_ID/topics/cantus-chat-events"
},
"payload_options": {"include_resource": true}
}'YOUR_SPACE_ID comes from the URL of the Chat space you want the bot to listen to (for example the AAAA... part in https://chat.google.com/room/AAAA...).
5. Write myskills/app.py
# myskills/app.py
from cantus.core.registry import Registry
from cantus.protocols.skill import register_skill
from cantus.serve import GoogleChatPubSubChannel
registry = Registry()
@register_skill
def echo(text: str) -> str:
"""Echo back whatever the user said."""
return text
registry.register("skill", echo)
# All three values come from CANTUS_SERVE_CHANNEL_GOOGLE_CHAT_* env vars.
# (The constructor accepts them directly too, but keep the SA path out of
# source code so different deployments don't get crossed.)
google_chat_channel = GoogleChatPubSubChannel()6. Put the SA path and subscription details in your shell
export CANTUS_SERVE_CHANNEL_GOOGLE_CHAT_CREDENTIALS_PATH=$HOME/.cantus/sa.json
export CANTUS_SERVE_CHANNEL_GOOGLE_CHAT_SUBSCRIPTION=projects/YOUR_PROJECT_ID/subscriptions/cantus-chat-sub
export CANTUS_SERVE_CHANNEL_GOOGLE_CHAT_SPACE=spaces/YOUR_SPACE_IDNot secrets:
CREDENTIALS_PATHis just a file path, andSUBSCRIPTIONandSPACEare public Google identifiers. The real secret is the contents of the SA JSON file. Keep that file out of git and protect read access withchmod 600.
7. Run cantus serve
cantus serve \
--registry-import myskills.app:registry \
--channels myskills.app:google_chat_channelExpected log (cantus startup lines omitted above it):
INFO cantus.serve.channels GoogleChatPubSubChannel connected: projects/.../subscriptions/cantus-chat-sub8. Manual smoke test: say something in the Chat space
Go to your Google Chat space and say hello. cantus should:
- Pull a
google.workspace.chat.message.v1.createdevent from Pub/Sub. - Put the event on an internal queue (your skill can
receiveit for processing). - Assuming the skill calls
channel.send({"data": {"text": "hello back"}}), the bot replieshello backin the same space.
If no events arrive, check in order:
gcloud pubsub subscriptions pull cantus-chat-sub --auto-ack --limit=10to see whether the topic actually has messages. If not, the Workspace Events API subscription may have failed.- Whether the SA has
roles/pubsub.subscriberon that subscription. - Whether the
cantus servelog shows backoff retries. After 10 consecutive failures it recordsself.last_errorand stops reconnecting, but the rest of the server keeps running — the channel gives up without crashing the lifespan.
9. Shutdown
Ctrl+C stops cantus serve. The lifespan then:
- Calls
channel.disconnect()to cancel the Pub/Sub pull. - Closes the app-scoped
httpx.AsyncClient. The order matters: outboundsendmust still work beforedisconnectruns.
Clean up the Pub/Sub resources (optional):
gcloud pubsub subscriptions delete cantus-chat-sub
gcloud pubsub topics delete cantus-chat-eventsSecurity notes
- Never commit the SA JSON file. Add patterns like
*sa.jsonand*service-account*.jsonto.gitignore. - For production, replace the static SA key with Workload Identity Federation to avoid the risk of leaking long-lived keys.
- When
CANTUS_SERVE_CHANNEL_GOOGLE_CHAT_CREDENTIALS_PATHis unset, cantus falls back toGOOGLE_APPLICATION_CREDENTIALS(Google's standard ADC variable). So if neither is set, the channel fails fast at construction time with a fixed error string that does not echo any input value. cantus serve --auth-mode beareror--auth-mode api-keyadds auth to the Skill HTTP endpoints. Channel inbound traffic goes over Pub/Sub pull, so there is no HTTP route to protect there.