Recommended resources
The freely available resources this lesson will direct you to.
Welcome back to the final module of our course! In the previous module, you built a sophisticated, robust, and responsive AI assistant. It's feature-complete and running flawlessly on your local machine using long polling. Now, it's time to prepare it for the real world. A bot running on your laptop with bun start isn't very useful if you want it to be available 24/7.
This module is all about production deployment. We'll take your bot from a local development process to a cloud-based service. The first and most crucial step in this transition is changing how your bot receives updates from Telegram. We will move from the development-friendly long polling method to the production-standard webhook mechanism.
In this lesson, you will set up a webhook endpoint for your bot. This involves replacing bot.start() with a proper HTTP server that listens for incoming updates from Telegram. Given our stack, you'll learn to do this using Bun's high-performance, built-in HTTP server, Bun.serve, and see how seamlessly it integrates with grammY.
From Pull to Push: Long Polling vs. Webhooks
So far, your bot has been using long polling. In this mode, your bot's code actively reaches out to Telegram's servers and asks, "Are there any new messages for me?". It keeps an open connection and waits for a response. If there are no messages, the connection times out, and your bot immediately makes a new request. This is a "pull" model. It's simple, works out of the box without any network configuration, and is perfect for development.
For a production environment, however, this is inefficient. A much more scalable and resource-friendly approach is using webhooks. With a webhook, the roles are reversed. You provide Telegram with a public URL—your webhook endpoint. Then, instead of you asking for updates, Telegram instantly sends an HTTP POST request to your URL whenever a new update for your bot occurs. This is a "push" model.
The grammY documentation provides a nice analogy to illustrate this difference.
This guide from the official grammY documentation explains the core concepts behind the two main deployment types for Telegram bots.
Read the introduction and the section How Do Webhooks Work?. The ice cream analogy is a helpful way to conceptualize the "pull" vs. "push" dynamic.
The Webhook Lifecycle: A Visual Guide
The webhook model changes the architecture of our application. It's no longer a simple script but a web service. Let's look at the sequence of events.

Let's trace the steps shown in the diagram:
- User sends a message: The user interacts with your bot in their Telegram client.
- Telegram -> Your Webhook: Telegram's servers immediately make an HTTP POST request to the public URL you registered. The body of this request contains the JSON payload of the
Updateobject. - Webhook -> Router: Your HTTP server (which we'll build with
Bun.serve) receives this request. It parses the JSON body and passes the resultingUpdateobject to the grammY framework. - Router -> Business Logic: grammY's router inspects the
Updateobject and directs it to the correct handler you've defined (e.g.,bot.on("message:text", ...)orbot.command("start", ...)). - Execution and Reply: Your handler code (the business logic) executes. When you call a method like
ctx.reply(), grammY makes a request to the Telegram Bot API. - Telegram -> User: Telegram processes the API call and delivers your bot's response to the user in their chat.
Our task today is to implement the "FastAPI App (Webhook)" part of this diagram using Bun and grammY.
Building the Server with Bun.serve
Bun is not just a runtime and package manager; it also includes a highly optimized, built-in HTTP server. Your experience building complex front-end applications means you're very familiar with the client-side fetch API. Bun's server design elegantly mirrors this by having you provide a fetch handler function.
Let's see how simple it is to get a server running. The following video gives a very quick demonstration.
I Stopped Using Express.js: Because Bun and Hono 🔥
This video by Sam Meech-Ward demonstrates creating a basic server with Bun.
Watch the short segment from the beginning where he introduces Bun.serve. Notice how it just takes a fetch function as its core logic.
As you can see, the basic structure is:
Bun.serve({
fetch(req) {
return new Response("Hello, world!");
},
port: 3000,
});
console.log("Listening on port 3000...");
This is all the code needed for a minimal HTTP server. Now, how do we get this server to talk to our grammY bot?
Integrating grammY with Bun.serve
The grammY framework is designed to be web-framework-agnostic. It achieves this through a system of adapters. You simply tell grammY which server environment you're using, and it provides the correct middleware function to handle incoming requests.
Instead of calling bot.start(), which is exclusively for long polling, we will use the webhookCallback function.
This part of the grammY documentation explains how to integrate the bot with a web server using webhookCallback.
First, read the section How to Use Webhooks. Pay attention to the use of webhookCallback and the important note that you must not call bot.start(). Then, scroll down to the Web Framework Adapters section. In the table, you'll find an entry for 'bun', which corresponds to Bun.serve. This is the key to connecting grammY to our server.
The webhookCallback(bot, "bun") function generates a request handler that is perfectly compatible with Bun.serve. It takes a Request object and returns a Promise<Response>, which is exactly what the fetch property of Bun.serve expects.
Putting It All Together
Let's modify our bot's entry point to switch from long polling to serving via a webhook. We'll create a new file, src/server.ts, to contain our webhook logic, keeping the core bot definition in src/bot.ts. This separation of concerns is good practice.
First, let's slightly refactor src/bot.ts. We need to export the bot instance so we can import it into our server file, and we should remove the startup function and bot.start() call.
src/bot.ts (Modified)
// src/bot.ts
import { Bot, Context, session, SessionFlavor } from "grammy";
import { claude } from "./services/claude";
import Anthropic from "@anthropic-ai/sdk";
// --- Type Definitions ---
// (Your existing Message, SessionData, MyContext types)
interface Message {
role: "user" | "assistant";
content: string;
}
interface SessionData {
messages: Message[];
}
type MyContext = Context & SessionFlavor<SessionData>;
// --- Bot Initialization ---
// IMPORTANT: Export the bot instance
export const bot = new Bot<MyContext>(process.env.TELEGRAM_BOT_TOKEN!);
bot.use(session({
initial: (): SessionData => ({ messages: [] }),
}));
// --- Bot Handlers ---
// (All your bot.command, bot.on, etc. handlers remain exactly the same)
const resetSession = (ctx: MyContext) => {
ctx.session.messages = [];
return ctx.reply("Hello! I'm an AI assistant. I've cleared our previous conversation. How can I help you?");
};
bot.command("start", resetSession);
bot.command("new", resetSession);
bot.on("message:text", async (ctx) => {
// ... your full message handler logic
});
// --- General Error Handling ---
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Error while handling update ${ctx.update.update_id}:`);
console.error(err.error);
});
// REMOVED: The old startup function that called bot.start()
Now, let's create the new server file.
src/server.ts (New File)
// src/server.ts
import { webhookCallback } from "grammy";
import { bot } from "./bot"; // Import the bot instance
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
Bun.serve({
// The webhookCallback function generates a handler compatible with Bun.serve
fetch: webhookCallback(bot, "bun"),
port: port,
});
console.log(`Bot server listening on port ${port}...`);
And finally, update your package.json to run this new server file.
package.json
{
// ...
"scripts": {
"start": "bun run src/server.ts", // Changed from bot.ts
"dev": "bun --watch src/server.ts"
},
// ...
}
Now, when you run bun start, you will see "Bot server listening on port 3000...". Your bot is no longer polling Telegram. Instead, it's running as a web server, waiting for Telegram to send it updates.
What's Next: The Public URL Problem
There's one piece of the puzzle missing. Your server is running on localhost:3000. Telegram's servers on the public internet have no way to send a request to this address. For a webhook to work, it needs a publicly accessible URL.
In the next lesson, we will solve this. You'll learn how to register your webhook URL with Telegram using a simple script. We will also discuss strategies for obtaining a public URL, which is the final step before deploying your bot to a cloud platform. For local development and testing of webhooks, tools like ngrok can create a secure tunnel to your local machine, giving you a temporary public URL. We'll dive into this process and secure our endpoint in the next session.
Conclusion
You have successfully taken the first, most significant step toward a production-ready bot. By replacing long polling with a webhook, you've transitioned your application to a more efficient and scalable architecture.
Key Takeaways:
- Webhooks use a "push" model, where Telegram sends updates to your server, which is more efficient for production than the "pull" model of long polling.
- Bun's built-in
Bun.serveprovides a simple, high-performance HTTP server. - The
grammYwebhookCallback(bot, "bun")function seamlessly integrates your bot withBun.serve. - When using webhooks, you must not call
bot.start(). - A functioning webhook requires a publicly accessible URL, which we will address in the next lesson.
With this new server structure in place, you are perfectly positioned to deploy and run your bot in the cloud.