This page is machine-translated from the Russian original. If something reads oddly, the Russian version is the source of truth — open an issue.
To store tokens and other sensitive data securely, you can use two approaches:
.env (recommended)bot.setAppConfig({ env: './.env' });
An example .env file:
# Platform tokens
TELEGRAM_TOKEN=123456:ABC-DEF...
VK_TOKEN=vk1.a.abc123...
VK_CONFIRMATION_TOKEN=abcdef # required for VK (to confirm the webhook)
VK_SECRET_KEY=abc123... # optional: the VK secret key for verifying requests
VIBER_TOKEN=1234567890-ABCDEF...
ALISA_TOKEN=y0_AgAAAAA... # the skill's OAuth token (for media uploads), without the "OAuth " prefix
MARUSIA_TOKEN=abc.123...
MAX_TOKEN=abc123...
SMARTAPP_TOKEN=... # Sber SmartApp token (generated by the CLI; not required for the adapter to work)
# Webhook secrets (see "Webhook signature verification" below). They go to
# tokens.telegram.webhookSecret / tokens.max_app.webhookSecret and enable signature verification.
# They are created and registered by `npx umbot webhook <telegram|max> <https-url>`.
# Do not leave placeholders here: with any non-empty value the bot rejects requests with a different secret.
TELEGRAM_WEBHOOK_SECRET=...
MAX_WEBHOOK_SECRET=...
# Yandex SpeechKit — for TTS in chatbots (Telegram/VK/Max).
# The value is automatically written to speech_kit_token of all three platforms.
# A service account API key is recommended (sent as `Api-Key`); an IAM token `t1.…`
# is also accepted (sent as `Bearer`), but it lives no longer than 12 hours.
SPEECH_KIT_TOKEN=AQVN...
# MongoDB connection (if you use MongoAdapter)
DB_HOST=mongodb://localhost:27017
DB_USER=root
DB_PASSWORD=secret
DB_NAME=umbot
Important! Never add .env files to a Git repository. Use different tokens for development and production. Set
ALISA_TOKENwithout theOAuthprefix — the framework adds it itself. The deprecatedYANDEX_TOKENname is supported for backward compatibility, butALISA_TOKENtakes precedence.
What happens if the file is not found? The framework tries to get the tokens from process.env. If they are not there either, an error message is written to the log file. The adapter stays registered, but operations that need a token (sending messages, uploading media) will not work.
Environment variables without
env. Ifenvis not configured at all, the framework still silently tries to read the known variables (TELEGRAM_TOKEN,VK_TOKEN, ...) fromprocess.envand fill in the tokens with them — this lets you pass tokens viadocker run -eor a serverless function environment withoutenv: 'local'. Tokens that are already set are not overwritten.
.envIt is convenient to keep the keys of your own integrations (a weather API, a CRM) in the same .env. The framework reads only its own
variables from it, and you can read yours with the same parser — the loadEnvFile function from umbot/utils: the same rules for
comments (" #" outside quotes), quotes and empty values as the framework uses.
import { loadEnvFile } from 'umbot/utils';
const envFile = loadEnvFile('./.env').data ?? {};
const weatherKey = process.env['WEATHER_KEY'] || envFile['WEATHER_KEY'] || '';
Projects generated by umbot create from-flow get an env('NAME') helper for this in src/utils.ts (see json-format,
"HTTP requests: variables and secrets").
bot.setAppConfig({
db: {
host: 'mongodb://localhost:27017',
user: 'bot_user',
pass: 'secure_password',
database: 'bot_database',
},
tokens: {
telegram: {
token: 'your-telegram-token',
},
vk: {
token: 'your-vk-token',
},
},
});
The mechanics are as follows: the adapter constructor token (new TelegramAdapter('token')) is written to the config when
bot.use() is called (in the adapter's init()). After that, env decides:
env (a file or 'local') — setAppConfig({ env }) and every subsequent
setPlatformParams overwrite the tokens from env: values from .env/process.env overwrite
both the constructor token and the inline tokens (no matter whether setAppConfig is called before or after bot.use()).env is not configured at all: then setPlatformParams
only fills in missing tokens from process.env without overwriting the ones that are set.tokens object in setAppConfig — is merged with the platform's existing tokens.process.env without a configured env — only fills in missing tokens, overwriting nothing.A practical tip: do not mix approaches for one platform. Either pass the token in the adapter constructor and do not configure
env, or use.env/process.envand create adapters without a token.
| Scenario | Recommendation |
|---|---|
| Development, prototype | env: '.env' — simple and safe |
| Production on a server | env: 'local' + environment variables on the server |
| Tests | Pass directly in config.tokens or in the adapter constructor |
| Several environments (dev/prod) | .env files with different tokens, passed via env |
IAppConfigsetAppConfig takes a Partial<IAppConfig> object:
| Field | Type | Description |
|---|---|---|
error_log |
string |
Path to the error log folder (error.log, warn.log). A file larger than 10 MB is renamed to <name>.1 (one previous copy is kept), so logs take no more than ~40 MB |
json |
string |
Path to the JSON data folder (used by FileAdapter) |
db |
IAppDB |
Database connection parameters |
isLocalStorage |
boolean |
Use the platform's local storage instead of a database |
memorySession |
IMemorySessionConfig | false |
An in-process userData session for Telegram/VK/MAX/Viber without a database when isLocalStorage: true. Defaults to { maxSize: 10000, ttl: 86400000 }; false disables it |
env |
string |
Path to the .env file OR the string 'local' for process.env |
tokens |
ITokenPlatform |
Platform tokens (for adapters, if not passed via the constructor) |
Nested types:
interface IAppDB {
host: string; // for example, 'mongodb://localhost:27017'
user?: string;
pass?: string; // Note: the field is called pass, not password
database: string;
options?: Record<string, unknown>;
}
interface ITokenPlatform {
[platform: string]: {
token?: string;
// speech_kit_token — for TTS in Telegram/VK/Max
// (passed through the index signature below, not declared explicitly in the interface)
[key: string]: string | number | undefined;
};
}
Important.
speech_kit_tokenis not declared explicitly inITokenPlatform— it is passed through the index signature. At the TypeScript level this works:appConfig.tokens.telegram.speech_kit_tokenhas the typestring | number | undefined.
bot.setAppConfig({
json: './data', // folder for JSON files (FileAdapter)
error_log: './errors', // folder for logs
isLocalStorage: true, // local storage (for voice platforms)
env: '.env', // path to the file with tokens
db: {
// MongoDB connection (if not isLocalStorage)
host: 'mongodb://localhost:27017',
database: 'umbot',
},
});
Telegram/VK/MAX/Viber have no local storage: with
isLocalStorage: trueand no DB adapter,userDatais kept in process memory (memorySession). The data is lost on restart and is not shared between processes, replicas and serverless function calls — for production with dialog steps, connect a database.
IAppParamsetPlatformParams takes an IAppParam object:
| Field | Type | Description |
|---|---|---|
intents |
IAppIntent[] | null |
Required. The list of intents for command recognition |
welcome_text |
string | string[] |
The greeting text (when messageId === 0) |
help_text |
string | string[] |
The help text (for the "help" command) |
empty_text |
string | string[] |
The text when no command matches |
isAuthUser |
boolean |
Whether user authorization is required |
utm_text |
string | null |
UTM tags for links (with null, utm_source=umbot&utm_medium=cpc&utm_campaign=phone is automatically added to link buttons without UTM; a string replaces these tags entirely) |
Intent:
interface IAppIntent {
name: string;
slots: (string | RegExp)[]; // string → substring; RegExp → .test()
is_pattern?: boolean; // treat strings as regex (false by default)
}
bot.setPlatformParams({
welcome_text: 'Hi! I can count.',
help_text: 'This is a math game.',
empty_text: 'I didn\'t get that. Say "help".',
intents: [
{ name: 'game', slots: ['game', 'start game'] },
{ name: 'bye', slots: ['bye', 'goodbye'] },
{ name: 'phone', slots: ['\\+?\\d{11}'], is_pattern: true }, // a slot as regex
],
});
Important: the
intentsfield is required even if it is empty:intents: []. Without it TypeScript reports a type error.
const ctx = bot.getAppContext();
ctx.appConfig; // the filled IAppConfig (with all defaults)
ctx.platformParams; // IAppParam
ctx.platforms; // platform registry { alisa: AlisaAdapter, telegram: ... }
ctx.database.adapter; // the active DB adapter
ctx.command; // CommandReg (command registry)
ctx.httpClient; // the fetch function (can be overridden)
ctx.log('...'); // log
ctx.logError('msg', { error: 'details' });
ctx.logWarn('msg', { warning: 'details' });
ctx.logMetric('name', value, { platform: 'telegram' });
setAppMode| Mode | Logs | ReDoS check | When to use |
|---|---|---|---|
dev |
Detailed | Warns but does not block | Development, BotTest |
prod |
Minimal | Warns but does not block (the mode is unsafe, kept for backward compatibility) | Pre-prod |
strict_prod |
Minimal | Blocks dangerous ones | Production |
The default mode. Until setAppMode() is called, the mode is determined by the NODE_ENV environment variable:
NODE_ENV=production — strict_prod, otherwise — dev (Express and frontend bundlers rely on NODE_ENV
the same way). An explicit setAppMode() always takes precedence. The Docker image generated by the CLI sets NODE_ENV=production.
What strict_prod does:
dev and prod modes dangerous RegExps are used as is: with a warning if re2 is installed, and with an error in the logs if not (without re2 the built-in Node engine is vulnerable to catastrophic backtracking).Secret masking in logs (tokens and passwords are replaced with
***) works in all modes and is disabled only by a custom logger withmaskSecrets: false.
The webhook is the only entry point of your application. Until signature verification is enabled, anyone who learns the webhook
URL can send requests on behalf of any user: bypass authorization by userId, read and
overwrite other users' userData, control other users' dialog steps and spend API quotas. The webhook URL is not a secret
(it is visible to the platform, logs, domain registries), so the signature is not an option but a mandatory configuration step.
Signature verification is enabled automatically as soon as a secret is set — there is nothing else to "turn on". The framework warns in the log at startup if no secret is set for a connected platform.
| Platform | What to set | Where the secret lives on the platform side |
|---|---|---|
| Telegram | TELEGRAM_WEBHOOK_SECRET / tokens.telegram.webhookSecret |
the secret_token field when calling setWebhook |
| VK | tokens.vk.secret_key (or vk_secret_key) |
the "Secret key" setting in the group settings (VK Callback API) |
| MAX | MAX_WEBHOOK_SECRET / tokens.max_app.webhookSecret (or secret) |
the secret field in POST /subscriptions |
| Viber | tokens.viber.token |
the bot token — it is also the HMAC key (x-viber-content-signature) |
| Alice, SmartApp, Marusia | — | the platform does not sign requests at all (see below) |
For Telegram and MAX the easiest way is one command in the project folder: it takes the token from .env, generates a secret,
registers the webhook with it right away and saves the secret to .env (TELEGRAM_WEBHOOK_SECRET / MAX_WEBHOOK_SECRET).
The secret is written only after a successful registration, so the values in .env and on the platform do not diverge.
If .env already has a secret, it is used.
npx umbot webhook telegram https://your-domain/webhook
npx umbot webhook max https://your-domain/webhook # MAX: HTTPS on port 443 only
After the command, restart the bot — signature verification turns on automatically.
You can generate a secret manually with any command:
node -e "console.log(require('crypto').randomBytes(24).toString('base64url'))"
# or: openssl rand -base64 24
# 1. The secret is the same string in both places
curl "https://api.telegram.org/bot<TOKEN>/setWebhook" \
-d "url=https://your-domain/webhook" \
-d "secret_token=<SECRET>"
// 2. The same secret in the application configuration: TELEGRAM_WEBHOOK_SECRET in .env/the environment
// is picked up automatically, or explicitly:
bot.use(new TelegramAdapter('YOUR_BOT_TOKEN'));
bot.setAppConfig({
tokens: {
telegram: { token: 'YOUR_BOT_TOKEN', webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET },
},
});
Without webhookSecret, the adapter accepts any request with an update_id field — this is acceptable only for local
debugging. With a secret set, requests without the x-telegram-bot-api-secret-token header (or with a wrong value)
are rejected with 401 before any logic runs.
Enable the "Secret key" in the group settings (Manage → API usage → Callback API) and pass the same value:
bot.use(new VkAdapter('YOUR_VK_TOKEN', { vk_secret_key: 'YOUR_SECRET' }));
// or via the config: tokens.vk.secret_key
VK sends secret in the body of every callback request; the adapter compares it in constant time
(timingSafeEqual). If the secret is not enabled in the group, verification cannot be enabled (there is nothing to compare against), see
ipFilter below.
MAX passes the secret in the x-max-bot-api-secret header:
bot.use(new MaxAdapter('YOUR_MAX_TOKEN', { secret: 'YOUR_SECRET' }));
// or via the config: tokens.max_app.webhookSecret
Alice, SmartApp and Marusia provide no webhook signature mechanism — the entire payload, including
user_id, is controlled by the sender of the request. This is a platform limitation, not a framework one:
userId as an authenticated identity;userData whose loss or substitution is critical.An additional layer for any platform is ipFilter (see middleware.md): restricting incoming
requests to the platforms' IP ranges (for example, for Telegram only: 149.154.160.0/20, 91.108.4.0/22).
The framework supports re2. Using this library significantly speeds up
regular expression processing and reduces memory usage. Memory usage drops
roughly 3-7 times, and execution time drops 2-15 times on average.
npm install re2
The framework automatically detects whether re2 is installed and uses it.
Using the file database in a release version of the application is not recommended, since it can lead to the application crashing with a large number of records. This is because the file database keeps the data in RAM.
To save data to a database correctly:
MongoAdapter), or create your own (bot.use(new MyAdapter()))bot.setAppConfig({db:{...}}), or in the constructor when connecting the adapter..env file exists at the specified path= — the parser trims them (TELEGRAM_TOKEN = abc works the same as TELEGRAM_TOKEN=abc; quotes around the value are allowed — the parser removes them)dev mode for detailed logs: bot.setAppMode('dev')db.host format: it must include the protocol (mongodb://localhost:27017, not localhost:27017)error_log shows the connection errorintents is passed to setPlatformParams (even if empty: intents: [])userCommand)dev mode to see the command lookup processMore about configuration (IAppConfig, IAppParam), token priority and the contents of .env — in the Configuration: IAppConfig and IAppParam section.
Full reference — API v-3.1 · all versions.