14 KiB
Configuration
arah² is an npm-workspaces monorepo with three workspaces — theme, bot, web — plus a root
package.json that composes their scripts. There is no committed .env file: secrets live in
bot/.dev.vars (gitignored) for local work and in Cloudflare Workers secrets for deployment.
Configuration surfaces, by kind:
| Surface | File | Applies to |
|---|---|---|
| Workspaces + scripts | package.json |
all |
| Worker deployment + bindings | bot/wrangler.jsonc |
bot |
| Secret env contract | bot/.dev.vars (from .dev.vars.example) |
bot |
| Worker env typing | bot/src/config.ts |
bot |
| Local secrets template | bot/.dev.vars.example |
bot |
| Bundler config | web/vite.config.ts |
web |
| TypeScript config | bot/tsconfig.json, web/tsconfig.json |
bot, web |
| Design tokens (source of truth) | theme/src/tokens.css → theme/src/theme.css |
theme + everything downstream |
| Ignore rules | .gitignore, bot/.gitignore |
repo |
Environment variables
Three variables reach the Worker (bot/src/config.ts declares them as a typed Env interface);
a fourth (WEBHOOK_URL) is consumed only by the one-time registration scripts, never by the Worker.
| Variable | Required | Default | Description |
|---|---|---|---|
BOT_TOKEN |
Required | none | Telegram Bot API token from @BotFather. Used by createBot() in bot/src/bot/bot.ts to construct the grammY Bot, and by validateInitData() in bot/src/security/initdata.ts as the HMAC key for Mini App initData verification. |
WEBHOOK_SECRET |
Required | none | Value Telegram echoes in the X-Telegram-Bot-Api-Secret-Token header on every update. /telegram/webhook constant-time-compares it before any bot processing or KV access. If missing or empty at request time the route fails closed with 500 Server misconfigured — grammY's secret check would otherwise accept every request. |
WEBHOOK_URL |
Required for the one-time scripts | none | Full origin + path of the deployed Worker, used by bot/scripts/set-webhook.mjs and bot/scripts/set-menu-button.mjs. If the value points at the workers.dev root (no path), set-webhook.mjs appends /telegram/webhook automatically. The Worker never reads this variable. |
chat_sessions |
Required (binding) | n/a | Not an env var but a Cloudflare KV namespace binding. Wired in bot/wrangler.jsonc under kv_namespaces; every kv.put() in bot/src/kv/sessions.ts and bot/src/kv/location-pin.ts writes through it with an explicit expirationTtl. |
Local secret template — bot/.dev.vars.example:
# Copy to .dev.vars and fill for local wrangler dev. 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=
Script-level precedence: every script in bot/scripts/ resolves a variable as
process.env.<NAME> ?? devVars.<NAME> — shell environment wins over the file, and the file is the
fallback. set-webhook.mjs requires all three of BOT_TOKEN, WEBHOOK_URL, WEBHOOK_SECRET and
exits 1 listing whichever are missing. register-commands.mjs requires only BOT_TOKEN.
set-menu-button.mjs requires BOT_TOKEN and WEBHOOK_URL.
Configuration flow
flowchart TD
subgraph Local["Local development"]
DEVVARS["bot/.dev.vars (gitignored)"]
WRANGLERDEV["wrangler dev --port 8787 --local"]
end
subgraph Scripts["One-time Telegram registration"]
SC1["scripts/set-webhook.mjs"]
SC2["scripts/register-commands.mjs"]
SC3["scripts/set-menu-button.mjs"]
end
subgraph Production["Deployed Worker"]
SECRETS["wrangler secrets: BOT_TOKEN, WEBHOOK_SECRET"]
KV["KV namespace binding: chat_sessions"]
ASSETS["Static assets: ../web/dist"]
WORKER["src/webhook.ts fetch handler"]
end
DEVVARS --> WRANGLERDEV
DEVVARS --> SC1
DEVVARS --> SC2
DEVVARS --> SC3
SECRETS --> WORKER
KV --> WORKER
ASSETS --> WORKER
WRANGLERDEV --> WORKER
style Local fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style Scripts fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style Production fill:#D9D2C6,stroke:#1A1A1A,color:#1A1A1A
style DEVVARS fill:#EB8008,stroke:#1A1A1A,color:#1A1A1A
style SECRETS fill:#ED3F1C,stroke:#1A1A1A,color:#FFFFFF
The Mini App in web/ has no build-time or runtime configuration surface. It calls its own
origin with relative paths (/api/session, /api/location, /api/location-fallback), so the API
base URL is implied by wherever the bundle is served from — which is why the Worker serves
../web/dist as its own static assets and reserves /api/* and /telegram/* for itself.
Required vs optional settings
Fail loudly (Worker):
WEBHOOK_SECRET— the only setting with an explicit runtime guard, atbot/src/webhook.ts:65:if (!env.WEBHOOK_SECRET)logs[webhook] rejected: misconfiguredand returns500 Server misconfigured. The rationale is documented in the file: grammY'scompareSecretTokensilently accepts every request when the secret isundefined, so an unarmed binding must never reach it.BOT_TOKEN— declared required in theEnvtype but not guarded at runtime; a missing token surfaces as a grammY authentication failure on the first request that instantiates the bot.
Fail loudly (scripts): set-webhook.mjs, register-commands.mjs, and set-menu-button.mjs each
print a missing-variable list to stderr and process.exit(1) before any network call.
With defaults / derived:
KV namespace id—bot/wrangler.jsoncalready carries a concrete id forchat_sessions; the file's leading comment notes it was filled in afternpx wrangler kv namespace create chat_sessions.WEBHOOK_URLpath segment — auto-derived byset-webhook.mjswhen the configured origin has no path.- KV TTLs — constants in source, not configuration:
SESSION_TTL_SECONDS = 900(bot/src/kv/sessions.ts) andPIN_TTL_SECONDS = 300(bot/src/kv/location-pin.ts). Both are pinned by a grep gate in the test suite so no write path can persist without a TTL.
Worker configuration file (bot/wrangler.jsonc)
{
// KV namespace id is filled in plan 03 task 1 after `npx wrangler kv namespace create chat_sessions`
"name": "araharah-bot",
"main": "src/webhook.ts",
"compatibility_date": "2026-10-22",
"workers_dev": true,
"assets": {
"directory": "../web/dist",
"binding": "ASSETS",
"not_found_handling": "none"
},
"run_worker_first": ["/api/*", "/telegram/*"],
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
}
Key notes:
assets.directory: "../web/dist"— the Worker serves the Mini App bundle itself, sonpm run build(theme then web) must run beforewrangler deploy; a stale or absentweb/distships no Mini App.run_worker_firstis a glob array, not a boolean:/api/*and/telegram/*are handled bysrc/webhook.tsfirst; everything else falls through to the static asset binding.not_found_handling: "none"— unmatched paths are not served the SPAindex.htmlfallback.workers_dev: trueserves on the<name>.<account>.workers.devURL, which is theWEBHOOK_URLtarget.
Build and run scripts
Root package.json (workspaces: theme, bot, web; all packages are "private": true, ESM via
"type": "module"):
| Command | Description |
|---|---|
npm run build |
Builds the theme then the web bundle (theme → web). |
npm run deploy |
build, then npm run deploy -w bot (wrangler deploy + set-webhook.mjs). |
npm run dev |
build, then npm run dev -w bot (wrangler dev --port 8787 --local). |
npm run bot:test / bot:typecheck / bot:dev / bot:deploy / bot:commands |
Per-workspace passthroughs to the bot package. |
npm run web:test / web:typecheck / web:dev |
Passthroughs to the web package. |
npm run web:build |
Same as build; web:build:harness runs the harness-mode build; web:capture runs the screenshot harness. |
npm run sheet / gallery / audit / capture |
Passthroughs to the theme design-system tooling. |
Workspace-level commands worth knowing:
| Command | Where | Notes |
|---|---|---|
npm test |
bot, web |
Both run node test/runner.mjs (a hand-rolled runner, no external test framework). |
npm run typecheck |
bot, web |
tsc --noEmit, strict plus noUncheckedIndexedAccess. |
npm run dev |
bot |
wrangler dev --port 8787 --local — reads bot/.dev.vars. |
npm run deploy |
bot |
wrangler deploy && node scripts/set-webhook.mjs. |
npm run commands |
bot |
node scripts/register-commands.mjs — setMyCommands in default scope and in language_code: "ms"; descriptions are derived from src/i18n/strings.ts helpMenu lines so the menu and /help reply cannot drift. |
npm run build |
web |
vite build → web/dist. |
npm run build:harness |
web |
vite build --mode harness → web/dist-harness, with mock installer and fetch stubs. |
npm run dev |
web |
vite dev server on port 5173 (strictPort: true). |
npm run capture |
web |
node scripts/capture.mjs — serves web/dist-harness and screenshots the boot/live/denied states. |
npm run build |
theme |
tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify. |
npm run audit |
theme |
node ./scripts/audit.mjs ./dist/rams.css — flags any rendered color/spacing/typography/radius/shadow/blur value not derived from src/tokens.css. Exits 0 clean, 1 violations, 2 drift. |
npm run basemap |
theme |
node basemap/build.mjs — forks OpenFreeMap liberty into theme/basemap/liberty-rams.json, deriving every cartographic value from the token file. |
npm run gallery / sheet / capture |
theme |
Design tooling; gallery serves on http://localhost:8123 via python3 -m http.server 8123. |
Bundler configuration (web/vite.config.ts)
Vite is configured from a function of mode, with one switch driving a dual-build contract:
__ARAHARAH_HARNESS__is injected viadefineastrueonly in--mode harnessandfalseeverywhere else. Vite replaces the identifier textually, so the production bundle tree-shakes the mock installer and/api/*fetch stubs out entirely;web/index.htmlcarries its mock profile block inside the same guarded branch.build.outDirisdist-harnessin harness mode,distotherwise, withemptyOutDir: trueandtarget: "es2022". The two outputs must never share a directory — the file's own comment calls that out as the reason the split exists.base: "/", dev server on port 5173 withstrictPort: true.__ARAHARAH_HARNESS__is declared ambient inweb/src/vite-env.d.tssotsc --noEmitpasses in both modes.- Harness-only URL parameters (documented in
web/index.html):?mock=android|ios|tdesktop|macos|web,?scheme=light|dark,?pin=1,?locale=en|ms,?sessiondelay=<ms>,?fixdelay=<ms>.
Design token configuration
theme/src/tokens.css is the single source of truth for every color, spacing, typography, and radius
value. theme/src/theme.css imports Tailwind v4 plus tokens.css and components.css, declares
self-hosted Geist and Space Grotesk @font-face rules from theme/fonts/ (no Google Fonts CDN), and
scopes class generation with explicit @source lines — the four mockup markup files under
stitch_minimalist_route_navigator/ are the only allowed utility sources, with
@source not "../scripts" and @source not "../basemap" excluding prose.
Downstream consumers:
web/src/shell.css—@import "@araharah/theme/dist/rams.css";web/src/map/map.ts—import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";theme/basemap/liberty-rams.json— the generated basemap style; its source URL, sprite, and glyph endpoints point athttps://tiles.openfreemap.org/....
Ignore rules and generated output
.gitignore at the repo root excludes .planning/, node_modules/, dist/ (which covers both
theme/dist/ and web/dist/), web/dist-harness/, theme/screenshots/, web/screenshots/,
.env, .env.* (with !*.example), and *:Zone.Identifier. bot/.gitignore adds .dev.vars and
.wrangler/.
Practical consequence: web/dist and theme/dist are never committed. Both must be rebuilt from
source before a deploy, which is why npm run deploy and npm run dev both run build first.
Per-environment overrides
There are no committed .env.development, .env.production, or .env.test files. The separation
is structural rather than file-based:
| Environment | Configuration source | How it differs |
|---|---|---|
Local wrangler dev |
bot/.dev.vars |
Secrets read from the gitignored file; KV emulated locally under bot/.wrangler/; Mini App served from web/dist. |
Local vite dev |
none | Vite dev server on 5173 with no Worker behind it — main.ts falls back to the mock WebApp via import.meta.env.DEV. |
| Harness capture | vite build --mode harness |
web/dist-harness plus web/index.html's URL-parameter profile. Never deployed. |
| Production | wrangler.jsonc + Cloudflare secrets |
Secrets set with wrangler secret put rather than a file; assets come from the uploaded ../web/dist; KV resolves to the real namespace. |
@araharah/theme and @araharah/web are consumed by relative path within the workspace, so there is
no registry token or publish step to configure.