Zakirullin Files Telegram Bot Architecture: A Deep Dive into the Go Implementation

The Zakirullin Files Telegram bot is a single-binary Go server that runs three concurrent subsystems—Telegram update polling, an HTTP sync server, and a background worker—routing each user’s updates through dedicated goroutines to a per-user filesystem store.

This open-source project ([zakirullin/files.md](https://github.com/zakirullin/files.md)) implements a unique approach to Telegram bot architecture, treating user data as a filesystem of markdown files rather than a traditional database. Understanding its architecture reveals how to build scalable, stateful bots with strong concurrency guarantees and seamless web integration.

Core Architecture Overview

The bot operates as a monolithic Go binary that simultaneously manages three long-running subsystems:

All subsystems share a unified storage layer. The UserFS (storage/<userID>/*.md) serves as the single source of truth for both Telegram interactions and web-based edits.

Data Storage and State Management

The architecture separates data into three distinct persistence layers:

UserFS: Filesystem as Database

User-generated content lives in a per-user filesystem tree managed by the UserFS abstraction. Each user receives an isolated directory (storage/<userID>/) containing markdown files. This design enables git-like versioning and allows the HTTP server to read and write notes using standard file operations ([server/fs/fs.go](https://github.com/zakirullin/files.md/blob/main/server/fs/fs.go)).

In-Memory Transient State

Runtime state—including last keyboard message IDs, recent commands, and temporary buffers—resides in an in-memory DB package (server/db). This layer provides fast access to ephemeral data that does not require persistence across restarts.

Static Configuration

System-wide preferences and bot settings are loaded from config.json via the server/config package, while per-user preferences (timezones, quick-buttons) are managed by [server/userconfig/userconfig.go](https://github.com/zakirullin/files.md/blob/main/server/userconfig/userconfig.go).

The Bot Core and Request Flow

At the heart of the system lies the Bot type defined in [server/bot.go](https://github.com/zakirullin/files.md/blob/main/server/bot.go). This struct implements the main Reply method, which processes incoming Update objects through a deterministic decision tree:

  1. Inline Query Handling: Responds to inline mode requests immediately
  2. Plugin Execution: Routes text through optional plugins (e.g., world-clock) that can answer without UI interaction
  3. Command Dispatch: Extracts commands and dispatches to a handler map (e.g., /help, /start)
  4. Content Persistence: Saves plain text or image messages to the user's markdown files

The Update abstraction in [server/pkg/tg/upd.go](https://github.com/zakirullin/files.md/blob/main/server/pkg/tg/upd.go) normalizes raw tgbotapi.Update structs into a consistent interface used throughout the bot logic.

Concurrency Model and Goroutine Architecture

The bot achieves per-user sequential consistency through a channel-based worker pattern:

Per-User Supervisor Pattern

When the main update loop receives a message, it resolves the userID and checks for an existing channel. If absent, it spawns a new supervisor goroutine with panic recovery:

ch, ok := userChannels[userID]
if !ok {
    ch = make(chan tgbotapi.Update, 100)
    userChannels[userID] = ch
    go supervisor(userID, ch, telegram) // panic-recovering worker
}
ch <- upd

This pattern ensures that a single user’s updates are processed sequentially, eliminating race conditions on shared markdown files. Different users process updates in parallel, each with isolated UserFS and in-memory DB instances.

Thread-Safe File Access

Because the bot never writes concurrently for the same user, the HTTP sync server can safely read and write the same markdown files without locking mechanisms. This architecture simplifies the concurrency model while maintaining data integrity.

Key Components and Source Files

Component File Path Responsibility
Entry Point [cmd/server/server.go](https://github.com/zakirullin/files.md/blob/main/cmd/server/server.go) Initializes Telegram client, starts update loop, launches HTTP server
Bot Logic [server/bot.go](https://github.com/zakirullin/files.md/blob/main/server/bot.go) Implements Bot.Reply, command routing, and plugin orchestration
Telegram Wrapper [server/pkg/tg/tg.go](https://github.com/zakirullin/files.md/blob/main/server/pkg/tg/tg.go) Thin abstraction over Bot API for sending, editing, and deleting messages
Update Adapter [server/pkg/tg/upd.go](https://github.com/zakirullin/files.md/blob/main/server/pkg/tg/upd.go) Normalizes Telegram updates into the internal Update interface
Filesystem Layer [server/fs/fs.go](https://github.com/zakirullin/files.md/blob/main/server/fs/fs.go) Handles Write, Read, directory creation, and quota enforcement
User Config [server/userconfig/userconfig.go](https://github.com/zakirullin/files.md/blob/main/server/userconfig/userconfig.go) Manages per-user config.json (timezones, quick-buttons)
Example Plugin [server/plugins/world_clock.go](https://github.com/zakirullin/files.md/blob/main/server/plugins/world_clock.go) Demonstrates autonomous text processing without bot UI

Implementation Examples

Initializing the Update Loop and Supervisors

The main function in [cmd/server/server.go](https://github.com/zakirullin/files.md/blob/main/cmd/server/server.go#L48-L66) demonstrates the startup sequence and long-polling mechanism:

func main() {
    _ = godotenv.Load()
    _ = config.LoadBotConfig()

    api, err := tgbotapi.NewBotAPI(config.ServerCfg.BotAPIToken)
    if err != nil {
        fmt.Printf("No Telegram bot token: %s\n", err)
        select {} // web-only mode
    }
    telegram := tg.NewTG(api)

    tgConfig := tgbotapi.NewUpdate(0)
    tgConfig.Timeout = 60
    updates := api.GetUpdatesChan(tgConfig)

    userChannels := map[int64]chan tgbotapi.Update{}
    for upd := range updates {
        tgUpd := tg.NewTGUpd(upd)
        userID, _ := resolveUserID(tgUpd, telegram)

        ch, ok := userChannels[userID]
        if !ok {
            ch = make(chan tgbotapi.Update, 100)
            userChannels[userID] = ch
            go supervisor(userID, ch, telegram)
        }
        ch <- upd
    }
}

Sending Messages with Inline Keyboards

The [server/pkg/tg/tg.go](https://github.com/zakirullin/files.md/blob/main/server/pkg/tg/tg.go#L34-L46) wrapper simplifies message composition:

func (tg *TG) SendHTML(userID int64, text string, kb *Keyboard) (int, error) {
    return tg.Send(userID, text, kb, MarkupHTML)
}

// Usage in a command handler
func (b *Bot) showHelp(_ []string) error {
    kb := tg.NewInlineKeyboard(
        tg.Button{Text: "Docs", URL: "https://github.com/zakirullin/files.md"},
        tg.Button{Text: "Close", CallbackData: CmdDoNothing},
    )
    _, err := b.tg.SendHTML(b.userID, "🤖 Welcome to Files Bot!", kb)
    return err
}

Registering a New Command Handler

Adding commands requires three steps in [server/bot.go](https://github.com/zakirullin/files.md/blob/main/server/bot.go#L24-L33):

// 1. Define constant
const CmdPing = "ping"

// 2. Register in handler map
handlers[CmdPing] = b.handlePing

// 3. Implement handler
func (b *Bot) handlePing(_ []string) error {
    _, err := b.tg.Send(b.userID, "🏓 Pong!", nil, tg.MarkupHTML)
    return err
}

Summary

  • The Zakirullin Files bot uses a single-binary architecture combining Telegram polling, HTTP serving, and background workers in one Go process.
  • Per-user goroutines with supervisor patterns ensure sequential update processing while enabling parallel user handling.
  • UserFS treats markdown files as the primary data store, creating a natural bridge between the Telegram bot and the web-based PWA.
  • The Bot.Reply method serves as the central router, handling inline queries, plugins, commands, and content persistence through a deterministic decision tree.
  • All file operations flow through the server/fs package, maintaining consistency between chat interactions and sync API operations.

Frequently Asked Questions

How does the bot handle concurrent updates from multiple users?

Each user receives a dedicated goroutine with a buffered channel. When an update arrives, the main loop routes it to the user's specific channel, ensuring sequential processing per user while allowing parallel execution across different users. This eliminates file corruption risks without requiring complex locking mechanisms.

What storage backend does the bot use for notes and messages?

The bot uses the local filesystem as its primary storage backend. User data resides in storage/<userID>/*.md markdown files accessed through the UserFS abstraction. Transient runtime state stays in an in-memory map, and user preferences are stored in JSON configuration files.

Can the bot function without a Telegram token?

Yes. If the Telegram Bot API token is missing or invalid, the binary enters "web-only mode" by blocking indefinitely with select {}, allowing the HTTP sync server and PWA to operate independently of the Telegram integration.

How are commands processed within the bot architecture?

Commands flow through the Bot.Reply method in server/bot.go, which extracts the command string and dispatches to a predefined handler map. Developers register new commands by defining a constant, adding an entry to the handler map, and implementing a method with the signature func ([]string) error.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →