Guidance for working in this repository. Read this first, then the doc it points you to for the area you're changing.
Two independent prayer-time Telegram bots share one Git repository but not one runtime:
- Legacy city bots (repo root:
serverless/,domain/,config/,internal/db/,log/,migrations/,infra/gcp/). One codebase, one bot per city, keyed bybot_id. GCP Cloud Functions + Supabase Postgres (public.chats,public.prayers) + GCS CSV schedules. Documented in the rootREADME.md. - Global worldwide bot (
global/). A separate Go module with its own container image, Cloud Run services, Terraform state, Telegram token, Secret Manager entries, and Postgres schemas. Nothing underglobal/imports or changes the legacy runtime, and vice-versa.
Active work is on the global bot. Unless a task is explicitly about the legacy city bots, you are working in global/.
- Read
global/docs/README.mdfirst — it indexes every engineering doc. Then read the doc that owns the area you're changing (see routing table below). Architectural or persistence changes must update the owning doc in the same PR. - Graphify (per
AGENTS.md): readgraphify-out/GRAPH_REPORT.mdbefore broad source reading or grep. For "how does X relate to Y" usegraphify query "…"/graphify path "A" "B"/graphify explain "…". After changing code, rungraphify update .(AST-only, no API cost).
| Doc | Owns |
|---|---|
global/docs/architecture.md |
System boundary, trust relationships, Mini App, data ownership, calculation profile, delivery overview, rollout gates |
global/docs/code-map.md |
Which package/executable owns a behavior; change-routing table for common tasks |
global/docs/data-model.md |
Tables, invariants, retention, migration rules |
global/docs/request-flows.md |
Webhook, location, Mini App session, schedule display, occasions, Qibla/calendar, feedback |
global/docs/reminder-delivery.md |
Source of truth for planning, dispatch, retries, idempotency, cleanup categories |
global/docs/runtime-and-deployment.md |
Env isolation, runtime topology, secrets, DB connection rules, deploy workflow |
global/docs/operations.md |
Health/logs, incident triage table, secret rotation, profile sync, recovery |
global/docs/testing.md |
Test strategy: time injection, unit vs. integration, running TEST_DATABASE_URL integration tests |
Three Cloud Run services from one immutable image, selected by container command:
cmd/webhook— public. Telegram commands/callbacks, feedback, owner dashboard, and the embedded Mini App (static files +/api/miniapp/*) served at/app/.cmd/dispatch— Scheduler-only. Claims due schedules, drains the outbox into Cloud Tasks, runs retention cleanup.cmd/send— Cloud Tasks-only. Idempotent Telegram delivery, recurrence advance, message cleanup.cmd/botprofile,cmd/bootstrapdb— deploy-time commands (profile/webhook sync; schema creation).
The module is hexagonal (ports and adapters); dependencies point inward and cmd/* are the only composition roots. Layers: internal/domain (DTOs + pure policy + ErrNotFound — the shared vocabulary of every signature), internal/port (shared interfaces: Store, Calculator, LocationResolver, MetalSource), internal/core/* (pure logic and application services: prayertime, hijri, occasions, qibla, calendarfile, i18n with 8 locales — en, ar, es, fr, ru, tr, uz, tt — and reminders planner/dispatch/sender), internal/adapter/in/* (driving: telegram bot UI, miniapp web UI + signed init-data auth + calendar feed), internal/adapter/out/* (driven: store all SQL, location Google Maps, metals, botprofile; each carries a var _ port.X assertion and translates its errors to domain.ErrNotFound), plus platform packages config, database, assets, httpx. Core never imports an adapter. See code-map.md for ownership and dependencies.
make test # go test ./...
make check # gofmt -w cmd internal && go vet ./... && go test ./...
make build # go build ./cmd/...Run make check before finishing a change. The legacy root module has its own Makefile.
- pgx + Supabase transaction pooler: runtime connections MUST use
pgx.QueryExecModeExec(no named prepared-statement cache), or you get42P05/26000. In that mode, JSONB params must be passed as JSON text, not[]byte(else22P02) — use the shared JSON-text encoder ininternal/adapter/out/store. - Schema isolation: all global SQL goes through
internal/adapter/out/store, which qualifies the logicalglobal_botschema with the env schema (global_bot_testing/global_bot_production). Never touch legacypublic.*. Goose must always target-table="${GLOBAL_DB_SCHEMA}.goose_db_version"; bootstrap the schema before the first migration. - Delivery is at-least-once in a narrow window (Telegram send succeeds, completion commit fails). The sender must make a compensating
deleteMessagescall after a post-send failure before returning a retryable error. Cleanup uses one message slot per category (prayer,tomorrow,weekly_fasting,weekly_kahf,islamic_occasion); every message also gets a 36h cleanup task (Telegram can't delete messages older than 48h). - Profile version increments on any change affecting calculated times; queued tasks carry it and go stale instead of sending after a change.
- Mini App auth: never trust a Telegram user ID from a JSON body. Identity comes only from the signed
initDataheader (HMAC verified, dedup fields, <24h). Private-chat scoped; group config stays in the bot with admin authorization. - Privacy: coordinates rounded to 3 decimals on persist; reverse-geocoded city is not stored (only Place ID); feedback is never stored in Postgres. Never log tokens, secrets, DB URLs, coordinates, Maps keys, full Telegram updates, or the calendar bearer URL.
- Hijri correction (-2..+2 days) shifts displayed Hijri dates and occasion matching only — never prayer instants.
- Calculation engine is hidden behind the
port.Calculatorinterface (implemented bycore/prayertimeovergithub.com/hablullah/go-prayer); keep that boundary so engines stay swappable.
Manual Deploy global prayer bot workflow, env testing (GitHub dev secrets, schema global_bot_testing) or production (GitHub prod, global_bot_production). Only three global secrets are new: GLOBAL_BOT_TOKEN, GLOBAL_WEBHOOK_SECRET, GLOBAL_OWNER_ID. Migrations run before the new image; schema changes must be backward-compatible with the previously running revision. Never interchange testing/production state prefixes or tokens.