Architecture¶
Overview¶
Zentr is a single Cloudflare Worker (Hono/TypeScript) acting as the backend for a Telegram bot and a future mobile app. Users interact via Telegram; their financial data lives in their own Google Sheet, inside their own Google Drive.
Hexagonal Architecture (Ports & Adapters)¶
┌─────────────────────────────────────────────────────┐
│ ports/ (primary ports — driving side) │
│ http/ HTTP entry points (Hono handlers) │
│ cron/ Scheduled job runners │
├─────────────────────────────────────────────────────┤
│ application/ Use cases — orchestrate domain │
│ transaction/ Add, list, settle, defer │
│ digest/ Daily & weekly digests │
│ snapshot/ Net-worth snapshots │
│ user/ Onboard, get │
├─────────────────────────────────────────────────────┤
│ infrastructure/ Adapters — implement ports │
│ sheets/ Google Sheets API (data store) │
│ persistence/ D1 (users) + KV (sessions) │
│ ai/ Gemini analysis │
│ messaging/ Telegram send + parse │
├─────────────────────────────────────────────────────┤
│ domain/ Pure business logic — no I/O │
│ transaction/ Transaction entity + ports │
│ user/ User entity + port │
│ digest/ Digest value objects │
│ shared/ Money, DateRange │
└─────────────────────────────────────────────────────┘
Layer Rules¶
domain/— zero external imports. No Hono, no KV, no fetch, no npm packages.application/— imports fromdomain/only. Depends on repository interfaces (ports), not concrete adapters.infrastructure/— implements domain ports. All network/storage I/O lives here.ports/— wires use cases to the outside world. Thin layer; no business logic.
Request Flow (Webhook)¶
POST /webhook
└─ WebhookHandler.handle()
├─ TelegramParser.extractChatId / extractText / extractCommand
├─ [pre-auth] OnboardUserUseCase / config reply
└─ GetUserUseCase → User (with locale)
├─ SheetsAuth.getAccessToken (KV cache → refresh if expired)
├─ SheetsClient (Google Sheets API)
├─ SheetsTransactionRepository (implements TransactionRepository)
└─ AddTransactionUseCase / ListOpenTransactionsUseCase / SettleTransactionUseCase
└─ TelegramSender.sendText (Telegram Bot API)
Cron Flow¶
Three cron expressions in wrangler.toml:
| Cron | Handler | Purpose |
|---|---|---|
0 8 * * * |
DailyDigestCron |
Yesterday's transactions + per-diem status |
0 8 * * 1 |
WeeklySummaryCron |
Last week summary + Gemini AI analysis |
0 7 * * * |
DeferredSettleCron |
Auto-settle deferred transactions whose date ≤ today |
Each cron iterates all users from D1 and fans out with Promise.all.
OAuth Flow¶
/start command
└─ SheetsAuth.buildOAuthUrl(chatId, redirectUri)
└─ User visits Google OAuth consent screen
└─ Google redirects to GET /auth/callback?code=…&state=chatId
└─ OAuthCallbackHandler
├─ SheetsAuth.exchangeCode(code, redirectUri) → refreshToken
└─ KVSessionStore.set("pending_token:{chatId}", refreshToken, ttl=600s)
└─ User sends /config SHEET_ID
└─ OnboardUserUseCase: save {chatId, sheetId, refreshToken, locale} to D1
Cloudflare Bindings¶
| Binding | Type | Purpose |
|---|---|---|
KV |
KV Namespace | Refresh token cache, pending OAuth sessions |
DB |
D1 Database | User records (chat_id, sheet_id, refresh_token, plan, locale) |
TELEGRAM_TOKEN |
var | Bot API token |
GEMINI_KEY |
var | Gemini API key |
GEMINI_MODEL |
var | Model name (default: gemini-2.5-flash) |
GOOGLE_CLIENT_ID |
var | OAuth client ID |
GOOGLE_CLIENT_SECRET |
var | OAuth client secret |
TEMPLATE_SHEET_ID |
var | ID of master template Sheet |
Project Structure¶
backend/
├── src/
│ ├── domain/
│ │ ├── transaction/ Transaction entity, TransactionType, TransactionStatus, port
│ │ ├── user/ User entity, port
│ │ ├── digest/ DailyDigest, WeeklyDigest value objects
│ │ └── shared/ Money, DateRange
│ ├── application/
│ │ ├── transaction/ Add, ListOpen, Settle, SettleDeferred use cases
│ │ ├── digest/ SendDailyDigest, SendWeeklyDigest
│ │ ├── snapshot/ SaveSnapshot
│ │ └── user/ OnboardUser, GetUser
│ ├── infrastructure/
│ │ ├── sheets/ SheetsTransactionRepository, SheetsClient, SheetsAuth
│ │ ├── persistence/ D1UserRepository, KVSessionStore
│ │ ├── ai/ GeminiAnalysisService
│ │ └── messaging/ TelegramSender, TelegramParser
│ ├── ports/
│ │ ├── http/ Hono app, WebhookHandler, OAuthCallbackHandler
│ │ └── cron/ DailyDigestCron, WeeklySummaryCron, DeferredSettleCron
│ └── types.ts
├── migrations/
│ └── 001_users.sql
├── wrangler.toml
└── package.json
D1 Schema¶
CREATE TABLE users (
chat_id TEXT PRIMARY KEY,
sheet_id TEXT NOT NULL,
refresh_token TEXT NOT NULL,
plan TEXT DEFAULT 'free',
created_at TEXT DEFAULT (datetime('now'))
);