Multi-platform social media scheduling for creators and small teams.
How it works · Installation · Platforms · Contributing
| Package | Description |
|---|---|
apps/chronex-client |
Next.js 16 app — auth, tRPC, media upload, workspace UI, platform connection flows |
apps/chronex-worker |
Cloudflare Worker — consumes queue jobs, publishes content, updates post status |
packages/db |
Shared Drizzle schema and Neon/Postgres client |
User signs in → creates workspace
↓
Platform tokens stored per workspace in DB
↓
Media uploaded to Backblaze B2 → metadata saved to Neon
↓
Post created → one `posts` row + one `platform_posts` row per target platform
↓
If post is within 12h → pushed directly to Cloudflare Queues
↓
Cron sweep runs every 12h → enqueues anything pending in next window
↓
Worker consumes messages → dispatches correct platform handler
- Node.js 20+
- pnpm 10+
- Postgres database (Neon recommended)
- Backblaze B2 bucket
- Cloudflare account with Queues + Workers enabled
- OAuth apps / bots for each platform you want to connect
git clone https://github.com/prncexe/chronex.git
cd chronex
pnpm installChronex uses three separate env surfaces:
A. Client env — apps/chronex-client/.env
cp apps/chronex-client/.env.example apps/chronex-client/.envKey groups to fill in:
| Group | Variables |
|---|---|
| Database | DATABASE_URL |
| Better Auth | BETTER_AUTH_SECRET, BETTER_AUTH_URL |
| App URL | NEXT_PUBLIC_APP_URL |
| Backblaze B2 | B2_KEY_ID, B2_APP_KEY, B2_BUCKET_ID, B2_DOWNLOAD_URL |
| Cloudflare | CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_QUEUE_ID, CLOUDFLARE_API_TOKEN |
| Platforms | GitHub, Google, Instagram, Threads, LinkedIn, Slack, Discord, Telegram |
B. Worker env — apps/chronex-worker/.dev.vars
cp apps/chronex-worker/.dev.vars.example apps/chronex-worker/.dev.varsRequired locally:
DATABASE_URL
DISCORD_BOT_TOKEN
DISCORD_WEBHOOK_NAME
DISCORD_WEBHOOK_AVATAR_URL
B2_KEY_ID
B2_APP_KEY
B2_BUCKET_ID
B2_DOWNLOAD_URL
C. DB / Drizzle env — packages/db/.env
cp packages/db/.env.example packages/db/.envSet DATABASE_URL. This is what Drizzle reads when running schema commands from the shared DB package.
pnpm db:pushOr with migrations:
pnpm db:generate
pnpm db:migrate# Both client + worker
pnpm dev
# Client only
pnpm dev:web
# Worker only
pnpm dev:workerConfigure your platform apps to use these callback URLs:
| Platform | Local | Production |
|---|---|---|
http://localhost:3000/instagram |
https://your-domain.com/instagram |
|
| Threads | http://localhost:3000/threads |
https://your-domain.com/threads |
http://localhost:3000/linkedin |
https://your-domain.com/linkedin |
|
| Slack | http://localhost:3000/slack |
https://your-domain.com/slack |
| Discord | http://localhost:3000/discord |
https://your-domain.com/discord |
| Telegram | http://localhost:3000/api/oauth/telegram |
https://your-domain.com/api/oauth/telegram |
For production, update
BETTER_AUTH_URL,NEXT_PUBLIC_APP_URL, and everyNEXT_PUBLIC_*_REDIRECT_URI.
Chronex stores all uploaded media in B2 and generates authorized download URLs for both the app and worker.
B2_KEY_ID
B2_APP_KEY
B2_BUCKET_ID
B2_DOWNLOAD_URL
Replace the placeholders in scripts/backblaze/cors-rules.json:
YOUR_ACCOUNT_ID
YOUR_BUCKET_ID
https://your-app-domain.com
http://localhost:3000
Then apply it:
# Linux/macOS
./scripts/backblaze/apply-cors.sh YOUR_B2_ACCOUNT_AUTH_TOKEN
# Windows
./scripts/backblaze/apply-cors.ps1 -AuthorizationToken YOUR_B2_ACCOUNT_AUTH_TOKENOr manually via curl:
curl https://api.backblazeb2.com/b2api/v2/b2_update_bucket \
-H "Authorization: YOUR_B2_ACCOUNT_AUTH_TOKEN" \
-H "Content-Type: application/json" \
--data-binary "@scripts/backblaze/cors-rules.json"Create both queues in Cloudflare:
chronex-platform-jobschronex-dlq
The worker expects:
- Producer binding:
CHRONEX_QUEUE_PRODUCER - Consumer queue:
chronex-platform-jobs - Dead letter queue:
chronex-dlq - Cron trigger: every 12 hours
pnpm --filter worker deploycd apps/chronex-worker
wrangler secret put DATABASE_URL
wrangler secret put DISCORD_BOT_TOKEN
wrangler secret put B2_KEY_ID
wrangler secret put B2_APP_KEY
wrangler secret put B2_BUCKET_ID
wrangler secret put B2_DOWNLOAD_URL
DISCORD_WEBHOOK_NAMEandDISCORD_WEBHOOK_AVATAR_URLcan be secrets or plain vars depending on your preference.
pnpm --filter worker tail
curl https://YOUR_WORKER_URL/healthTelegram has a different connection flow from the other OAuth providers:
- Chronex registers a webhook at
/api/oauth/telegram - The bot token is stored in the auth token table for the workspace
- Users connect a Telegram group or channel by sending a generated registration code in that target chat
- Private chats with the bot are not used as publish destinations
- Telegram channel add links require admin permissions; Chronex requests
post_messagesfor channel setup
Required env vars:
TELEGRAM_BOT_TOKEN
NEXT_PUBLIC_TELEGRAM_BOT_USERNAME
TELEGRAM_WEBHOOK_SECRET
pnpm dev # Start client + worker
pnpm dev:web # Start client only
pnpm dev:worker # Start worker only
pnpm build # Build all packages
pnpm format # Format codebase
pnpm format:check # Check formatting
pnpm db:push # Push schema to DB
pnpm db:studio # Open Drizzle StudioSee CONTRIBUTING.md.
- Fork the repo
- Create a feature branch
- Update docs/examples if setup behavior changes
- Open a PR with a clear summary — include screenshots for UI changes
MIT — use it however you like.