The Core Problem
Most content creators manage distribution through manual copy-paste. Write a post in a CMS, copy the URL to Twitter, paste the title into a Telegram channel, reformat for Dev.to, write a newsletter version in email. Each platform has different formatting requirements, character limits, and audience expectations.
For a team publishing three posts per week across four channels, that's 12 manual distribution actions per week -- roughly 2 hours of pure overhead. In practice, most teams give up and only distribute to one channel (usually the blog), leaving massive reach on the table.
AIKit solves this with a serverless multi-channel distribution engine. Every post published to the AIKit blog is automatically distributed to Telegram, Dev.to (as a draft), and a newsletter queue -- all from a single D1 database insert. No manual steps. No copy-paste. No platform switching.
Architecture: Event-Driven Distribution
The distribution engine follows an event-driven architecture triggered by database changes:
```
D1 ec_posts insert → Workers Hooks → Distribution Pipeline
├─ Channel 1: Telegram (send_message API)
├─ Channel 2: Dev.to (Forem REST API)
├─ Channel 3: Newsletter queue (KV-based)
└─ Channel 4: Blog auto-live (same D1 insert)
```
The pipeline is built on three serverless primitives:
1. D1 as the Event Source
Every blog post insert into the `ec_posts` table is the single source of truth. The distribution pipeline polls D1 every 15 minutes for newly published posts (where `published_at > last_check`):
```typescript
interface DistributionCheck {
lastCheck: Date;
postsToDistribute: Post[];
}
async function findNewPosts(db: D1Database, lastCheck: string): Promise<Post[]> {
const result = await db.prepare(
`SELECT id, title, excerpt, content, slug
FROM ec_posts
WHERE status = 'published' AND published_at > ?
ORDER BY published_at ASC`
).bind(lastCheck).all<Post>();
return result.results || [];
}
```
2. KV as Distribution State
Cloudflare KV stores the distribution state for each post and channel:
```typescript
interface DistributionState {
postId: string;
channels: {
telegram: { status: 'pending' | 'sent' | 'failed'; messageId?: number };
devto: { status: 'pending' | 'sent' | 'failed'; articleId?: number };
newsletter: { status: 'pending' | 'queued' | 'sent'; emailId?: string };
};
createdAt: string;
distributedAt?: string;
}
```
Storing distribution state in KV (not D1) avoids write contention on the main database and provides low-latency status checks for the admin dashboard.
3. Workers as Distribution Executors
Each channel has its own Worker function, allowing independent retry policies and failure isolation:
```typescript
async function distributeToTelegram(post: Post): Promise<DistributionResult> {
const message = `📝 **${post.title}**
${post.excerpt?.slice(0, 120)}...
🔗 https://ai-kit.net/blog/${post.slug}`;
const response = await fetch('https://api.telegram.org/bot' + BOT_TOKEN + '/sendMessage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: HOME_CHAT_ID, text: message, parse_mode: 'Markdown' })
});
const result = await response.json();
return result.ok ?
{ status: 'sent', messageId: result.result.message_id } :
{ status: 'failed', error: result.description };
}
```
Each channel handler is independently deployable. If Telegram is down, Dev.to distribution still happens.
Channel 1: Telegram Home Channel
Telegram is the primary distribution channel because it has the highest engagement rate (35-50% open rate vs 5-15% for newsletters). The distribution format is optimized for Telegram's Markdown subset:
```
📝 **Serverless Multi-Channel Distribution: How AIKit Publishes From One Queue**
Every blog post is automatically distributed to Telegram, Dev.to, and newsletter — zero manual work. Architecture deep-dive inside.
🔗 https://ai-kit.net/blog/serverless-multi-channel-distribution
```
The excerpt is truncated to 120 characters to fit in Telegram's preview without cutoff. A clean link with the full URL ensures click-through tracking.
Channel 2: Dev.to Cross-Post (Draft Mode)
Dev.to posts are created as drafts (not published live). This gives the admin 24 hours to review and adjust formatting before the post goes public on Dev.to:
```typescript
async function distributeToDevto(post: Post): Promise<DistributionResult> {
const frontMatter = ``;
const response = await fetch('https://dev.to/api/articles', {
method: 'POST',
headers: {
'api-key': DEVTO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
article: {
title: post.title,
body_markdown: frontMatter + '
' + post.content,
published: false,
tags: mapCategoryToDevtoTags(post.category),
canonical_url: `https://ai-kit.net/blog/${post.slug}`
}
})
});
return response.ok ?
{ status: 'sent', articleId: (await response.json()).id } :
{ status: 'failed', error: await response.text() };
}
```
The `canonical_url` field is the critical SEO element. It tells Google that the ai-kit.net version is the original, preventing duplicate content penalties.
Channel 3: Newsletter Queue
Newsletter distribution is the most delayed channel because it requires accumulation. Posts are placed in a KV-based newsletter queue and batched into a weekly digest:
```typescript
async function queueForNewsletter(post: Post): Promise<void> {
const weekKey = `newsletter:queue:${getCurrentWeekKey()}`;
const queue = JSON.parse(await KV_NAMESPACE.get(weekKey) || '[]');
queue.push({
title: post.title,
slug: post.slug,
excerpt: post.excerpt,
publishedAt: post.publishedAt
});
await KV_NAMESPACE.put(weekKey, JSON.stringify(queue));
}
```
Every Monday at 8 AM CT, a cron Worker builds the weekly digest HTML from the queued posts and sends it via Gmail SMTP relay.
Results: Distribution Metrics
Since implementing automated multi-channel distribution:
- **236 posts distributed** automatically, zero manual actions
- **Telegram home channel**: 4x engagement vs. blog-only distribution
- **Dev.to drafts**: 85% review-to-publish rate (admin approves most drafts as-is)
- **Newsletter queue**: Building toward first weekly digest
- **Time saved**: ~2 hours/week in manual copy-paste eliminated
Key Takeaways
- Treating D1 as the single event source eliminates sync problems between channels
- KV as distribution state storage provides low-latency status tracking without D1 write load
- Each channel handler is independently deployable and retryable -- failure isolation at the Worker level
- Dev.to drafts with canonical URLs preserve SEO authority while extending reach
- The same architecture pattern scales to additional channels: LinkedIn articles, Medium, Substack
- Serverless means zero infrastructure costs -- each Worker invocation costs fractions of a cent