Skip to content

starter-series/telegram-bot-starter

Repository files navigation

Telegram Bot Starter

grammY + Docker + GitHub Actions CI/CD + one-click deploy.

Build your bot. Push to deploy.

CI License: MIT grammY Node Docker

English | 한국어


Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.

Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server · Python MCP Server · Cloudflare Pages


Quick Start

Via create-starter (recommended):

gh repo create my-telegram-bot --template starter-series/telegram-bot-starter --clone
cd my-telegram-bot && npm install
npm run preflight
cp .env.example .env  # fill in BOT_TOKEN from @BotFather
npm run dev

Or clone directly:

git clone https://github.com/starter-series/telegram-bot-starter my-telegram-bot
cd my-telegram-bot && npm install
npm run preflight
cp .env.example .env
npm run dev

See docs/TELEGRAM_SETUP.md for the detailed BotFather guide.

What's Included

├── src/
│   ├── index.js                  # Entry point (polling or webhook)
│   ├── config.js                 # Environment config loader
│   ├── commands/                 # Bot commands (auto-loaded)
│   │   ├── start.js              # /start — greeting
│   │   └── help.js               # /help — list commands
│   ├── handlers/                 # Message handlers (auto-loaded)
│   │   └── echo.js               # Echo text messages
│   └── lib/
│       ├── health.js             # GET /health HTTP server
│       ├── logger.js             # Structured JSON logger
│       ├── rate-limiter.js       # Per-user rate limiter
│       └── safe-reply.js         # Non-throwing reply wrapper (handles Telegram 429 Retry-After)
├── scripts/
│   └── bump-version.js           # Bump package.json version
├── tests/
│   ├── commands.test.js          # Command + handler tests
│   ├── health.test.js            # Health endpoint tests
│   └── safe-reply.test.js        # safe-reply 429-retry behavioral tests
├── Dockerfile                    # Production container
├── docker-compose.yml            # Dev with hot reload
├── .github/
│   ├── workflows/
│   │   ├── ci.yml                # Audit, lint, test, Docker build
│   │   ├── cd-railway.yml        # Deploy to Railway
│   │   ├── cd-fly.yml            # Deploy to Fly.io
│   │   └── setup.yml             # Auto setup checklist on first use
│   └── PULL_REQUEST_TEMPLATE.md
├── docs/
│   ├── TELEGRAM_SETUP.md         # BotFather setup guide
│   └── DEPLOY_GUIDE.md           # Railway & Fly.io deployment guide
└── package.json

Features

  • grammY — Modern, fast, lightweight Telegram bot framework
  • CI Pipeline — Security audit, lint, test, Docker build verification on every push
  • CD Pipeline — One-click deploy to Railway or Fly.io + auto GitHub Release
  • Docker — Production Dockerfile + dev compose with hot reload
  • Polling & Webhook — Long polling by default, webhook mode via env var
  • Health endpoint — Built-in GET /health + Docker HEALTHCHECK so Fly.io / Railway can detect a crashed bot
  • Version managementnpm run version:patch/minor/major to bump package.json
  • Dev modenpm run dev for live reload with node --watch
  • Starter code/start + /help commands, echo handler, modular structure
  • Safe replysafe-reply.js swallows reply errors and obeys Telegram's 429 Retry-After so a single rate-limited send never crashes the bot loop
  • Deploy guides — Step-by-step docs for BotFather, Railway, and Fly.io
  • Template setup — Auto-creates setup checklist issue on first use
  • Supply-chain hardeningnpm ci --ignore-scripts in CI, committed lockfile, gitleaks pinned by sha256, Railway CLI pinned by version

CI/CD

CI (every PR + push to main)

Step What it does
Security audit npm audit for dependency vulnerabilities
Lint ESLint for code quality
Build Syntax smoke check for shipped JavaScript files
Test Jest with coverage thresholds
Docker build Builds the container image to catch build errors
Trivy scan Gates the container image on CRITICAL CVEs

Security & Maintenance

Workflow What it does
CodeQL (codeql.yml) Static analysis for security vulnerabilities (push/PR + weekly)
Maintenance (maintenance.yml) Weekly CI health check — auto-creates issue on failure
Stale (stale.yml) Labels inactive issues/PRs after 30 days, auto-closes after 7 more

CD (manual trigger via Actions tab)

Step What it does
Version guard Fails if git tag already exists for this version
Deploy Pushes to Railway or Fly.io
GitHub Release Creates a tagged release with auto-generated notes

How to deploy:

  1. Set up GitHub Secrets (see below)
  2. Bump version: npm run version:patch (or version:minor / version:major)
  3. Go to Actions tab → Deploy to Railway (or Fly.io) → Run workflow

GitHub Secrets

Railway (cd-railway.yml)

Secret Description
RAILWAY_TOKEN Railway API token
RAILWAY_SERVICE_ID Target service ID

See docs/DEPLOY_GUIDE.md for setup guide.

Fly.io (cd-fly.yml)

Secret Description
FLY_API_TOKEN Fly.io deploy token

See docs/DEPLOY_GUIDE.md for setup guide.

Development

# Start with hot reload
npm run dev

# Or use Docker
docker compose up

# Token-free preflight: syntax smoke + Jest
npm run preflight

# Bump version (updates package.json)
npm run version:patch   # 1.0.0 → 1.0.1
npm run version:minor   # 1.0.0 → 1.1.0
npm run version:major   # 1.0.0 → 2.0.0

# Lint & test
npm run lint
npm run build
npm test

Adding Commands

Create a new file in src/commands/:

// src/commands/echo.js
module.exports = {
  name: 'echo',
  description: 'Echo your message',
  async execute(ctx) {
    const text = ctx.match || 'Nothing to echo';
    await ctx.reply(text);
  },
};

Commands are auto-loaded — no need to edit any other file.

Adding Handlers

Create a new file in src/handlers/:

// src/handlers/photo.js
module.exports = {
  name: 'photo',
  register(bot) {
    bot.on('message:photo', async (ctx) => {
      await ctx.reply('Nice photo!');
    });
  },
};

Handlers are auto-loaded — no need to edit any other file.

Security notesafeReply(ctx, text) defaults to plain text, which is safe to echo. If you switch to safeReply(ctx, text, { parse_mode: 'HTML' }) or 'MarkdownV2', escape user input first or an attacker who controls bot input can forge fake links / fake buttons. See grammY formatting docs.

Why This Over Heavyweight Templates?

donbarbos/telegram-bot-template (450+ stars) is the most complete Telegram bot template. This template takes a different approach:

This template donbarbos/telegram-bot-template
Philosophy Thin starter with CI/CD Full production stack
Framework grammY (JS) aiogram (Python)
Infrastructure Docker only Docker + PostgreSQL + Redis + Grafana
Dependencies 2 runtime 15+
Learning curve Read the grammY docs Learn the full stack
CI/CD Full pipeline included Full pipeline included
AI/vibe-coding LLMs generate clean JS LLMs must handle Python + ORM + monitoring
Best for Utility bots, simple automation Large bots with DB, caching, monitoring

Choose this template if:

  • You want a lightweight bot that's easy to understand and extend
  • You need production CI/CD + Docker without the heavyweight stack
  • You're using AI tools to generate bot code — simple JS produces the cleanest output
  • Your bot doesn't need a database or Redis

Choose donbarbos if:

  • You need PostgreSQL, Redis, or monitoring from day one
  • You prefer Python/aiogram over JavaScript/grammY
  • You want a full production stack out of the box

What about TypeScript?

This template uses JavaScript for simplicity. To add TypeScript:

  1. Add typescript and @types/node to devDependencies
  2. Add a tsconfig.json
  3. Update npm start to compile and run from dist/
  4. Rename .js files to .ts

TypeScript is opt-in, not forced. For many bots, JavaScript is all you need.

Health Check

The bot exposes a tiny HTTP health server (src/lib/health.js) so Docker, Fly.io, and Railway can detect a crashed or stuck bot process.

Mode Port Endpoint
Polling (default) HEALTH_PORT (default 3000) — standalone server GET /health
Webhook (WEBHOOK_URL set) PORT — mounted on the webhook server, no extra listener GET /health
Status Body
200 OK (ready) { "status": "ok", "uptime": <seconds>, "mode": "polling"|"webhook" }
503 Service Unavailable (starting / disconnected) { "status": "starting", "uptime": <seconds>, "mode": "polling"|"webhook" }

Configuration

# .env
HEALTH_PORT=3000   # polling mode only — change if 3000 is already taken
# PORT=3000        # webhook mode — /health is mounted on this port
# WEBHOOK_SECRET=  # webhook mode only — required when WEBHOOK_URL is set

Fly.io — add an HTTP service check to fly.toml:

[[http_service]]
  internal_port = 3000
  force_https = true

  [[http_service.checks]]
    interval = "30s"
    timeout = "5s"
    grace_period = "30s"
    method = "GET"
    path = "/health"

Railway — in the service's Settings → Deploy, set the health-check path to /health and the port to 3000.

Dockerdocker ps will show (healthy) / (unhealthy) automatically; the HEALTHCHECK runs wget --spider http://localhost:${HEALTH_PORT}/health every 30s.

Scope

Currently implemented

  • grammY bot with polling (default) and webhook modes, /start + /help commands, echo handler — all auto-loaded from src/commands/ and src/handlers/.
  • src/lib/safe-reply.js — non-throwing reply wrapper that respects Telegram's 429 Retry-After (100% line coverage).
  • src/lib/rate-limiter.js, src/lib/health.js, src/lib/logger.js.
  • CI: npm audit --audit-level=high, ESLint, Jest with --coverage, Docker build, Trivy CRITICAL CVE scan, CodeQL (weekly), gitleaks (sha256-pinned).
  • CD: Railway and Fly.io workflows with a version-tag guard and auto-generated GitHub Releases.
  • Test suite: Jest covers bootstrap wiring, env validation, lifecycle, commands, handlers, health, logging, rate limiting, and safe replies (see npm test output).

Planned

  • None. A starter is finished when it ships; further features go into downstream projects, not here.

Design intent

  • Two runtime dependencies (grammy, dotenv) and plain JavaScript — so an LLM extending this repo doesn't have to reason about an ORM, a transpile step, or a type system to add a command.
  • safe-reply is non-throwing on purpose: a single rate-limited reply must never crash the polling loop or webhook handler. Errors are logged via the structured logger, not propagated.
  • Auto-loading via src/commands/index.js and src/handlers/index.js: drop a file in, the bot picks it up — no central registry to edit, fewer merge conflicts for AI-generated PRs.
  • --ignore-scripts in CI: lifecycle scripts are a known supply-chain attack surface; the starter opts out by default.
  • Health endpoint piggybacks on the webhook server when WEBHOOK_URL is set — one listener, not two.

Non-goals

  • TypeScript (opt-in path documented above; not the default).
  • Database, ORM, Redis, Grafana, payments, sessions/FSM, i18n — use donbarbos/telegram-bot-template instead (see comparison table above).
  • Wrapping the full Telegram Bot API surface — this starter exposes grammY directly.
  • A plugin marketplace or theme system; a starter is meant to be forked, not extended.

Redacted

  • None. This is a public starter; there are no internal references, customers, or accounts to redact.

Contributing

PRs welcome. Please use the PR template.

License

MIT

Releases

No releases published

Packages

 
 
 

Contributors