A social media post scheduler with a calendar view, multi-channel composer, queue management, and analytics. Built with Preact + Tailwind CSS + Hono + D1. Deploys to Cloudflare Workers via Clawnify.
Think of it as an open-source alternative to Buffer, Hypefury, Typefully, or Twitter Hunter -- a complete content scheduling system you can self-host and customize.
- Multi-channel composer -- write once, publish to multiple platforms with per-platform character limits
- Calendar view -- month grid showing scheduled posts with platform color dots
- Queue management -- chronological list of scheduled posts with one-click publish
- Drafts -- save unfinished posts and come back to them later
- Channel management -- add your social media accounts with platform detection and color coding
- Labels -- categorize posts with colored labels for organization
- Media attachments -- upload images to posts
- Analytics -- bar charts showing posts per channel, per label, and daily activity
- Dashboard -- at-a-glance stats, upcoming posts, and recent drafts
- Native previews -- see the post as it will look on each selected platform before it goes out
- Direct publishing -- publish to X, LinkedIn, Instagram, Facebook Pages, TikTok and Bluesky through the accounts connected in Clawnify
- URL routing -- bookmarkable pages (
/compose,/calendar,/queue,/drafts,/channels,/analytics)
| Platform | Character Limit | Publishing |
|---|---|---|
| X / Twitter | 280 | text or photo |
| 3,000 | text or photo | |
| 2,200 | photo + caption (Business or Creator account, picked up from the connection) | |
| 63,206 | text or photo, as a Page you manage | |
| TikTok | 2,200 | photo post |
| Bluesky | 300 | text, with an image and link cards |
| Mastodon | 500 | coming soon |
| Threads | 500 | coming soon |
Connect each account once in Clawnify (Settings → Integrations), then add it as a channel here. Facebook channels are Pages: the channel form lists the Pages the connected account manages so you can pick one.
git clone https://github.com/clawnify/OpenPost.git
cd open-post
pnpm installStart the dev server:
pnpm devOpen http://localhost:5173 in your browser. The database schema is applied automatically on startup.
Publishing runs through the accounts connected in Clawnify -- no API keys in the app. Locally there is no credential service, so posts save and schedule but publishing reports each channel as not connected.
| Layer | Technology |
|---|---|
| Frontend | Preact, TypeScript, Tailwind CSS v4, Vite |
| Backend | Hono (Cloudflare Worker) |
| Database | D1 (SQLite at the edge) |
| Icons | Lucide |
| Credentials | Clawnify credential service binding (Composio OAuth) |
- Node.js 20+
- pnpm
src/
server/
index.ts -- Hono API with D1 + credentials middleware
db.ts -- D1-native database adapter
credentials.ts -- Credential service binding adapter (prod + local fallback)
twitter.ts -- X API v2 client (OAuth 2.0 Bearer + OAuth 1.0a)
schema.sql -- Database schema (channels, posts, labels, media)
client/
app.tsx -- Root component with router
context.tsx -- Preact context for app state
hooks/
use-app.ts -- State management + CRUD operations
use-router.ts -- pushState URL router
components/
sidebar.tsx -- Navigation sidebar
dashboard.tsx -- Stats cards + upcoming posts
post-composer.tsx -- Multi-channel post editor with char limits
calendar-view.tsx -- Month grid calendar
queue-view.tsx -- Scheduled posts queue
drafts-view.tsx -- Draft posts list
channel-list.tsx -- Channel management
analytics-view.tsx -- Bar charts and daily activity
post-card.tsx -- Reusable post preview card
error-banner.tsx -- Error display
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/channels |
List channels |
| POST | /api/channels |
Create channel |
| PUT | /api/channels/:id |
Update channel |
| DELETE | /api/channels/:id |
Delete channel |
| POST | /api/channels/:id/sync-profile |
Re-pull the channel's platform profile |
| GET | /api/platforms/facebook/pages |
Pages the connected Facebook account manages (for the channel form) |
| GET | /api/labels |
List labels |
| POST | /api/labels |
Create label |
| PUT | /api/labels/:id |
Update label |
| DELETE | /api/labels/:id |
Delete label |
| GET | /api/posts |
List posts (filterable by status, channel, date range) |
| GET | /api/posts/:id |
Get post with channels, labels, media |
| POST | /api/posts |
Create post |
| PUT | /api/posts/:id |
Update post |
| DELETE | /api/posts/:id |
Delete post |
| GET | /api/posts/calendar?month=YYYY-MM |
Posts grouped by day for calendar |
| POST | /api/posts/:id/publish |
Publish post to assigned channels |
| GET | /api/stats |
Dashboard stats + per-channel/label breakdowns |
In production, the app uses Cloudflare Service Bindings to fetch fresh OAuth tokens from Clawnify's central credential Worker -- zero network hop, no secrets stored on the app.
flowchart LR
app["App Worker"] -->|RPC| cred["CredentialService.getToken<br/>(twitter, orgId)"]
cred --> composio["Composio<br/>(auto-refresh)"]
composio --> token["Fresh Bearer token"]
token --> api["X API v2"]
See credentials-service-binding.md for the full architecture.
npx clawnify deployMIT