Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Configuring grammY Sessions

Hello again. In our last lesson, you refactored your bot's codebase into a clean, modular structure using the Composer class. This was a crucial step in managing complexity, a principle you're well-acquainted with from your front-end development work. Now, with our code organized, we can address a more fundamental challenge: the stateless nature of bots.

By default, a Telegram bot has no memory. Each incoming message is processed in isolation, without any knowledge of previous interactions. To build anything beyond the simplest command-response bot, we need to give it the ability to store and retrieve data across multiple updates. This lesson is dedicated to solving that problem using grammY's session plugin. You'll learn how to configure sessions to persist user-specific state, effectively giving your bot a memory.

This concept of state management is central to modern application development. Just as you manage component state in React or application state with a store like Redux, we will implement a system to manage conversational state for our bot.

Why Bots Need a Memory

Unlike regular user accounts on Telegram, bots have very limited access to historical data. They operate on a "need-to-know" basis, with access only to the information contained in the current update.

To understand these limitations and why we need an external storage solution, let's start with a foundational read from the grammY documentation.

Sessions and Storing Data (built-in) - grammY

This document explains the core problem of statelessness in Telegram bots and introduces sessions as the solution.

Read the section Why must we think about storage?. It clarifies that a bot only knows about the currently incoming update, which is the key motivation for using sessions.

As the documentation states, if you want your bot to remember anything—a user's preference, their progress in a multi-step process, or simply a count of how many times they've said "hello"—you must store that data yourself. The session plugin provides a clean, elegant middleware-based pattern for this.

How Sessions Work

The session middleware acts as a gatekeeper for your handlers. When an update arrives, it performs two main actions around your bot's logic:

  1. Before your handler runs: It loads the relevant data for the current chat or user from a storage location and attaches it to the context object as ctx.session.
  2. After your handler runs: It takes whatever is currently in ctx.session and saves it back to the storage location.

This means you never have to interact with the database or storage layer directly. You simply read from and write to ctx.session.

Sessions and Storing Data (built-in) - grammY

This section explains the middleware pattern that makes sessions so convenient.

Read the section What are sessions?. The pizza-counting example clearly illustrates the load-modify-save lifecycle of session data.

Implementing In-Memory Sessions

Let's start with the simplest implementation: storing session data in your computer's RAM. This is perfect for development and testing, as it requires zero setup.

There are four key steps to add sessions to your bot:

  1. Define the session data shape: Create a TypeScript interface that describes the data you want to store.
  2. Flavor the context: Update your MyContext type to include SessionFlavor, making ctx.session available and type-safe.
  3. Provide initial data: Create a function that returns a default session object for new users.
  4. Install the middleware: Use bot.use() to register the session middleware.

Let's integrate this into the modular project structure from our previous lesson.

First, let's update src/types.ts (or create it if you haven't) to define our session structure and the flavored context. We'll create a simple session to store a user's preferred name and a message counter.

src/types.ts

import type { Context, SessionFlavor } from "grammy";

// Define the shape of your session data.
export interface SessionData {
  preferredName: string | null;
  messageCount: number;
}

// Flavor the context type to include sessions.
export type MyContext = Context & SessionFlavor<SessionData>;

Next, in your main src/bot.ts file, you'll install the session middleware.

src/bot.ts

import { Bot, session } from "grammy";
import type { MyContext, SessionData } from "./types";
// ... other imports for your features

// 1. Define a function that returns the initial session data.
//    It's crucial that this function returns a NEW object each time!
function initialSession(): SessionData {
  return { preferredName: null, messageCount: 0 };
}

const bot = new Bot<MyContext>(Bun.env.BOT_TOKEN || "");

// 2. Install the session middleware.
bot.use(session({ initial: initialSession }));

// 3. A simple middleware to increment the message count on every message.
bot.use(async (ctx, next) => {
  // Only count messages, not other updates like callback queries
  if (ctx.message) {
      // `ctx.session` is available here!
      ctx.session.messageCount++;
  }
  await next(); // Run the rest of the handlers
});


// ... register your command and other feature modules from the previous lesson
// bot.use(commands);
// bot.use(admin);

// Example usage in a command handler
bot.command("me", async (ctx) => {
  const { preferredName, messageCount } = ctx.session;
  const name = preferredName ?? ctx.from?.first_name ?? "User";
  await ctx.reply(`Hello, ${name}! You have sent ${messageCount} messages to me.`);
});

bot.command("setname", async (ctx) => {
    const newName = ctx.match;
    if (!newName) {
        await ctx.reply("Please provide a name, e.g., `/setname Alex`");
        return;
    }
    ctx.session.preferredName = newName;
    await ctx.reply(`Got it! I will call you ${newName} from now on.`);
});


bot.start();
console.log("Bot with in-memory sessions started!");

Now, run your bot. Send it the /setname command followed by a name. Then, send /me. The bot will remember the name you set. If you restart your bot, however, this memory will be erased.

Persisting Session Data

In-memory sessions are useful for quick tests, but they are not suitable for a production bot because all data is lost on restart. To make our bot's memory permanent, we need to connect the session middleware to a persistent storage solution.

Sessions and Storing Data (built-in) - grammY

This part of the documentation introduces persistent storage adapters, including the zero-setup freeStorage option.

First, read Storing Your Data to understand the need for persistence. Then, skim the overview of adapters. Finally, focus on the Free Storage section, as it's the easiest way to get started.

The grammY team provides a free, persistent cloud storage solution perfect for hobby projects and learning. It uses your bot token for authentication, so there's no complex setup.

Let's upgrade our bot to use it. First, add the package to your project:

bun add @grammyjs/storage-free

Now, modify your src/bot.ts file to import and use freeStorage. The change is minimal.

src/bot.ts (updated for persistent storage)

import { Bot, session } from "grammy";
import { freeStorage } from "@grammyjs/storage-free"; // Import the free storage adapter
import type { MyContext, SessionData } from "./types";

function initialSession(): SessionData {
  return { preferredName: null, messageCount: 0 };
}

const bot = new Bot<MyContext>(Bun.env.BOT_TOKEN || "");

// Install the session middleware WITH the persistent storage adapter.
bot.use(
  session({
    initial: initialSession,
    storage: freeStorage<SessionData>(bot.token), // Pass the bot token here
  })
);

// The rest of your bot code remains exactly the same!
bot.use(async (ctx, next) => {
  if (ctx.message) {
      ctx.session.messageCount++;
  }
  await next();
});

bot.command("me", async (ctx) => {
  const { preferredName, messageCount } = ctx.session;
  const name = preferredName ?? ctx.from?.first_name ?? "User";
  await ctx.reply(`Hello, ${name}! You have sent ${messageCount} messages to me.`);
});

bot.command("setname", async (ctx) => {
    const newName = ctx.match;
    if (!newName) {
        await ctx.reply("Please provide a name, e.g., `/setname Alex`");
        return;
    }
    ctx.session.preferredName = newName;
    await ctx.reply(`Got it! I will call you ${newName} from now on.`);
});


bot.start();
console.log("Bot with PERSISTENT sessions started!");

With this single change to the storage option, your bot's memory is now persistent. Try setting your name, restarting the bot, and sending the /me command again. It will remember you!

Advanced: Customizing Session Scope

By default, grammY stores one session per chat. In a private chat, this is effectively per-user. In a group chat, all members share the same session data. What if you want to store data specifically for each user, even within a group? Or perhaps you need a unique session for every user in every group they share with the bot?

You can achieve this with the getSessionKey option. This function determines the unique identifier for each session.

Sessions and Storing Data (built-in) - grammY

This advanced section explains how to control where and for whom data is stored.

Read the section on Session Keys. Pay close attention to the three code examples that show how to store data per-chat (the default), per-user, and per-user-chat combination.

Let's modify our session configuration to be per-user. This means a user's preferredName and messageCount will be the same whether they talk to the bot in a private chat or mention it in a group.

src/bot.ts (with per-user sessions)

// ... imports

bot.use(
  session({
    initial: initialSession,
    storage: freeStorage<SessionData>(bot.token),
    // Define the session key strategy.
    getSessionKey: (ctx) => ctx.from?.id.toString(),
  })
);

// ... rest of the bot code

By returning the user's ID from getSessionKey, you've changed the fundamental behavior of sessions from chat-based to user-based. This is a powerful technique for tailoring your bot's state management to your specific needs.

Conclusion

Today you've given your bot a memory. You've learned how to overcome the stateless nature of the Telegram Bot API by using grammY's session plugin. This is a foundational skill for building any kind of interactive bot.

Key Takeaways:

  • Telegram bots are inherently stateless and can only access data from the current update.
  • The grammY session middleware provides a simple ctx.session property to store data that persists between updates.
  • Session data is defined with a TypeScript interface and the context is "flavored" with SessionFlavor<YourData>.
  • Sessions start as in-memory, which is great for development but loses all data on restart.
  • By providing a storage adapter, like the zero-config @grammyjs/storage-free, you can make session data persistent.
  • You can control the scope of your sessions (per-chat, per-user, etc.) using the getSessionKey function.

Now that your bot can remember information about its users, the door is open to creating much more complex and engaging interactions. In our next lesson, we will build directly upon this foundation to create multi-step conversation flows, allowing you to guide users through forms and collect structured information.

End of Module,Exit lesson

Can't find a good explanation? Sign up and we'll make it for you

Sign up