tweb is a full-featured Telegram web client (https://web.telegram.org/k/) built with Solid.js and TypeScript. It implements Telegram's MTProto protocol directly in the browser (no third-party API wrappers). The codebase is large (~100k+ lines excluding vendor), mature, and highly performance-oriented.
Author: Eduard Kuzmenko. License: GPL v3.
| Layer | Technology |
|---|---|
| UI Framework | Solid.js (custom fork in src/vendor/solid/) |
| Language | TypeScript 5.7 |
| Build | Vite 5 |
| CSS | SCSS (sass) |
| Testing | Vitest |
| Package Manager | pnpm 9 |
| Protocol | MTProto (custom implementation) |
| Storage | IndexedDB + CacheStorage + localStorage |
| Workers | SharedWorker + ServiceWorker |
pnpm install
pnpm start # Dev server on :8080
pnpm build # Production build → dist/
pnpm test # Run tests (Vitest)
pnpm lint # ESLint on src/**/*.tsDebug query params: ?test=1 (test DCs), ?debug=1 (verbose logging), ?noSharedWorker=1 (disable shared worker).
src/
├── components/ # Solid.js UI components (.tsx)
│ ├── chat/ # Chat bubbles, topbar, sidebars
│ ├── popups/ # Modal/popup components
│ ├── mediaEditor/ # Media editing UI
│ └── ... # 200+ feature folders
├── lib/
│ ├── appManagers/ # 55+ domain managers (chats, users, messages, etc.)
│ ├── mtproto/ # MTProto protocol implementation
│ ├── storages/ # IndexedDB/localStorage wrappers
│ ├── rootScope.ts # Global event emitter & app context
│ └── mainWorker/ # Background worker logic
├── stores/ # Solid.js reactive stores (13 stores)
├── helpers/ # 145+ utility functions
├── hooks/ # Solid.js hooks
├── pages/ # Auth pages (login, signup, etc.)
├── config/ # App constants, state schema, emoji, currencies
├── environment/ # Browser feature detection (39 modules)
├── scss/ # Global stylesheets
├── vendor/ # Third-party forks (solid, solid-transition-group)
├── scripts/ # Build & codegen scripts
└── tests/ # Test files
Always use these aliases instead of relative paths:
@components/* → src/components/
@helpers/* → src/helpers/
@hooks/* → src/hooks/
@stores/* → src/stores/
@lib/* → src/lib/
@appManagers/* → src/lib/appManagers/
@environment/* → src/environment/
@config/* → src/config/
@vendor/* → src/vendor/
@layer → src/layer.d.ts (MTProto API types)
@types → src/types.d.ts (utility types)
@/* → src/
// Solid.js resolves to the custom fork:
solid-js → src/vendor/solid
solid-js/web → src/vendor/solid/web
solid-js/store → src/vendor/solid/store- Indent: 2 spaces (no tabs)
- Quotes: single quotes; template literals allowed
- Line endings: Unix (LF); file must end with newline
- No trailing spaces
- Comma dangle: never (
{a: 1, b: 2}not{a: 1, b: 2,}) - Object/array spacing: no spaces inside braces/brackets
{a: 1}not{ a: 1 }[1, 2]not[ 1, 2 ]
- Keyword spacing: no space after
if,for,while,switch,catchif(condition)notif (condition)for(...)notfor (...)
- Function paren: no space before paren —
function foo()notfunction foo () - No
return await: usereturn promisedirectly - Max 2 consecutive blank lines
prefer-constwith destructuring:all
strict: truebutstrictNullChecks: falseandstrictPropertyInitialization: falseuseDefineForClassFields: false— important for class field behaviorjsxImportSource: solid-js— JSX is Solid.js, not React- MTProto types live in
src/layer.d.ts(664KB, auto-generated); import from@layer - Utility types (AuthState, WorkerTask, etc.) live in
src/types.d.ts; import from@types - Global types available everywhere:
PeerId,UserId,ChatId,BotId,DocId,Long,Icon,ApiError,ErrorType,MaybePromise<T>. Defined insrc/global.d.ts.
Components are in .tsx files. Props typed inline. Use classNames() helper for class composition:
import {JSX} from 'solid-js';
import classNames from '@helpers/string/classNames';
export default function MyComponent(props: {
class?: string,
children: JSX.Element
}) {
return (
<div class={classNames('my-class', props.class)}>
{props.children}
</div>
);
}Scoped styles use .module.scss files. Import as styles:
import styles from '@components/chat/bubbles/service.module.scss';
// Usage: <div class={styles.wrap}>Stores in src/stores/ use createRoot + createSignal and export a hook:
import {createRoot, createSignal} from 'solid-js';
import rootScope from '@lib/rootScope';
const [value, setValue] = createRoot(() => createSignal(initialValue));
rootScope.addEventListener('some_event', setValue);
export default function useValue() {
return value;
}Business logic lives in AppManager subclasses in src/lib/appManagers/. They communicate via rootScope events and are accessed via rootScope.managers:
import {AppManager} from '@appManagers/manager';
export class AppSomethingManager extends AppManager {
protected after() {
// Initialization after state loaded
this.apiUpdatesManager.addMultipleEventsListeners({...});
}
}Normally most of the interaction with MTProto should be done through the app managers, wrapping the raw APIs with a nicer interface, caching layer, etc. Managers are the source of truth.
Invoking MTProto methods is done via:
// invoke normally
await this.apiManager.invokeApi('payments.checkCanSendGift', {gift_id: gift.id})
// invoke with deduplication
await this.apiManager.invokeApiSingle('payments.checkCanSendGift', {gift_id: gift.id})
// invoke and do something with the result (only available inside managers)
return this.apiManager.invokeApiSingleProcess({
method: 'some.method',
params: {...},
processResult: (result) => {
// when the result type has {chats, users} fields, use this method to save them
this.appUsersManager.saveApiPeers(result);
// when the result is `Updates`, use this method to handle them
this.apiUpdatesManager.processUpdateMessage(result);
}
});Global event bus and context. Available everywhere:
import rootScope from '@lib/rootScope';
rootScope.addEventListener('premium_toggle', handler);
rootScope.managers.appChatsManager.getChat(chatId);IMPORTANT: rootScope.managers.* are asynchronous proxies to a whared worker. Every manager method returns a Promise, even if the manager's own methods seem synchronous.
All MTProto types come from @layer:
import {Message, Chat, User, InputPeer} from '@layer';- Global styles in
src/scss/ - Component-scoped styles in
.module.scssnext to component files - BEM-like class naming convention
- CSS variables used for theming
| File | Purpose |
|---|---|
src/index.ts |
App entry point, account/auth init |
src/lang.ts |
All i18n strings (232KB) |
src/layer.d.ts |
MTProto API types (auto-generated, 664KB) |
src/types.d.ts |
Utility/app types |
src/global.d.ts |
Global interface augmentations |
src/config/state.ts |
Application state schema |
src/config/app.ts |
App constants |
src/lib/rootScope.ts |
Global event emitter |
vite.config.ts |
Build configuration |
eslint.config.mjs |
ESLint flat config |
- Do not add
eslint-disablewithout a reason - Do not use
return await(rule enforced) - Do not use spaces inside
{}for objects or[]for arrays - Do not use
if (with a space — useif( - Do not import from
reactor use React patterns — this is Solid.js - Do not use relative
../../imports when an alias exists - Do not use
var— useconst/let - Do not add trailing commas in arrays/objects
pnpm test # all tests
pnpm test src/tests/foo # specific test fileVitest config: threads: false, globals: true, jsdom environment, setup in src/tests/setup.ts.
Always prefix commands with rtk. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use.
Important: Even in command chains with &&, use rtk:
# ❌ Wrong
git add . && git commit -m "msg" && git push
# ✅ Correct
rtk git add . && rtk git commit -m "msg" && rtk git pushrtk cargo build # Cargo build output
rtk cargo check # Cargo check output
rtk cargo clippy # Clippy warnings grouped by file (80%)
rtk tsc # TypeScript errors grouped by file/code (83%)
rtk lint # ESLint/Biome violations grouped (84%)
rtk prettier --check # Files needing format only (70%)
rtk next build # Next.js build with route metrics (87%)rtk cargo test # Cargo test failures only (90%)
rtk vitest run # Vitest failures only (99.5%)
rtk playwright test # Playwright failures only (94%)
rtk test <cmd> # Generic test wrapper - failures onlyrtk git status # Compact status
rtk git log # Compact log (works with all git flags)
rtk git diff # Compact diff (80%)
rtk git show # Compact show (80%)
rtk git add # Ultra-compact confirmations (59%)
rtk git commit # Ultra-compact confirmations (59%)
rtk git push # Ultra-compact confirmations
rtk git pull # Ultra-compact confirmations
rtk git branch # Compact branch list
rtk git fetch # Compact fetch
rtk git stash # Compact stash
rtk git worktree # Compact worktreeNote: Git passthrough works for ALL subcommands, even those not explicitly listed.
rtk gh pr view <num> # Compact PR view (87%)
rtk gh pr checks # Compact PR checks (79%)
rtk gh run list # Compact workflow runs (82%)
rtk gh issue list # Compact issue list (80%)
rtk gh api # Compact API responses (26%)rtk pnpm list # Compact dependency tree (70%)
rtk pnpm outdated # Compact outdated packages (80%)
rtk pnpm install # Compact install output (90%)
rtk npm run <script> # Compact npm script output
rtk npx <cmd> # Compact npx command output
rtk prisma # Prisma without ASCII art (88%)rtk ls <path> # Tree format, compact (65%)
rtk read <file> # Code reading with filtering (60%)
rtk grep <pattern> # Search grouped by file (75%)
rtk find <pattern> # Find grouped by directory (70%)rtk err <cmd> # Filter errors only from any command
rtk log <file> # Deduplicated logs with counts
rtk json <file> # JSON structure without values
rtk deps # Dependency overview
rtk env # Environment variables compact
rtk summary <cmd> # Smart summary of command output
rtk diff # Ultra-compact diffsrtk docker ps # Compact container list
rtk docker images # Compact image list
rtk docker logs <c> # Deduplicated logs
rtk kubectl get # Compact resource list
rtk kubectl logs # Deduplicated pod logsrtk curl <url> # Compact HTTP responses (70%)
rtk wget <url> # Compact download output (65%)rtk gain # View token savings statistics
rtk gain --history # View command history with savings
rtk discover # Analyze Claude Code sessions for missed RTK usage
rtk proxy <cmd> # Run command without filtering (for debugging)
rtk init # Add RTK instructions to CLAUDE.md
rtk init --global # Add RTK to ~/.claude/CLAUDE.md| Category | Commands | Typical Savings |
|---|---|---|
| Tests | vitest, playwright, cargo test | 90-99% |
| Build | next, tsc, lint, prettier | 70-87% |
| Git | status, log, diff, add, commit | 59-80% |
| GitHub | gh pr, gh run, gh issue | 26-87% |
| Package Managers | pnpm, npm, npx | 70-90% |
| Files | ls, read, grep, find | 60-75% |
| Infrastructure | docker, kubectl | 85% |
| Network | curl, wget | 65-70% |
Overall average: 60-90% token reduction on common development operations.