A fully automated developer journal backend — write posts in your Telegram channel and they instantly appear on your website. Built with Express, TypeScript, and Supabase.
You post in Telegram → Bot Webhook → Express API → Supabase → React Frontend
- Post a message with
#hashtags→ saved automatically as a post with tags - Edit a message → post content and tags update instantly
- Delete a message → post is removed from the database
| Layer | Tech |
|---|---|
| Runtime | Node.js 18+ |
| Framework | Express 4 |
| Language | TypeScript 5 |
| Database | Supabase (PostgreSQL) |
| Deployment | Render |
backend/
├── sql/
│ └── schema.sql ← Run in Supabase SQL Editor first
├── src/
│ ├── index.ts ← Express app, CORS, server setup
│ ├── supabaseClient.ts ← Supabase singleton
│ ├── types/
│ │ └── index.ts ← Shared TypeScript interfaces
│ ├── routes/
│ │ ├── posts.ts ← CRUD API for posts
│ │ └── webhook.ts ← Telegram webhook handler
│ ├── scripts/
│ │ └── setWebhook.ts ← One-time script to register bot webhook
│ └── middleware/
│ └── errorHandler.ts ← Global error + 404 handler
├── .env.example
├── package.json
└── tsconfig.json
Go to supabase.com and create a new project.
Supabase Dashboard → SQL Editor → paste sql/schema.sql → Run.
This creates the Post, Tag, and PostTag tables with indexes and RLS policies.
Supabase Dashboard → Settings → API:
- Project URL →
SUPABASE_URL - service_role secret key →
SUPABASE_SERVICE_ROLE_KEY
⚠️ Never expose the service-role key on the frontend — it bypasses Row Level Security.
- Open Telegram → search
@BotFather→ send/newbot - Follow the prompts and copy the bot token
- Add the bot as an Admin to your channel
Forward any message from your channel to @username_to_id_bot — it replies with your channel's numeric ID (e.g. -1001234567890).
cp .env.example .envFill in all values:
# Supabase
SUPABASE_URL=https://your-project-id.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Server
PORT=3000
NODE_ENV=development
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000
# Telegram
TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRstuVWXyz
TELEGRAM_CHANNEL_ID=-1001234567890
TELEGRAM_WEBHOOK_URL=https://your-deployed-app.onrender.comnpm install
npm run dev # development with hot reload
npm run build # compile TypeScript
npm start # run compiled output
⚠️ Your server must be deployed with a public HTTPS URL before this step. For local testing use ngrok:ngrok http 3000
npm run webhook:setYou should see:
✅ Webhook registered: https://your-app.onrender.com/webhook/telegram
📋 Webhook info:
URL : https://your-app.onrender.com/webhook/telegram
Pending updates: 0
Just post in your channel — the bot handles everything automatically.
| What you do in Telegram | What happens |
|---|---|
Post "Learned about RLS today #Supabase #Backend" |
New post saved, tags extracted |
| Edit the message | Post content and tags updated |
| Delete the message | Post removed from database |
Hashtags become filterable tags on your frontend. The # symbols are stripped from the post body automatically.
Returns the latest N posts (default 9, max 50), newest first.
Response 200 OK
[
{
"createdAt": "2025-06-01T00:00:00Z",
"post": "Learned about RLS today",
"date_string": "June 1, 2025",
"date": "2025-06-01",
"post_link": "https://t.me/YourChannel/42",
"PostTag": [
{ "postId": 1, "tagId": 4, "Tag": { "id": 4, "tag": "Supabase" } }
]
}
]Returns a single post by ID.
Creates a new post manually.
{
"post": "Markdown content here",
"post_link": "https://t.me/...",
"date_string": "June 10, 2025",
"date": "2025-06-10",
"tags": ["TypeScript", "React"]
}Updates a post. All fields optional. tags array replaces existing tags.
Deletes a post and its tag links (cascade).
Returns all tags alphabetically.
Telegram webhook endpoint — called automatically by Telegram, not manually.
Health check → { "status": "ok", "timestamp": "..." }
curl -X POST http://localhost:3000/webhook/telegram \
-H "Content-Type: application/json" \
-d '{
"update_id": 123456,
"channel_post": {
"message_id": 42,
"chat": {
"id": -1001234567890,
"type": "channel",
"title": "My Dev Journal",
"username": "TerminalThoughts1"
},
"date": 1717000000,
"text": "Test post from curl #Backend #TypeScript"
}
}'Then verify it was saved:
curl http://localhost:3000/posts?max=9Run in Supabase SQL Editor:
TRUNCATE TABLE "PostTag", "Post", "Tag" RESTART IDENTITY CASCADE;In JounalIntegration.tsx, set BASE_URL to your server address:
export const BASE_URL = "http://localhost:3000"; // dev
// export const BASE_URL = "https://your-app.onrender.com"; // prod- Push to GitHub
- Render Dashboard → New Web Service → connect your repo
- Set build command:
npm install && npm run build - Set start command:
npm start - Add all environment variables from
.env.example - Deploy, then run
npm run webhook:setto register the Telegram webhook
⚠️ Render's free tier spins down after 15 minutes of inactivity. Upgrade to a paid plan or switch to Railway/Fly.io for production use.
- Pagination (
GET /posts?page=1&limit=9) - Full text search (
GET /posts?search=supabase) - Media support (photos/videos from Telegram)
- Post views counter
- Pin a post
- RSS feed (
GET /feed.xml) - Scheduled posts