Operator console
run the service without touching code.
The console at /admin is the MIKODES Admin Kit, served by the KESTREL server itself. It is for the person who runs the server, not for app users: it has its own accounts, roles, two-factor sign-in, an append-only audit log and a version history of every setting.
01What the console is
The console runs in the same process and on the same port as the server (server/src/admin/, server/packages/admin-kit/). Its API lives under /admin/api with a cookie session. An app user's token never opens it.
Every value you save applies at once, without a restart, and wins over .env. A value you have not saved yet starts from the matching .env variable, so an existing .env keeps working (server/src/admin/manifest.ts).
The console's own tables are in data/admin.sqlite next to the SQLite database (in Docker: /data/admin.sqlite), or in mk_admin_* tables in your PostgreSQL. Its secrets are encrypted with a key derived from KESTREL_MASTER_KEY, or with ADMIN_SECRET_KEY when you set one.
02First owner (setup code)
- Find the code. On the first start the server log prints a one-time setup code. With Docker:
Each start before the owner exists adds a new code; any printed code works until the owner is created. The code is shown in the log only, and the server stores only its hash.docker compose logs server | grep -A3 "setup code" - Open the console. Go to
https://your-domain/admin. - Create the owner. Enter the code, your email and a password of at least 12 characters. You are the owner.
- Turn on two-factor sign-in for your console account when the console offers it.
03Team and roles
| Role | Can |
|---|---|
| Viewer | Reads everything, changes nothing. Sees user emails masked. |
| Manager | Changes settings, not secrets or the team. Changes a user's plan, disables users, ends their sessions. |
| Owner | Everything, including secrets (Integrations), the team, rollbacks and configuration import. |
Add people under Team. Under Security & alerts an owner can require two-factor sign-in for owners and managers and set the console's alert rules.
04Settings, section by section
The console groups its sections in the sidebar. A Get started page walks through the fields marked for setup.
Brand
Product name, tagline, accent colour, logo, support email, website. Starts from BRAND_NAME, BRAND_ACCENT, BRAND_LOGO_URL. These values are published at GET /api/config for any client that reads them. The compiled KESTREL apps show the name, colours and icon set in their own source; see White label.
Legal
Terms of service URL, privacy policy URL, disclaimer. Starts from LEGAL_TERMS_URL, LEGAL_PRIVACY_URL. Published at GET /api/config.
Access: maintenance and announcement
Maintenance mode with a message pauses every change in the app: the server answers 503 maintenance with your message, which the app shows. Reads keep working, running bots and Smart Trades continue, and everything that only reduces risk stays open: sign-in, panic, the kill switch, closing positions, cancelling orders and algo orders, closing or cancelling Smart Trades, pausing or stopping bots and pausing copy links (docs/API.md, Maintenance). The announcement banner text is published at GET /api/config.
Tenancy
| Mode | Use it when | Effect |
|---|---|---|
| Self-hosted | You run KESTREL for yourself | No plan limits (only the system caps: 100 alerts per user, the server-wide AI rate). Copy links join two accounts of the same user. |
| SaaS | You run it for other people | Plans and limits apply. Users can publish a live account as a copy-trading provider and others can follow it. |
Starts from KESTREL_TENANCY.
Features
One switch each for: trade journal, analytics, CSV export, algo orders, copy trading, scanner, AI assistant, Telegram, push notifications, email notifications, panic button, risk guard. A switched-off feature disappears from the app and its API answers 404 feature_disabled. Each switch starts from KESTREL_FEATURE_<NAME>. Telegram, push and email also need their integration configured to show in the app; the AI assistant needs a provider, model and key.
Plans & limits
Two plans, Free and Pro. For each: the name, the price text the app shows (empty until you write one), and these limits, where 0 means no plan limit:
- Exchange accounts
- Bots (not stopped)
- Active Smart Trades
- Alerts (never above the system cap of 100)
- Copy links (not stopped)
- AI requests per hour (0 = the server-wide rate)
Limits apply only in SaaS mode. Over a limit, the create action answers 403 plan_limit with a message naming the limit, and the app shows it. Every new user starts on Free; you move a user to Pro on the Users page. KESTREL never takes a payment; see Revenue options.
Exchange broker ids
Your broker or partner id per exchange, attached to the orders your users place so the exchange can credit you its broker rebate. Off and empty until you set them. Details and what each exchange receives: Broker ids.
AI assistant
Provider (Off, Anthropic, OpenAI-compatible), base URL (empty = the provider's own), model, requests per user and hour (default 20). The API key is under Integrations. Starts from the KESTREL_AI_* variables. See AI assistant.
Delivery
Email sender (From), required with SMTP, starts from MAIL_FROM. Your Telegram chat id, used only by the Telegram Test button: send /start to your bot, then read your numeric chat id with any Telegram bot that shows it.
Integrations (owner only)
Write-only secrets: once saved they are never shown again. Each has a Test button.
| Field | Where to get it | Test | .env fallback |
|---|---|---|---|
| Telegram bot token | BotFather in Telegram (steps) | Checks the token with Telegram and, when your chat id is set under Delivery, sends you a test message (send /start to the bot from that chat first) | TELEGRAM_BOT_TOKEN |
| LLM API key | Your LLM provider's console | Makes one small live call to the model set under AI assistant (this call is billed by your provider) | KESTREL_AI_API_KEY |
| SMTP URL | Your email provider's SMTP settings, as smtps://user:pass@host:465 or smtp://user:pass@host:587 | Sends a test email to the operator email under Notifications (set the sender under Delivery first) | SMTP_URL |
| Firebase service account (JSON) | Firebase console, Project settings, Service accounts, Generate new private key. Paste the whole file. | Signs in to Google with it | FCM_SERVICE_ACCOUNT_FILE |
Telegram bot: free. Firebase Cloud Messaging: free (Spark plan). SMTP: your provider's free tier, then paid. LLM: paid per use to your provider.
Notifications
Operator email (where the SMTP test and the console's own alerts go), and a webhook URL with a signing secret for the console's alerts.
05Users
Users (Monitor group) lists every app account on the server: plan, status, open sessions, exchange accounts and running bots.
- Change plan: Free or Pro (managers and owners).
- Disable needs a reason, which goes into the audit log. It ends every session and closes the user's live connections at once, refuses new sign-ins (
403 account_disabled) and stops Telegram control from their chat. Running bots and open orders stay as they are: they trade the user's own exchange accounts. Enable reverses it. - End sessions signs the user out of every device without disabling them.
Viewers see the list with masked emails and cannot change anything.
06Overview, Status, Revenue
Overview
Counts from the database: users, new users in the period, disabled users, connected exchange accounts, paper accounts, running bots, active Smart Trades, active copy links, active alerts.
Status
Checks the server can really answer:
| Check | Severity |
|---|---|
| Database answers | Blocker |
| Trading worker is running | Blocker |
| Exchanges reachable (public market data) | Warning |
| Broker ids are valid | Warning |
| Telegram bot (asks Telegram who the bot is) | Info |
| Email (SMTP) configured | Info |
| Push (Firebase service account signs in to Google) | Info |
| AI assistant configured | Info |
An optional integration you have not set is shown as not set, not as broken.
Revenue
Empty by design. KESTREL takes no fee, and broker rebates are paid by the exchanges straight to you, where the server cannot see them, so the console shows no estimate.
07Audit log, History, Export / Import
- Audit log: every console action (sign-ins, setting changes, team changes, user disables with their reason). Append-only.
- History: every saved version of the settings. Compare two versions; owners can roll back. Secrets are never part of a version.
- Export / Import: a JSON file with every setting except secrets. Import shows exactly what would change before anything is applied; only owners import.
The app users' own audit log (keys, kill switch, bots, trades) is separate and shown in the app under Safety, Audit log (Audit log).
08Lost the owner password
There is deliberately no web route for this: access to the server is the proof. On a Docker install with SQLite, run on the server:
cd kestrel
docker compose exec server node --input-type=module -e '
const k = await import("/app/dist/packages/admin-kit/server/index.js");
const s = k.sqliteStorage("/data/admin.sqlite");
console.log(await k.listMembers(s));
const r = await k.resetOwnerPassword(s, "owner@example.com", { resetTwoFactor: false });
console.log("New one-time password:", r.password);'
Use your owner email. The reset sets a new one-time password (printed once), signs that account out everywhere, clears its failed sign-in counter and writes an audit entry. Use { resetTwoFactor: true } if the authenticator is lost too. With PostgreSQL, use the kit's postgresStorage instead of sqliteStorage (server/packages/admin-kit/server/recovery.ts).
09Public demo host
Set ADMIN_DEMO_PASSWORD only on a public demo server: the console becomes read-only with the account demo@kestrel.demo, and the Users page refuses every change. Never set it on a real install.