A production-ready, async Telegram bot that captures notes via the /note command, persists them in a database, and synchronises every note to a Notion database in real time.
Built with clean architecture, full type hints, structured logging, and Docker support — ready for senior-level technical evaluation.
| Feature | Detail |
|---|---|
| Async Telegram Bot | Webhook-based, powered by FastAPI |
| Database Persistence | SQLAlchemy async with SQLite (switchable to PostgreSQL) |
| Notion Sync | Every note pushed to Notion via official API |
| Clean Architecture | Service layer, dependency injection, no logic in routes |
| Structured Logging | JSON file logs + console, rotating file handler |
| Database Migrations | Alembic with async support |
| Dockerised | Dockerfile + docker-compose ready |
| Environment Config | Pydantic Settings — zero hardcoded secrets |
app/
├── main.py # FastAPI app with lifespan
├── config.py # Pydantic Settings (env vars)
├── database.py # Async engine & session factory
├── logging_config.py # Structured logging setup
├── models/
│ └── note.py # SQLAlchemy ORM model
├── schemas/
│ └── note_schema.py # Pydantic validation schemas
├── services/
│ ├── note_service.py # Database CRUD operations
│ ├── notion_service.py # Notion API integration
│ └── telegram_service.py # Command orchestration
├── routes/
│ └── webhook.py # POST /webhook endpoint
└── utils/
Telegram → POST /webhook → webhook.py (validate secret)
→ telegram_service.py (orchestrate)
→ note_service.py (save to DB)
→ notion_service.py (push to Notion)
→ note_service.py (update sync status)
→ Send reply via Bot API
- Async Everywhere — Non-blocking I/O for database, HTTP, and Telegram calls. The server can handle many concurrent webhook requests.
- Service Layer — Business logic lives in services, not route handlers. Each service has a single responsibility.
- Dependency Injection — Database sessions are created per-request and passed down. No global mutable state.
- Separation of Concerns — Telegram parsing, database operations, and Notion API calls are fully isolated. Any component can be replaced independently.
- Python 3.11+
- A Telegram bot token (from @BotFather)
- A Notion integration token and database ID
# 1. Clone the repository
git clone <repo-url>
cd telegram_bot
# 2. Create a virtual environment
python -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\activate # Windows
# 3. Install dependencies
pip install -r requirements.txt
# 4. Configure environment
cp .env.example .env
# Edit .env with your actual tokens
# 5. Run database migrations
alembic upgrade head
# 6. Start the server
uvicorn app.main:app --reload --port 8000After starting the server on a public URL (e.g. via ngrok or VPS), register the webhook:
curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook",
"secret_token": "<YOUR_WEBHOOK_SECRET>"
}'Tip: The
secret_tokenmust match theTELEGRAM_WEBHOOK_SECRETin your.envfile. Telegram will send it as theX-Telegram-Bot-Api-Secret-Tokenheader with every update.
curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getWebhookInfo"- Go to notion.so/my-integrations.
- Click "New integration".
- Name it (e.g.
Telegram Notes Bot), select the workspace. - Copy the Internal Integration Token → paste into
NOTION_TOKENin.env.
Create a database with these exact property names and types:
| Property | Type |
|---|---|
Name |
Title |
Telegram User |
Rich text |
Created |
Date |
- Open your Notion database page.
- Click "…" → "Connections" → select your integration.
From the database URL:
https://www.notion.so/<workspace>/<DATABASE_ID>?v=...
Copy the DATABASE_ID part (32 hex characters) → paste into NOTION_DATABASE_ID in .env.
In any Telegram chat with the bot:
/note Buy groceries
Responses:
| Scenario | Reply |
|---|---|
| Success | ✅ Note saved and synced to Notion! |
| Notion fails | |
| Empty text | ❌ Please provide note text. Example: /note Buy milk |
cp .env.example .env
# Edit .env with your tokens
docker-compose up -d --build- Deploy the container:
docker-compose up -d --build- Configure Nginx reverse proxy:
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}- Enable HTTPS (required by Telegram for webhooks):
sudo certbot --nginx -d your-domain.com- Register the webhook pointing to your domain (see Webhook Setup above).
- Install the async PostgreSQL driver:
pip install asyncpg- Update
DATABASE_URLin.env:
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/notes_db
- Re-run migrations:
alembic upgrade headNo code changes required — the engine auto-configures for each backend.
| Variable | Description | Required |
|---|---|---|
TELEGRAM_BOT_TOKEN |
Bot token from @BotFather | ✅ |
TELEGRAM_WEBHOOK_SECRET |
Secret for webhook validation | ✅ |
DATABASE_URL |
SQLAlchemy async connection string | ✅ |
NOTION_TOKEN |
Notion integration token | ✅ |
NOTION_DATABASE_ID |
Target Notion database ID | ✅ |
APP_ENV |
development or production |
❌ |
LOG_LEVEL |
DEBUG, INFO, WARNING, ERROR |
❌ |
MIT