Skip to content

migrating from v2

v3 is a hard break. there is no compatibility layer, no codemod, and there will not be one. the api shape changed significantly enough that a one-to-one mechanical translation wouldn't be safe. that said, the concepts map cleanly once you know what moved where

the clean break

v2 is frozen at 2.27.0 on the lord branch. npm tarballs for [email protected] stay published forever — existing installs don't break. once v3 ships, lord is overwritten with v3 and the repo becomes a v3 repo. packages dropped in v3 (@puregram/hear, @puregram/prompt) will have their source removed from the tree, but the published tarballs remain

big renames

v2v3notes
Context (base type + the thing handlers receive)Update"context" is gone as a concept. the wrapped update is an Update
MessageContextMessageUpdateevery context class → matching *Update class
telegram.updates.on('message', handler)tg.onMessage(handler)per-kind dispatchers are first-class on Telegram
telegram.updates.on('callback_query', handler)tg.onCallbackQuery(handler)same pattern for every update kind
telegram.updates.use(middleware)tg.use(middleware)middleware is now on Telegram directly
telegram.updates.startPolling()tg.startPolling()polling is on Telegram, not Updates
context.is('message')update.is('message')same method, different class
context.chatIdupdate.raw.chat.id (or a shortcut)anchors are codegen'd — see the three-layer api
context.fromupdate.raw.fromraw payload access is always .raw.*

handlers still receive the update

the handler signature is the same shape: tg.onMessage(async (m) => { ... }). m is now a MessageUpdate instead of MessageContext, but the pattern is identical

reply shortcuts still exist

v2's context.reply(text) → v3's message.reply(text). they work the same way:

ts
// v2
telegram.updates.on('message', async (context) => {
  await context.reply('hello!')
  await context.replyWithPhoto(MediaSource.url('https://example.com/cat.jpg'))
})

// v3
tg.onMessage(async (message) => {
  await message.reply('hello!')
  await message.replyWithPhoto(MediaSource.url('https://example.com/cat.jpg'))
})

the full reply* family — reply, replyWithPhoto, replyWithVideo, replyWithDocument, etc. — is codegen'd onto every update kind that has a chat_id anchor

dropped runtime dependencies

v2 depgone becausev3 replacement
middleware-iov3 has its own priority dispatchtg.use(mw) / tg.useHook(...) built-in
inspectable + reflect-metadata + experimentalDecoratorsruntime cost, forced decorator flagsSymbol.for('nodejs.util.inspect.custom') — codegen'd
undicinode 22 has native fetchpluggable HttpClient (native fetch default)
debugown logger with same interfacePUREGRAM_DEBUG env var, puregram:* namespaces

dropped packages

v2 packagestatusv3 path
@puregram/heargonecompose a tg.command(...) or tg.onMessage(filter, handler) — filter is userland
@puregram/promptgonefolded into @puregram/flow as flow.prompt(...)

mergeMediaEvents (a Telegram constructor option in v2) is gone as a core option. in v3, media group collection lives in @puregram/flow as collectMediaGroup

plugin system

v2 had no plugin system — satellites were separate libraries you imported and wired up by hand. v3 has first-class plugins:

ts
// v2 — import and wire manually
import { SessionManager } from '@puregram/session'
const session = new SessionManager(...)
telegram.updates.use(session.middleware)

// v3 — extend the Telegram instance
import { session } from '@puregram/session'
const tg = Telegram.fromToken(token)
  .extend(session())

// tg.session is now typed and available in every handler
tg.onMessage(async (m) => {
  m.session.count ??= 0
  m.session.count++
})

plugins namespace under their name (update.flow, update.session, update.scene). there is no declare module 'puregram' augmentation — the type follows from .extend(plugin)

common pattern translation

v2 patternv3 equivalent
telegram.updates.on('message', h)tg.onMessage(h)
telegram.updates.on(['message', 'edited_message'], h)tg.onMessage(h); tg.onEditedMessage(h) (or tg.onUpdate(filter, h))
context.reply('text')message.reply('text')
context.send('text')message.send('text')
context.replyWithPhoto(src)message.replyWithPhoto(src)
telegram.api.sendMessage({ chat_id, text })tg.api.sendMessage({ chat_id, text }) — same
new TelegramMessage(payload) wrapperauto-wrapped as a MessageUpdate with .raw
context.is('message') type predicateupdate.is('message') — same
telegram.updates.use(mw) middlewaretg.use(mw) — same concept
telegram.api.sendMessage({ ..., suppress: true })tg.api.sendMessage({ ..., suppress: true }) — same

module format

v2 was CJS — require('puregram') worked. v3 is ESM-only:

ts
// v3 only — ESM
import { Telegram } from 'puregram'

if your project still uses CJS (require), you'll need to either:

  • add "type": "module" to package.json and convert imports, or
  • use a dynamic import() at your entry point

node 22+ is required

what to do

  1. read the your first bot page — the minimal v3 setup is a few lines
  2. check the three-layer api to understand where each kind of call lives
  3. for sessions/scenes/flow — install the satellite packages and use .extend(plugin). the READMEs in each packages/<name>/ have quick-start examples
  4. for anything genuinely unclear, open an issue

see also