Skip to content

Trystpilot - Telegram Authentication Integration Guide

Trystpilot — Telegram Login & Bot Integration

Section titled “Trystpilot — Telegram Login & Bot Integration”

Version: 1.0.0 · Last updated: 2026-03-02 Status: 🚀 Ready for implementation Recommended approach: Telegram Login Widget + optional bot


This guide covers implementing Telegram authentication across three integration points:

  1. Telegram Login Widget — User & admin login (primary auth)
  2. Telegram Bot — Notifications, commands, optional interactions
  3. Database integration — Store Telegram ID linked to users/profiles

Why Telegram?

  • ✅ User-friendly (everyone uses it)
  • ✅ Secure OAuth-based authentication
  • ✅ No need to store passwords
  • ✅ Bot can handle moderation notifications
  • ✅ Optional: Can collect minimal user data (no PII exposure)

  1. Open Telegram, find @BotFather
  2. Send /newbot command
  3. Follow prompts to create a new bot:
    • Name: Trystpilot Bot (user-visible)
    • Username: trystpilot_bot (must end in _bot)
  4. Save the API token → Will look like: 123456789:ABCDefGHIjklmnOPQRstUVwxyz
  1. Send /mybotsTrystpilot BotSettings
  2. Select Manage inline mode
  3. Go back, select Domain
  4. Add your domain: trystpilot.xyz
  5. Important: Telegram will add your domain to a whitelist
<!-- In your Next.js app (server-side or client component) -->
<script async src="https://telegram.org/js/telegram-widget.js?15"></script>
<telegram-login
data-telegram-login="trystpilot_bot"
data-size="large"
data-auth-url="https://trystpilot.xyz/api/auth/telegram/callback"
data-request-access="write"
></telegram-login>

Parameters:

  • data-telegram-login — Bot username (without @)
  • data-size — Button size: small, medium, large
  • data-auth-url — Your callback endpoint (CRITICAL: must match domain registered with BotFather)
  • data-request-accesswrite for notifications, read for read-only

Add Telegram fields to your existing tables:

-- For admin users
ALTER TABLE admin_users ADD COLUMN (
telegram_id BIGINT UNIQUE,
telegram_username VARCHAR(255),
telegram_first_name VARCHAR(255),
telegram_verified_at TIMESTAMP,
telegram_hash VARCHAR(255) -- For verification
);
-- For regular user profiles (optional)
ALTER TABLE profiles ADD COLUMN (
telegram_id BIGINT UNIQUE,
telegram_username VARCHAR(255),
telegram_verified_at TIMESTAMP
);
-- New table: Telegram sessions
CREATE TABLE telegram_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
telegram_id BIGINT NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
admin_id UUID REFERENCES admin_users(id) ON DELETE CASCADE,
session_token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

File: app/api/auth/telegram/callback/route.ts

import { createHash } from 'crypto';
import { db } from '@/lib/db/client';
import { cookies } from 'next/headers';
// Validate Telegram login data using HMAC-SHA256
function verifyTelegramAuth(data: Record<string, any>, botToken: string): boolean {
const { hash, ...authData } = data;
// Create data check string (must be alphabetically sorted)
const dataCheckString = Object.entries(authData)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, val]) => `${key}=${val}`)
.join('\n');
// Compute HMAC-SHA256
const secretKey = createHash('sha256').update(botToken).digest();
const computedHash = createHash('sha256')
.update(dataCheckString)
.update(secretKey)
.digest('hex');
return computedHash === hash;
}
export async function POST(request: Request) {
try {
const data = await request.json();
const botToken = process.env.TELEGRAM_BOT_TOKEN!;
// Step 1: Verify the login data
if (!verifyTelegramAuth(data, botToken)) {
return Response.json(
{ error: 'Invalid Telegram auth signature' },
{ status: 401 }
);
}
// Step 2: Check if login is not too old (prevent replay attacks)
const authDate = new Date(data.auth_date * 1000);
const now = new Date();
if (now.getTime() - authDate.getTime() > 60000) { // 60 seconds
return Response.json(
{ error: 'Login data is too old (replay attack detected)' },
{ status: 401 }
);
}
const telegramId = data.id;
const telegramUsername = data.username;
const firstName = data.first_name;
// Step 3: Check if user is requesting admin access or regular user
const isAdminAttempt = request.headers.get('x-admin-attempt') === 'true';
if (isAdminAttempt) {
// ADMIN LOGIN FLOW
// Step 3a: Find or create admin user
let admin = await db.query(
'SELECT * FROM admin_users WHERE telegram_id = $1',
[telegramId]
);
if (!admin.rows.length) {
// Check if admin whitelist is enabled (recommended)
const whitelist = process.env.TELEGRAM_ADMIN_WHITELIST?.split(',') || [];
if (whitelist.length > 0 && !whitelist.includes(String(telegramId))) {
return Response.json(
{ error: 'Telegram ID not in admin whitelist' },
{ status: 403 }
);
}
// Create new admin (with auto-approve if whitelist passed)
admin = await db.query(
`INSERT INTO admin_users (telegram_id, telegram_username, telegram_first_name, telegram_verified_at)
VALUES ($1, $2, $3, NOW())
RETURNING *`,
[telegramId, telegramUsername, firstName]
);
}
// Step 4: Create session
const sessionToken = require('crypto').randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await db.query(
`INSERT INTO telegram_sessions (telegram_id, admin_id, session_token, expires_at)
VALUES ($1, $2, $3, $4)`,
[telegramId, admin.rows[0].id, sessionToken, expiresAt]
);
// Step 5: Set session cookie
const cookieStore = await cookies();
cookieStore.set('telegram_session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 7 * 24 * 60 * 60 // 7 days in seconds
});
return Response.json({
success: true,
redirectTo: '/moderation',
message: 'Admin login successful'
});
} else {
// REGULAR USER LOGIN FLOW
// Step 3b: Find or create user profile
let profile = await db.query(
'SELECT * FROM profiles WHERE telegram_id = $1',
[telegramId]
);
if (!profile.rows.length) {
// Auto-create profile linked to Telegram
profile = await db.query(
`INSERT INTO profiles (alias_name, telegram_id, telegram_username, telegram_verified_at, moderation_status)
VALUES ($1, $2, $3, NOW(), 'pending')
RETURNING *`,
[`User_${telegramId}`, telegramId, telegramUsername]
);
}
// Step 4: Create session
const sessionToken = require('crypto').randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
await db.query(
`INSERT INTO telegram_sessions (telegram_id, user_id, session_token, expires_at)
VALUES ($1, $2, $3, $4)`,
[telegramId, profile.rows[0].id, sessionToken, expiresAt]
);
// Step 5: Set session cookie
const cookieStore = await cookies();
cookieStore.set('telegram_user_session', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 30 * 24 * 60 * 60 // 30 days in seconds
});
return Response.json({
success: true,
redirectTo: `/profile/${profile.rows[0].alias_name}`,
message: 'User login successful'
});
}
} catch (error) {
console.error('Telegram auth error:', error);
return Response.json(
{ error: 'Authentication failed' },
{ status: 500 }
);
}
}

File: lib/auth/verify-telegram-session.ts

import { db } from '@/lib/db/client';
export async function verifyTelegramSession(
sessionToken: string,
type: 'admin' | 'user' = 'user'
): Promise<{ valid: boolean; telegramId?: number; userId?: string; adminId?: string }> {
try {
const result = await db.query(
`SELECT * FROM telegram_sessions
WHERE session_token = $1 AND expires_at > NOW()`,
[sessionToken]
);
if (!result.rows.length) {
return { valid: false };
}
const session = result.rows[0];
if (type === 'admin' && !session.admin_id) {
return { valid: false };
}
if (type === 'user' && !session.user_id) {
return { valid: false };
}
return {
valid: true,
telegramId: session.telegram_id,
userId: session.user_id,
adminId: session.admin_id
};
} catch (error) {
console.error('Session verification error:', error);
return { valid: false };
}
}

File: app/moderation/middleware.ts (or use route-level middleware)

import { NextRequest, NextResponse } from 'next/server';
import { verifyTelegramSession } from '@/lib/auth/verify-telegram-session';
export async function middleware(request: NextRequest) {
const sessionToken = request.cookies.get('telegram_session')?.value;
if (!sessionToken) {
return NextResponse.redirect(new URL('/login/telegram', request.url));
}
const session = await verifyTelegramSession(sessionToken, 'admin');
if (!session.valid) {
return NextResponse.redirect(new URL('/login/telegram', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/moderation/:path*']
};

A. Telegram Login Button (Client Component)

Section titled “A. Telegram Login Button (Client Component)”

File: app/components/TelegramLoginButton.tsx

'use client';
import Script from 'next/script';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
interface TelegramLoginButtonProps {
callbackUrl?: string;
isAdmin?: boolean;
size?: 'small' | 'medium' | 'large';
}
export function TelegramLoginButton({
callbackUrl = '/profile',
isAdmin = false,
size = 'large'
}: TelegramLoginButtonProps) {
const router = useRouter();
useEffect(() => {
// Handle Telegram login widget callback
if (typeof window !== 'undefined') {
(window as any).onTelegramAuth = async (user: any) => {
try {
const response = await fetch('/api/auth/telegram/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-admin-attempt': String(isAdmin)
},
body: JSON.stringify(user)
});
const data = await response.json();
if (data.success) {
router.push(data.redirectTo || callbackUrl);
} else {
alert(data.error || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
alert('An error occurred during login');
}
};
}
}, [router, callbackUrl, isAdmin]);
return (
<>
<Script
src="https://telegram.org/js/telegram-widget.js?15"
strategy="beforeInteractive"
/>
<div className="flex justify-center my-4">
<telegram-login
data-telegram-login={process.env.NEXT_PUBLIC_TELEGRAM_BOT_USERNAME!}
data-size={size}
data-onauth="onTelegramAuth"
data-request-access="write"
/>
</div>
</>
);
}

File: app/login/telegram/page.tsx

import { TelegramLoginButton } from '@/app/components/TelegramLoginButton';
export default function TelegramLoginPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="mx-auto w-full max-w-md rounded-lg bg-white p-8 shadow">
<h1 className="mb-2 text-2xl font-bold">Login with Telegram</h1>
<p className="mb-6 text-gray-600">
Use your Telegram account to log in securely.
</p>
<TelegramLoginButton
callbackUrl="/profile"
isAdmin={false}
size="large"
/>
<p className="mt-4 text-center text-sm text-gray-500">
We respect your privacy. We only store your Telegram ID, not your phone number or other data.
</p>
<hr className="my-6" />
<h2 className="mb-4 text-lg font-semibold">Admin Access</h2>
<TelegramLoginButton
callbackUrl="/moderation"
isAdmin={true}
size="medium"
/>
</div>
</div>
);
}

(Already done via BotFather above — save your token)

Terminal window
# Configure webhook (one-time setup)
curl -X POST https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook \
-d "url=https://trystpilot.xyz/api/webhooks/telegram"
Terminal window
# Send test message to your bot
# Find @trystpilot_bot in Telegram, send any message
# Check logs for webhook delivery

File: app/api/webhooks/telegram/route.ts

import { db } from '@/lib/db/client';
interface TelegramUpdate {
update_id: number;
message?: {
message_id: number;
from: { id: number; username: string };
chat: { id: number };
text: string;
};
}
export async function POST(request: Request) {
try {
const update: TelegramUpdate = await request.json();
if (!update.message) {
return Response.json({ ok: true }); // Ignore non-message updates
}
const { from, chat, text } = update.message;
const telegramId = from.id;
// Command handling
if (text === '/start') {
await sendTelegramMessage(
chat.id,
`👋 Welcome to Trystpilot!\n\n` +
`Log in at: https://trystpilot.xyz/login/telegram\n\n` +
`Commands:\n` +
`/status - Check your profile status\n` +
`/help - Get help\n` +
`/logout - Log out`
);
} else if (text === '/status') {
// Check user profile
const profile = await db.query(
'SELECT alias_name, moderation_status, review_count FROM profiles WHERE telegram_id = $1',
[telegramId]
);
if (!profile.rows.length) {
await sendTelegramMessage(
chat.id,
'No profile found. Log in at https://trystpilot.xyz/login/telegram'
);
} else {
const p = profile.rows[0];
await sendTelegramMessage(
chat.id,
`📊 Profile Status\n\n` +
`Alias: ${p.alias_name}\n` +
`Status: ${p.moderation_status}\n` +
`Reviews: ${p.review_count}`
);
}
} else if (text === '/help') {
await sendTelegramMessage(
chat.id,
`ℹ️ Help\n\n` +
`Trystpilot is an anonymous review platform.\n` +
`Use our website at https://trystpilot.xyz\n\n` +
`Questions? Contact us at support@trystpilot.xyz`
);
} else {
// Default response
await sendTelegramMessage(
chat.id,
`I don't understand that command.\n\n/help for available commands`
);
}
return Response.json({ ok: true });
} catch (error) {
console.error('Telegram webhook error:', error);
return Response.json({ ok: false });
}
}
// Helper function to send messages via Telegram Bot API
async function sendTelegramMessage(chatId: number, text: string) {
const response = await fetch(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: text,
parse_mode: 'Markdown'
})
}
);
if (!response.ok) {
throw new Error(`Telegram API error: ${response.statusText}`);
}
}

File: lib/telegram/send-notification.ts

export async function notifyAdminViaTelegram(
adminTelegramId: number,
message: string
) {
const response = await fetch(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: adminTelegramId,
text: `⚠️ Moderation Alert\n\n${message}`,
parse_mode: 'Markdown'
})
}
);
if (!response.ok) {
throw new Error(`Failed to send notification`);
}
}
// Usage in moderation dashboard:
// await notifyAdminViaTelegram(admin.telegram_id,
// `New review flagged for profile "${profile.alias_name}"`)

Add to .env.example and Vercel Project Settings

Section titled “Add to .env.example and Vercel Project Settings”
Terminal window
# ─── Telegram Authentication ───────────────────────────────────────────
# Bot token from BotFather (/newbot command)
TELEGRAM_BOT_TOKEN=<your-bot-token>
# Bot username (for widget, must end in _bot)
NEXT_PUBLIC_TELEGRAM_BOT_USERNAME=trystpilot_bot
# Admin whitelist (comma-separated Telegram IDs; empty = no whitelist)
TELEGRAM_ADMIN_WHITELIST=123456789,987654321
# Optional: Encryption key for session tokens (generate: openssl rand -hex 32)
TELEGRAM_SESSION_SECRET=<random-hex-string>

Create migration file: db/migrations/006_telegram_auth.sql

-- Telegram authentication tables
-- Add Telegram columns to admin_users
ALTER TABLE admin_users ADD COLUMN IF NOT EXISTS (
telegram_id BIGINT UNIQUE,
telegram_username VARCHAR(255),
telegram_first_name VARCHAR(255),
telegram_verified_at TIMESTAMP,
telegram_hash VARCHAR(255)
);
-- Add Telegram columns to profiles
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS (
telegram_id BIGINT UNIQUE,
telegram_username VARCHAR(255),
telegram_verified_at TIMESTAMP
);
-- Telegram session table
CREATE TABLE IF NOT EXISTS telegram_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
telegram_id BIGINT NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
admin_id UUID REFERENCES admin_users(id) ON DELETE CASCADE,
session_token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT at_least_one_user CHECK (
(user_id IS NOT NULL AND admin_id IS NULL) OR
(user_id IS NULL AND admin_id IS NOT NULL)
)
);
CREATE INDEX idx_telegram_sessions_token ON telegram_sessions(session_token);
CREATE INDEX idx_telegram_sessions_expires ON telegram_sessions(expires_at);
CREATE INDEX idx_telegram_sessions_user_id ON telegram_sessions(user_id);
CREATE INDEX idx_telegram_sessions_admin_id ON telegram_sessions(admin_id);

  • Verify HMAC signatures — Always validate login data using crypto.createHash()
  • Check auth timestamp — Reject logins older than 60 seconds (prevents replay attacks)
  • Use httpOnly cookies — Session tokens never exposed to JavaScript
  • Rotate session tokens — Generate new tokens on login
  • Admin whitelist — Restrict admin access to specific Telegram IDs
  • Log Telegram logins — Audit trail for security
  • Webhook signature verification — Verify requests from Telegram Bot API
  • Store bot token in code — Use environment variables only
  • Skip HMAC verification — Attackers can forge login data
  • Trust client-side data — Always verify signatures server-side
  • Log sensitive data — Never log auth tokens or secrets
  • Use HTTP for callbacks — Must be HTTPS
  • Expose Telegram IDs — Treat as sensitive (not displayed to other users)

Terminal window
# Install ngrok: https://ngrok.com/download
# Start ngrok tunnel
ngrok http 3000
# Output: https://abc123.ngrok.io
# Update bot webhook
curl -X POST https://api.telegram.org/bot<TOKEN>/setWebhook \
-d "url=https://abc123.ngrok.io/api/webhooks/telegram"
# Find your bot in Telegram, send a message
# Check terminal for webhook delivery
  1. Create test user in Telegram
  2. Click login widget on http://localhost:3000/login/telegram
  3. Authenticate with test account
  4. Check database for new session

  • BotFather bot created and token saved
  • Domain added to BotFather settings
  • Telegram Login Widget verified (test button works)
  • Database migrations applied
  • .env.example updated with TELEGRAM_* variables
  • Vercel Project Settings has all TELEGRAM_* secrets configured
  • Webhook endpoint deployed and accessible
  • Admin whitelist configured (if applicable)
  • Test login flow (widget → callback → redirect)
  • Test bot commands (/start, /status, /help)
  • Test admin panel access via Telegram
  • Logging configured for audit trail
  • Terms of Service updated (mention Telegram auth)
  • Privacy Policy updated (mention Telegram ID storage)

IssueCauseSolution
Widget not showingWrong bot username or domain not whitelistedVerify NEXT_PUBLIC_TELEGRAM_BOT_USERNAME and ask BotFather to add domain again
Callback fails with 401HMAC signature verification failedCheck TELEGRAM_BOT_TOKEN is correct; verify data is being passed correctly
Callback timeoutDatabase query is slowAdd indexes on telegram_id columns; check connection pooling
Webhook not deliveringURL not HTTPS or domain mismatchUse https:// only; check setWebhook response for errors
Session expires immediatelyexpires_at calculation wrongCheck server clock is correct; verify timestamp logic
Admin login shows 403Telegram ID not in whitelistAdd your Telegram ID to TELEGRAM_ADMIN_WHITELIST in Vercel

What data is stored:

  • telegram_id — Unique identifier (not reverse-identifiable)
  • telegram_username — Public username (user-provided)
  • first_name — First name only (not last name)

What is NOT stored:

  • ❌ Phone number
  • ❌ Email address
  • ❌ Location data
  • ❌ Chat history
  • ❌ Profile picture

Privacy statement for users:

We only collect your Telegram ID and first name for authentication. We do not have access to your phone number, email, or any other personal data. You can revoke our bot’s access anytime by blocking it in Telegram.


10. Optional: Telegram Passport (Advanced)

Section titled “10. Optional: Telegram Passport (Advanced)”

For identity verification in Phase 2+, Telegram Passport can collect verified documents:

// Example: Request passport data
const requestButton = {
text: 'Verify Identity',
request_user: {
request_id: 1,
user_is_bot: false,
user_is_premium: false
}
};
// Available passport types:
// - personal_details (name, DOB, gender, nationality)
// - passport / driver_license / identity_card
// - address_document
// - utility_bill

Not recommended for MVP — Too complex. Use widget login only.


ParameterTypeRequiredExample
data-telegram-loginstringtrystpilot_bot
data-sizestringlarge
data-auth-urlstringhttps://trystpilot.xyz/api/auth/telegram/callback
data-request-accessstringwrite
data-onauthfunctiononTelegramAuth
{
id: 123456789, // Unique Telegram ID
first_name: "John", // First name
username: "john_doe", // Optional username
photo_url: "https://...", // Optional profile photo
auth_date: 1234567890, // Unix timestamp
hash: "abcd1234..." // HMAC-SHA256 signature
}

  • docs/SECURITY.md — Security & compliance
  • docs/DEVOPS.md — Deployment & CI/CD
  • CLAUDE.md — Project status
  • .env.example — Environment variable reference


Status: ✅ Ready for implementation Estimated effort: 2-3 hours for MVP (widget + basic bot) Next phase: Telegram Passport for identity verification (Phase 2+)

Last updated: 2026-03-02