Files
Arah2/docs/GETTING-STARTED.md
T
araharah-executor 91ad0c9d93 docs: generate + verify project documentation — canonical docs, per-package READMEs, verified fix pass
- New: docs/ARCHITECTURE.md, docs/GETTING-STARTED.md, docs/DEVELOPMENT.md,
  docs/TESTING.md, docs/CONFIGURATION.md (colored mermaid diagrams, quoted labels)
- Regenerated README.md (arah² / arah2bot naming, canonical clone URL)
- New per-package READMEs: theme/, bot/, web/
- Verified 13 docs against the live codebase (2 fix iterations + 2 residual
  corrections); corrected 56 stale claims across AGENTS/DESIGN/IDEAS/GSD_FIX
2026-10-23 20:56:40 +08:00

16 KiB

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.


Prerequisites

Requirement Why Notes
Node.js + npm Every build, dev, test, and deploy script in the repo is an npm script; all three workspaces are ESM ("type": "module") No engines field, .nvmrc, or .node-version exists anywhere in the repo. Use a current Node LTS.
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 and the Workers KV namespace 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:163), so they need npx playwright install chromium first. No system Chrome is used.

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 three workspaces from the root. npm workspaces hoist dependencies into a single root node_modules, so one install covers theme, bot, and web — 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 four 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

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 and /privacy appear in the Telegram UI:

npm run bot:commands

register-commands.mjs registers exactly ["help", "privacy"] in two scopes — the default scope plus language_code: "ms" — and deliberately reserves no stub entries for future commands, so /start is a working handler that does not appear in the menu (type it manually). 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 in this phase
/privacy /privasi States exactly what is stored (~15 min) and what is never stored

The Mini App opens from the bot's menu button. The script that repoints it 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 both suites and both typechecks — the runners are hand-rolled and zero-dependency (node bot/test/runner.mjs and node web/test/runner.mjs), so no test framework needs installing:

npm run bot:test        # 6 suites in bot/test/*.test.mjs
npm run web:test        # 6 suites in web/test/*.test.mjs
npm run bot:typecheck   # tsc --noEmit
npm run web: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 bot suites are commands, initdata, location-pin, privacy, sessions, and webhook; the web suites are build-artifact, copy-gate, location-chain, no-storage, session, and theme. Two 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.

Optionally, 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 guard is at bot/src/webhook.ts:65 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.

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.

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, 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.
  • ../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 Zero-dependency test runners
npm run bot:typecheck / npm run web:typecheck tsc --noEmit
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.