Files
arah2/docs/GETTING-STARTED.md

20 KiB
Raw Permalink Blame History

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. For the full list of environment variables, config files, and per-package scripts, see CONFIGURATION.md. For a package overview, see the root README.md. For what arah² honestly does not do yet, see 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 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 for what each variable does.


Installation

  1. Clone the repository.

    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.

    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.

    npm install
    

First run

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:

cp bot/.dev.vars.example bot/.dev.vars

Then fill in all three variables:

# 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. 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.<NAME> ?? devVars.<NAME>, so your shell environment wins over the file and the file is the fallback.

2. Build

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

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:

# 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:

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:

# 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:

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:

node bot/scripts/set-webhook.mjs

Expected output on success:

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:

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:

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:

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:

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 <workspace>/test/runner.mjs each), so no test framework needs installing:

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 <name> / FAIL <name> 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:

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:

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 404s. 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. 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:

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 — 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 — every environment variable, bot/wrangler.jsonc and web/vite.config.ts, and the full command reference per workspace.
  • TESTING.md — each suite's contract and how to write a new one.
  • 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 — package overview, HTTP surface, design-system usage.
  • ../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. A fork must also provision its own R2 road-graph bucket (npx wrangler r2 bucket create <name> + routing/scripts/upload-road-graph.mjs) — the fork contract is ROAD-GRAPH.md.