Skip to content

vcode-sh/better-auth-telegram

Better Auth Telegram

npm version npm downloads CI codecov License: MIT

Telegram authentication plugin for Better Auth. Login Widget. Mini Apps. Link/unlink. HMAC-SHA-256 verification. The whole circus.

Built on Web Crypto API — works in Node, Bun, Cloudflare Workers, and whatever edge runtime you're pretending to need. No node:crypto tantrums.

117 tests. If it breaks, roast me on X. If it works, also roast me. I'm there either way, posting through the pain.

Requirements

  • Node.js >= 22 (or Bun, or any runtime with Web Crypto API)
  • better-auth@^1.4.18

Install

npm install better-auth-telegram

Setup

1. Talk to a bot to create a bot

Message @BotFather, send /newbot, save the token, then /setdomain with your domain.

For local dev you'll need ngrok because Telegram demands HTTPS. Localhost? Never heard of it.

2. Server

import { betterAuth } from "better-auth";
import { telegram } from "better-auth-telegram";

export const auth = betterAuth({
  plugins: [
    telegram({
      botToken: process.env.TELEGRAM_BOT_TOKEN!,
      botUsername: "your_bot_username", // without @
    }),
  ],
});

3. Client

import { createAuthClient } from "better-auth/client";
import { telegramClient } from "better-auth-telegram/client";

export const authClient = createAuthClient({
  fetchOptions: {
    credentials: "include", // required for link/unlink
  },
  plugins: [telegramClient()],
});

4. Database

The plugin adds telegramId and telegramUsername to both user and account tables. If using Prisma:

model User {
  // ... existing fields
  telegramId       String?
  telegramUsername  String?
}

model Account {
  // ... existing fields
  telegramId       String?
  telegramUsername  String?
}

Then npx prisma migrate dev and pray.

Usage

Sign in

authClient.initTelegramWidget(
  "telegram-login-container",
  { size: "large", cornerRadius: 20 },
  async (authData) => {
    const result = await authClient.signInWithTelegram(authData);
    if (!result.error) router.push("/dashboard");
  }
);

Link / Unlink

// link (user must be authenticated)
await authClient.linkTelegram(authData);

// unlink
await authClient.unlinkTelegram();

Getting "Not authenticated"? You forgot credentials: "include". Go back to Client setup.

All API-calling client methods accept an optional fetchOptions parameter for custom headers, cache control, etc:

await authClient.signInWithTelegram(authData, {
  headers: { "x-custom-header": "value" },
});

Redirect flow

authClient.initTelegramWidgetRedirect(
  "telegram-login-container",
  "/auth/telegram/callback",
  { size: "large" }
);

Mini Apps

Enable on server:

telegram({
  botToken: process.env.TELEGRAM_BOT_TOKEN!,
  botUsername: "your_bot_username",
  miniApp: {
    enabled: true,
    validateInitData: true,
    allowAutoSignin: true,
  },
});

Then on client:

// auto sign-in (one less click, revolutionary)
const result = await authClient.autoSignInFromMiniApp();

// or manual
const result = await authClient.signInWithMiniApp(
  window.Telegram.WebApp.initData
);

// or just validate without signing in
const validation = await authClient.validateMiniApp(
  window.Telegram.WebApp.initData
);

Configuration

Option Default Description
botToken required From @BotFather
botUsername required Without the @
allowUserToLink true Let users link Telegram to existing accounts
autoCreateUser true Create user on first sign-in
maxAuthAge 86400 Auth data TTL in seconds (replay attack prevention)
mapTelegramDataToUser Custom user data mapper
miniApp.enabled false Enable Mini Apps endpoints
miniApp.validateInitData true Verify Mini App initData
miniApp.allowAutoSignin true Allow auto sign-in from Mini Apps
miniApp.mapMiniAppDataToUser Custom Mini App user mapper

Full types in src/types.ts.

Endpoints

Method Path Auth Description
POST /telegram/signin No Sign in with widget data
POST /telegram/link Session Link Telegram to account
POST /telegram/unlink Session Unlink Telegram
GET /telegram/config No Get bot username
POST /telegram/miniapp/signin No Sign in from Mini App
POST /telegram/miniapp/validate No Validate initData

All endpoints are rate-limited. Signin/miniapp: 10 req/60s. Link/unlink: 5 req/60s. Validate: 20 req/60s. Brute-forcing was never a strategy, now it's also a throttled one.

Error Handling

All endpoints throw APIError from better-auth/api. The plugin exposes $ERROR_CODES so you can match errors client-side like a civilised person:

import { telegram } from "better-auth-telegram";

const plugin = telegram({ botToken: "...", botUsername: "..." });

// In your error handler:
if (error.message === plugin.$ERROR_CODES.NOT_AUTHENTICATED) {
  // handle it
}

No more comparing against magic strings. You're welcome.

Security

HMAC-SHA-256 verification on all auth data via Web Crypto API (crypto.subtle). Timestamp validation against replay attacks. Bot token never touches the client. Works in every runtime that implements the Web Crypto standard — which is all of them now, congratulations internet.

Login Widget uses SHA256(botToken) as secret key. Mini Apps use HMAC-SHA256("WebAppData", botToken). Different derivation paths, same level of paranoia.

Is it bulletproof? No. Is it better than storing passwords in plain text? Significantly.

Troubleshooting

Widget not showing? Did you /setdomain with @BotFather? Is botUsername correct (no @)? Does the container exist in DOM? Are you on HTTPS?

Auth fails? Wrong bot token, domain mismatch with BotFather, or auth_date expired (24h default). Check browser console.

Local dev? ngrok http 3000, use the ngrok URL in BotFather's /setdomain and as your app URL. Yes, it's annoying. Welcome to OAuth.

Examples

See examples/ for a Next.js implementation.

Migrating to v0.4.0

Breaking changes — read before upgrading:

  • Verification functions are now asyncverifyTelegramAuth() and verifyMiniAppInitData() return Promise<boolean>. Slap an await in front if you're calling them directly.
  • Errors throw APIError — all endpoints throw APIError instead of returning ctx.json({ error }). Switch to Better Auth's standard error shape.
  • ESM-first"type": "module" in package.json. CJS still works via .cjs exports.
  • Peer dep bumped — requires better-auth@^1.4.18.

Full changelog in CHANGELOG.md.

Links

License

MIT — do whatever you want. I'm not your lawyer.

Created by Vibe Code.

About

Complete Telegram authentication plugin for Better Auth

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors 3

  •  
  •  
  •