# Getting Started This guide takes you from an empty machine to a locally running arah² — the Telegram bot (**arah2bot**), its Cloudflare Worker, and the Mini App webview — in five steps. For what each piece does, see [ARCHITECTURE.md](ARCHITECTURE.md). For the full list of environment variables, config files, and per-package scripts, see [CONFIGURATION.md](CONFIGURATION.md). For a package overview, see the root [README.md](../README.md). For what arah² honestly does **not** do yet, see [KNOWN-LIMITATIONS.md](KNOWN-LIMITATIONS.md). --- ## Prerequisites | Requirement | Why | Notes | | --- | --- | --- | | **Node.js + npm** | Every build, dev, test, and deploy script in the repo is an npm script; all five workspaces are ESM (`"type": "module"`) | No `engines` field, `.nvmrc`, or `.node-version` exists anywhere in the repo. Use a current Node LTS — the repo's development and all four test runners run on Node v24, and the search/routing corpus tooling relies on Node 24's native TypeScript type-stripping. | | A **Telegram account** | You chat with arah2bot and receive the Mini App webview | — | | A bot token from **[@BotFather](https://t.me/BotFather)** | `BOT_TOKEN` is required for the Worker and every registration script | Also used as the HMAC key that validates Mini App `initData` | | **Internet access** | `wrangler dev`, the Telegram Bot API, and OpenFreeMap tile fetches all need the network | — | | **A Cloudflare account** *(deploy and `npm run bot:commands` only)* | `wrangler deploy`, the Workers KV namespace, and the R2 road-graph bucket live there | Wrangler prompts for browser-based auth on first use | Only required for specific commands, not for a normal first run: - **Python 3** — `npm run gallery` serves the design gallery via `python3 -m http.server 8123` (`theme/package.json`, script `basemap:preview`). - **A Playwright-managed Chromium** — `npm run capture` and `npm run web:capture` both call bare `chromium.launch()` (`theme/scripts/screenshot.mjs:103`, `web/scripts/capture.mjs:201`), so they need `npx playwright install chromium` first. No system Chrome is used. - **`sharp`** (in `routing/`'s devDependencies) — only the `npm run glyphs -w routing` glyph-builder needs it; nothing else in a first run imports it. Install is still one `npm install` from the root. There is no database to install, no Docker step, and no `.env` file to author — local secrets live only in `bot/.dev.vars` (created in step 3). See [CONFIGURATION.md](CONFIGURATION.md#environment-variables) for what each variable does. --- ## Installation 1. **Clone the repository.** ```bash git clone http://gitmirah:3000/c-fu/arah2.git ``` 2. **Enter the project directory.** The clone is named `arah2` after the repo slug; the workspace root is what `package.json` lives in. ```bash cd arah2 ``` 3. **Install all five workspaces from the root.** npm workspaces hoist dependencies into a single root `node_modules`, so one install covers `theme`, `bot`, `web`, `search`, and `routing` — including `@araharah/theme`, which `web` consumes by relative path and never needs to be published. ```bash npm install ``` --- ## First run ```mermaid flowchart TD CLONE["git clone"] --> INSTALL["npm install"] INSTALL --> VARS["cp bot/.dev.vars.example bot/.dev.vars"] VARS --> FILL["fill BOT_TOKEN, WEBHOOK_SECRET, WEBHOOK_URL"] FILL --> BUILD["npm run build"] BUILD --> DEV["npm run dev — wrangler dev on :8787"] DEV --> HOOK["point the webhook at the tunnel"] HOOK --> TALK["message arah2bot in Telegram"] style CLONE fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A style INSTALL fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A style VARS fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A style FILL fill:#EB8008,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A style BUILD fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A style DEV fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3 style HOOK fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3 style TALK fill:#736B1E,stroke:#1A1A1A,stroke-width:2px,color:#F7F0E3 ``` ### 1. Create your local secrets file `bot/.dev.vars` is gitignored (`bot/.gitignore`) and must never be committed. Copy the template: ```bash cp bot/.dev.vars.example bot/.dev.vars ``` Then fill in all three variables: ```bash # Copy to .dev.vars and fill for local wrangler dev (D-15). NEVER commit .dev.vars. BOT_TOKEN= WEBHOOK_SECRET= # Full origin + path; set-webhook.mjs auto-appends /telegram/webhook if only an origin is set. WEBHOOK_URL= ``` - **`BOT_TOKEN`** — from [@BotFather](https://t.me/BotFather). Required. - **`WEBHOOK_SECRET`** — any high-entropy random string you choose. Telegram echoes it in the `X-Telegram-Bot-Api-Secret-Token` header, and the Worker constant-time-compares it before touching any update. - **`WEBHOOK_URL`** — the public origin Telegram will POST updates to. Locally this is a tunnel (see step 5); `set-webhook.mjs` appends `/telegram/webhook` automatically if you give it a bare origin. Every script in `bot/scripts/` resolves each variable as `process.env. ?? devVars.`, so your shell environment wins over the file and the file is the fallback. ### 2. Build ```bash npm run build ``` This runs `build -w theme` then `build -w web`. **The order matters:** `theme/dist/rams.css` is an input to the web build, so the theme must compile first. Neither `theme/dist` nor `web/dist` is committed (root `.gitignore` excludes `dist/`), so this step is required on every fresh clone and before every `dev`/`deploy`. ### 3. Run the Worker locally ```bash npm run dev ``` Root `dev` is `npm run build && npm run dev -w bot`, which is `wrangler dev --port 8787 --local`. Wrangler reads `bot/.dev.vars`, emulates the `chat_sessions` KV namespace under `bot/.wrangler/`, and serves three things on **one origin, port `8787`**: | Path | Served by | | --- | --- | | `/telegram/webhook` | Worker `fetch` handler (`run_worker_first`) | | `/api/*` | Worker `fetch` handler (`run_worker_first`) | | everything else | `web/dist` static assets via the `ASSETS` binding | Because it is the single origin, you can smoke-test the API without Telegram at all: ```bash # Unknown paths are 404 — the Worker's fetch handler routes exactly six paths. curl -i http://localhost:8787/api/location # → 401 (no bearer token) # An unauthenticated webhook POST fails closed when WEBHOOK_SECRET is unset, # otherwise it 401s on the secret-token mismatch. curl -i -X POST http://localhost:8787/telegram/webhook ``` The turn-card cron can be fired by hand against the local server — wrangler's scheduled-event endpoint, no tunnel needed: ```bash curl "http://localhost:8787/cdn-cgi/local/scheduled" ``` ### 4. Expose the webhook `wrangler dev` binds to localhost only, so Telegram cannot reach it. Expose port `8787` with a tunnel, then run the one-time registration script, which reads the same `bot/.dev.vars`: ```bash # Terminal 1 — the dev server (from step 3) npm run dev # Terminal 2 — a tunnel that publishes port 8787 at a public HTTPS origin cloudflared tunnel --url http://localhost:8787 ``` Take the `https://…trycloudflare.com` origin it prints, put it in `WEBHOOK_URL` in `bot/.dev.vars`, and register it: ```bash npm run deploy --workspace bot # or, without the deploy half: see below ``` `bot`'s own `deploy` script is `wrangler deploy && node scripts/set-webhook.mjs`, which is the wrong tool for local work. For a **local** webhook registration, run the script directly — it only calls Telegram's `setWebhook`; it does not deploy anything: ```bash node bot/scripts/set-webhook.mjs ``` Expected output on success: ```text setWebhook ok:true — updates will be delivered to the configured URL with the secret-token header. ``` If `WEBHOOK_URL` has no path, the script prints a normalization line first: ```text WEBHOOK_URL had no path — normalized to .../telegram/webhook ``` Register the persistent command menu (English + Bahasa Melayu) once, so `/help`, `/privacy`, and `/cari` appear in the Telegram UI: ```bash npm run bot:commands ``` `register-commands.mjs` registers exactly `["help", "privacy", "cari"]` in four private-chat scopes — default (en), default+ms, all_private_chats (en), all_private_chats+ms — and clears the group scopes, because Telegram resolves a chat's menu from the *narrowest* scope that has any commands and a stale set shadows the default one. `/start` is a working handler that does **not** appear in the menu (type it manually); `/search` is `/cari`'s registered twin handler. The descriptions are derived from the `helpMenu` lines in `bot/src/i18n/strings.ts`, the same text `/help` replies with, so the menu and the reply cannot drift. ### 5. Talk to arah2bot Open Telegram, find **arah2bot**, and send `/start`. The Worker receives the update at `/telegram/webhook`, verifies the secret-token header, and replies. Supported commands: | Command | Alias | Behaviour | | --- | --- | --- | | `/start` | `/mula` | One-sentence introduction to arah² and its privacy posture | | `/help` | — | Lists the commands available and points at the known-limits record | | `/privacy` | `/privasi` | States exactly what is stored (~15 min) and what is never stored | | `/cari` | `/search` | Searches the KL gazetteer (`/cari KLCC`) and replies with one venue card carrying the OPEN IN ARAHARAH Mini App button plus 2–4 alternates | The Mini App opens from the venue card's OPEN button, or from the bot's menu button. The script that repoints the menu button at the deployed origin (`type: web_app`) has **no npm alias** — run it directly: ```bash node bot/scripts/set-menu-button.mjs ``` ### Working on the Mini App alone For map and UI work that doesn't need the bot, run Vite instead of the Worker. It serves on port **`5173`** with `strictPort: true`, so a port clash fails loudly rather than drifting: ```bash npm run web:dev ``` There is no Worker behind it, so `web/src/main.ts` falls back to the mock Telegram WebApp via `import.meta.env.DEV` and the `/api/*` calls stay stubbed. This is the fastest map/UI loop; run `npm run dev` when you need the real session handshake. --- ## Verify the setup Run the four test suites and the four typechecks — the runners are hand-rolled and zero-dependency (`node /test/runner.mjs` each), so no test framework needs installing: ```bash npm run bot:test # 10 suites in bot/test/*.test.mjs npm run web:test # 14 suites in web/test/*.test.mjs npm run search:test # 8 suites in search/test/*.test.mjs npm run routing:test # 9 suites in routing/test/*.test.mjs npm run bot:typecheck # tsc --noEmit npm run web:typecheck # tsc --noEmit npm run search:typecheck # tsc --noEmit npm run routing:typecheck # tsc --noEmit ``` Each runner prints `PASS ` / `FAIL ` per case and exits `0` only when every case passed. If a suite fails to load, it is recorded as one failing case rather than crashing the run. (The search and routing counts include their golden-corpus suites, which measure the *committed* datasets offline — no network.) The bot suites are `cari`, `commands`, `guidance-cron`, `guidance-envelope`, `initdata`, `location-pin`, `privacy`, `route-privacy`, `sessions`, and `webhook`; the web suites include `build-artifact`, `copy-gate`, `destination-preload`, `failure-state-matrix`, `fullscreen`, `guidance-eta`, `guidance-replay`, `guidance-surface`, `homescreen`, `location-chain`, `no-storage`, `route-surface`, `session`, and `theme`. Four are worth knowing about as you get started: - **`no-storage`** pins that nothing in `web/src` uses `localStorage`, `sessionStorage`, cookies, IndexedDB, or the Cache API. - **`privacy`** greps that every `chat_sessions` write carries an explicit `expirationTtl`. - **`route-privacy`** pins the same category-only, claim-derived access control on `/api/route`. - **`guidance-*`** (bot and web) pin the envelope lifecycle, the cron card projection, and the Mini App's replay/ETA surfaces. Optionally, prove the repo-wide privacy and budget invariants — these are the maintainer-time audits a fork must re-run before its first driver: ```bash npm run privacy-audit # storage, KV-TTL, category-only logs, host allowlist, bundle telemetry npm run budget-audit # Worker CPU, bundle sizes, KV write classes, golden corpora ``` And prove the design tokens carry no off-token values: ```bash npm run audit ``` `theme/scripts/audit.mjs` scans the rendered CSS and exits `1` on any colour, spacing, typography, radius, shadow, or blur value not derived from `theme/src/tokens.css`; it prints `AUDIT CLEAN — …` and exits `0` when the sheet is pure. --- ## Common setup issues **`npm run dev` starts but every request returns the Mini App, or nothing.** `web/dist` may be stale or absent. `run_worker_first` covers only `/api/*` and `/telegram/*` (a glob **array**, not a boolean), so everything else falls through to the assets binding. Run `npm run build` first, or just use `npm run dev` — it already builds. **`[webhook] rejected: misconfigured` → `500 Server misconfigured`.** `WEBHOOK_SECRET` is missing or empty in `bot/.dev.vars`. The fail-closed guard sits at the top of the `/telegram/webhook` branch in `bot/src/webhook.ts` and it fails closed deliberately: grammY's `compareSecretToken` silently accepts *every* request when the secret is `undefined`, so an unarmed secret must never reach it. Generate a new random string and put it in `WEBHOOK_SECRET`. **`setWebhook ok:true` but arah2bot never responds.** Telegram accepted the URL, so it answered `ok` while delivering updates to a path that `404`s. This happens when `WEBHOOK_URL` is a bare workers.dev origin. `set-webhook.mjs` now appends `/telegram/webhook` for you — if you registered the webhook by hand or via a different tool, re-run `node bot/scripts/set-webhook.mjs` and confirm the registered URL includes the path. **`/cari` is in the code but missing from the Telegram command menu.** The bot-side bug that caused this was found and fixed on 2026-10-25 (`register-commands.mjs` sent the scope under the wrong parameter name); re-running `npm run bot:commands` now arms all private-chat scopes correctly. What can *still* bite: the Telegram client **caches** the menu, so a device that opened the bot before a fix may keep showing the stale menu until the app restarts. The workaround is always available — type `/cari` in the chat; the command itself has always worked. **`Missing BOT_TOKEN, WEBHOOK_URL, WEBHOOK_SECRET` and exit code `1`.** `set-webhook.mjs` checks all three before any network call and never echoes values — only names. Confirm you ran it from a path where `bot/.dev.vars` resolves (`bot/.dev.vars` is read relative to the script, so it works from the repo root too), and that you did not commit a truncated file. **Nothing happens when I send `/start`.** Almost always the tunnel. `wrangler dev --local` binds to localhost, so Telegram has no route in. Re-run the tunnel, update `WEBHOOK_URL`, re-run `node bot/scripts/set-webhook.mjs`, and confirm the `tunnel origin` in `.dev.vars` still matches the one currently printing in Terminal 2 — tunnels rotate their hostname on every restart. **`/api/route` answers bare `null` for long trips.** That is the honest degradation, not a bug: route computation runs inside the Cloudflare Workers free plan's small per-request CPU allowance. Short KL trips (2–8 km) measure 2–8 ms and resolve; cross-metro trips (20–30 km) measured p95 38–93 ms against the same graph, beyond the free plan, so the app shows the router-pending card instead of freezing or drawing a wrong line. The committed upgrade lever (ALT landmark preprocessing) is a next-milestone item — see [KNOWN-LIMITATIONS.md](KNOWN-LIMITATIONS.md). Workers Paid was deliberately rejected: it breaks the project's $0 mandate. **`chromium.launch()` fails — `npm run capture` or `npm run web:capture` errors on a missing browser.** Both scripts use bare `chromium.launch()` with no `executablePath` and no `channel`, so they need Playwright's own browser build: ```bash npx playwright install chromium ``` `npm run web:capture` additionally requires the harness build to exist — it serves `web/dist-harness/`, not `web/dist`, and waits for `RAMS-BASEMAP-READY` then `ARAHARAH-LIVE` signals. Run `npm run web:build:harness` first. Its 90-second denial timeout is deliberate: the chat-pin poll must elapse before the denied card appears. **Mini App boots but shows no map.** Check the network tab for tile requests to `https://tiles.openfreemap.org/…` — the generated `theme/basemap/liberty-rams.json` points there, so a blocked or offline request leaves a blank canvas. `npm run gallery` serves the basemap locally on `:8123` if you want to inspect the style in isolation. **`theme/dist/rams.css` is missing when `web` builds.** Run `npm run build` from the root rather than `npm run build -w web`. The root `build` script chains theme then web; the web build imports the compiled stylesheet, not the token source. --- ## Next steps - **[ARCHITECTURE.md](ARCHITECTURE.md)** — how the Worker, KV namespace, R2 graph, Mini App, and design system fit together; the Worker routing table; the three-link location acquisition chain. - **[CONFIGURATION.md](CONFIGURATION.md)** — every environment variable, `bot/wrangler.jsonc` and `web/vite.config.ts`, and the full command reference per workspace. - **[TESTING.md](TESTING.md)** — each suite's contract and how to write a new one. - **[KNOWN-LIMITATIONS.md](KNOWN-LIMITATIONS.md)** — stated plainly: no traffic awareness, no lane guidance, the cross-metro CPU degradation, the stale Telegram menu cache, and what is on the roadmap. Beta-quality caveats included (e.g. battery drain is a *planned* measurement before the next driver onboarding, not an estimate). - **[../README.md](../README.md)** — package overview, HTTP surface, design-system usage. - **[../DESIGN.md](../DESIGN.md)** — the Dieter Rams philosophy behind the token vocabulary. ### Everyday commands | Command | What it does | | --- | --- | | `npm run dev` | Build, then `wrangler dev --port 8787 --local` | | `npm run build` | Build theme → Mini App (`theme/dist/rams.css` must exist first) | | `npm run web:dev` | Vite dev server on `5173` with a mock WebApp and no Worker | | `npm run bot:test` / `npm run web:test` / `npm run search:test` / `npm run routing:test` | Zero-dependency test runners | | `npm run bot:typecheck` / `npm run web:typecheck` / `npm run search:typecheck` / `npm run routing:typecheck` | `tsc --noEmit` | | `npm run privacy-audit` | Repo-wide privacy audit (add `--` and `--live` for the live KV/cache probes) | | `npm run budget-audit` | Free-tier budget audit, measured and printed | | `npm run audit` | Off-token audit of the rendered CSS; non-zero on violations | | `npm run bot:commands` | Register the persistent Telegram command menu (EN + MS) | | `npm run deploy` | Build, then `wrangler deploy` + `set-webhook.mjs` | The two build outputs must **never** share an `outDir`: `web:build:harness` emits to `web/dist-harness/` with the mock installer and `/api/*` fetch stubs that production must not carry. `vite.config.ts` calls this out as the reason the split exists. ### Deploying `npm run deploy` publishes the Worker to Cloudflare and registers the webhook. It reads no local file for secrets — production values live as Cloudflare Workers secrets. See [CONFIGURATION.md](CONFIGURATION.md#per-environment-overrides). A fork must also provision its own R2 road-graph bucket (`npx wrangler r2 bucket create ` + `routing/scripts/upload-road-graph.mjs`) — the fork contract is [ROAD-GRAPH.md](ROAD-GRAPH.md).