Installation

Install nuxt-pigeon, create a Telegram bot, put two values in .env and send your first notification from a Nuxt server route. Five minutes, start to finish.

Five minutes from nothing to a message on your phone. The hard part is Telegram's, not ours, and it is two chats with a bot.

Copy this

Install the module

terminal
npx nuxt module add nuxt-pigeon

That adds it to nuxt.config for you. If you would rather do it by hand:

terminal
pnpm add nuxt-pigeon
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-pigeon'],
  nuxtPigeon: {
    channels: {
      telegram: true,
    },
  },
})

Get a bot token

Open @BotFather in Telegram and send /newbot. It asks for a name and a username, then hands you a token that looks like 8123456789:AAF....

Get your chat id

Write any message to your new bot. It will not answer, that is fine. Then open this in a browser, with your token in it:

https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates

Look for "chat":{"id":123456789. That number is your chat id.

Nothing appears? You have not written to the bot yet, or you wrote before it existed. Send another message and reload.

Put both in .env

.env
PIGEON_TELEGRAM_BOT_TOKEN=8123456789:AAF...
PIGEON_TELEGRAM_CHAT_ID=123456789

Send something

server/api/hello.get.ts
export default defineEventHandler(async () => {
  await telegram.send('It works.')

  return { sent: true }
})

Run it

terminal
pnpm dev

Open http://localhost:3000/api/hello. Your phone buzzes.

What just happened

  • telegram was not imported. Channels are auto imported in server/, like defineEventHandler is.
  • The token was never passed. It is read from the environment at request time, so the same build runs in staging and production with different credentials.
  • Nothing was constructed. No client, no instance, no setup call.
  • The message went out over one fetch, with a timeout, two retries on a 5xx, and growing delays between them. That is the default, and you can turn all of it off.

Where the credentials come from

Three places, in this order. The first one that has a value wins.

OrderSourceExample
1nuxt.configtelegram: { token: '…' }
2Nuxt runtime configNUXT_PIGEON_CHANNELS_TELEGRAM_TOKEN
3The module's own variablePIGEON_TELEGRAM_BOT_TOKEN

In practice you use the third one and never think about the other two. The exception is the app whose users enter the token on a settings page: that is credentials at runtime.

Never put a token in nuxt.config. It is committed, and for Discord and Slack the url alone is the credential. Only .env is right, and .env belongs in .gitignore.

What will cost you an hour

  • getUpdates returns an empty list until you have written to the bot. The bot cannot start a conversation, Telegram forbids it. You go first, always.
  • A group chat id is negative, for example -1001234567890. That minus sign is part of it.
  • In a group the bot only sees messages meant for it, because privacy mode is on by default. To read everything, make it an admin or turn privacy off in @BotFather with /setprivacy.

Read more