- 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
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 galleryserves the design gallery viapython3 -m http.server 8123(theme/package.json, scriptbasemap:preview). - A Playwright-managed Chromium —
npm run captureandnpm run web:captureboth call barechromium.launch()(theme/scripts/screenshot.mjs:103,web/scripts/capture.mjs:163), so they neednpx playwright install chromiumfirst. 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
-
Clone the repository.
git clone http://gitmirah:3000/c-fu/arah2.git -
Enter the project directory. The clone is named
arah2after the repo slug; the workspace root is whatpackage.jsonlives in.cd arah2 -
Install all three workspaces from the root. npm workspaces hoist dependencies into a single root
node_modules, so one install coverstheme,bot, andweb— including@araharah/theme, whichwebconsumes 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 theX-Telegram-Bot-Api-Secret-Tokenheader, 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.mjsappends/telegram/webhookautomatically 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-storagepins that nothing inweb/srcuseslocalStorage,sessionStorage, cookies, IndexedDB, or the Cache API.privacygreps that everychat_sessionswrite carries an explicitexpirationTtl.
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.jsoncandweb/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.