Credentials at runtime
.env is right for most apps. It is wrong for one kind: the app whose users enter the
Discord webhook or the Mastodon token on a settings page. Those values live in a
database, and changing them must not need a redeploy.
configure() is for that. Every channel has it.
Copy this
export default defineNuxtConfig({
modules: ['nuxt-pigeon'],
nuxtPigeon: {
channels: {
mastodon: { credentials: 'runtime', receive: true },
},
},
})
export default defineNitroPlugin(async () => {
const settings = await loadSettings() // your database, your shape
if (settings.mastodon) {
await mastodon.configure(settings.mastodon)
}
})
export default defineEventHandler(async (event) => {
const body = await readBody<{ instance: string; token: string }>(event)
await saveSettings({ mastodon: body })
await mastodon.configure(body) // applies now, the poller restarts on the new account
return mastodon.status()
})
That runs. Nothing is read from .env for Mastodon, the poller starts when the
plugin hands the values over, and the settings route swaps them without anyone
restarting anything.
What just happened
credentials: 'runtime'told the channel to ignore.envand wait. Without it the channel would have started on whatever the environment had, and theconfigure()in the plugin would have restarted it a second later.configure()stopped, set, and started. For a channel that polls, the poller was torn down and a new one began with no memory of the old account: its first round only marks the position, so nothing is replayed. Bluesky drops its session as well.- The values live in memory. A process restart forgets them, which is why the plugin hands them over again at every boot.
The four verbs
Every channel has them, and they do the same thing everywhere:
| Call | Does |
|---|---|
configure({ … }) | Sets new credentials and starts, or restarts, on them |
configure() | Goes back to the environment. In runtime mode, where there is none, that means off |
restart() | Same credentials, fresh state. A poller starts over from now |
stop() | Off. The values stay, so restart() brings it back |
status() | { running, source, configured }, for the settings page to show what is going on |
source is 'static' for the environment, 'runtime' after a configure(), and
'none' when nothing is there. running is only ever true for Mastodon and Bluesky,
the two that poll. The others have nothing that runs.
What configure() takes
The same fields the environment would, camel cased. Everything is optional, and in the default mode a field you leave out keeps its environment value.
| Channel | Fields | Needed to work |
|---|---|---|
telegram | token, chatId, secretToken | token |
discord | webhookUrl | webhookUrl |
slack | botToken, channel, webhookUrl, signingSecret | botToken or webhookUrl |
ntfy | server, topic, token | topic |
mastodon | instance, token | both |
bluesky | service, identifier, password | identifier and password |
webhook | url, secret, headers, endpoints: { name: { … } } | nothing, a call can name its url |
Hand over less than what is needed and configure() throws the same sentence a send
would, naming the field.
Two modes, one option
credentials | At startup | configure() |
|---|---|---|
'static', the default | Reads .env, starts if there is something there | Replaces it, warns that the channel started once for nothing |
'runtime' | Ignores .env, waits | Starts the channel. No comment |
The warning in the default mode is the whole reason the option exists. Seeing it means
one line in nuxt.config makes it go away, and until then nothing is broken: the
values from configure() win either way.
'runtime' mode a leftover PIGEON_MASTODON_TOKEN in .env is ignored and
mentioned once in the log. That is on purpose. The alternative, starting on the old
account and switching a moment later, would have polled as the wrong person.What will cost you an hour
- A new Telegram token is a different bot. The webhook is registered per bot, so
after
telegram.configure({ token })Telegram still delivers to the old one, which means to nobody. Calltelegram.setWebhook(baseUrl)again. - Telegram and Slack receive on a route, which reads its
secretTokenorsigningSecretper request. Aconfigure()applies to the next delivery, there is nothing to restart. Before the firstconfigure()inruntimemode the route answers 503, nothing is accepted on a guess. - Serverless has no process to keep the values in. Every isolate starts empty, and the pollers do not run there anyway. Use the environment there, that is what it is for.
configure()is not for switching between accounts per request. It is one set of credentials per process, replaced when they change. To post into a second Discord channel, passwebhookUrlon the call, the same waychatIdworks for Telegram:await discord.send('Release 1.2', { webhookUrl: process.env.DISCORD_RELEASES })editanddeleteneed it again. The url is the credential and is never kept in the handle, which is an object you pass around and log.
Read more
- Installation - where the environment variables are read, and in what order
- Receiving - what polls and what listens on a route
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.
Sending text
Send text to Telegram, Discord, Slack, ntfy, Mastodon and Bluesky with one verb. Formatting differs per service, and every limit is checked before sending.