Getting started
Introduction
- QuickstartFrom nothing to your first scheduled post, in about 10 minutes.
- Deploy guideThe four values Cloudflare asks for, made here, then the button.
- Writing postsEvery header field, images and videos, the first comment.
- TroubleshootingWhat an error in /status means, and how to fix it.
Write your LinkedIn posts in advance as Markdown files, push, and they go out on your personal profile at their time, through LinkedIn’s official API. It runs on your own Cloudflare account, on the free plan: no server, no subscription, and nobody else ever holds your posts or your token.
How it works
git push ──► Workers Builds ──► D1 migrations ─► deploy the Worker ─► sync posts to D1, media to KV
cron * * * * * ──► Worker: earliest due post ─► LinkedIn (image or video, post, first comment) ─► D1 status
- A post is a Markdown file in
posts/texts/with a small header: date, time,status: ready, optional image or video, optional first comment. - Every push to your copy deploys the Worker and syncs your posts to a D1 database and your media to Workers KV, on your account, through Workers Builds. Only what changed is sent. No GitHub secrets.
- Every minute, the Worker publishes the earliest post whose time has come. A failed try is retried
every minute for 10 minutes (9:00 … 9:10), then marked
failedwith the reason. - Never twice. A post is claimed atomically before it is sent; an unclear answer from LinkedIn is
marked
unknownand never retried; the same text is never posted twice; a posted post is never touched again by a sync. - Your data stays yours. The repository, the database, the media, the token and the LinkedIn app are all on your accounts. The project has no server and no telemetry.
What you need
- A Cloudflare account (free plan, no card needed) and a GitHub (or GitLab) account.
- A LinkedIn app with Share on LinkedIn, and an access token: LinkedIn setup. About 5 minutes; LinkedIn requires a Company Page to create the app (an empty one works); the token lasts 60 days.
Install
The deploy guide gets the four values ready before you press the button.
Three ways, same result (Install):
- The Deploy to Cloudflare button (about 3 minutes). Cloudflare copies this repository into your GitHub, creates
the database and the media store, writes their ids into your copy, asks for your LinkedIn token and
DRY_RUN, and deploys. Every later push to your copy redeploys and syncs. - A coding agent. Paste Install prompt for agents into Claude Code, Cursor or any agent opened in a clone of your private copy.
- By hand.
pnpm install && pnpm run setup.
Then:
- Set your timezone in
scheduler.json("timezone": "Europe/Paris";UTCby default). - Write a post (Writing posts), push.
- Check
GET /statuswith yourADMIN_TOKEN.
For a first test, keep DRY_RUN at "1" in wrangler.jsonc: posts are marked posted with a fake link
and nothing reaches LinkedIn. Set it to "0" and push to post for real.
A post
---
date: 2026-12-01
time: 09:00
status: ready
visibility: PUBLIC
media: images/launch.png
first_comment: The link: https://example.com
---
**The hook, in bold.**
The rest of the post.
Media
| Media | Where it goes | Card needed |
|---|---|---|
Images and videos up to 25 MB, in posts/images/ or posts/videos/ |
Workers KV | no |
| A file already online | media: https://... |
no |
| Videos over 25 MB (LinkedIn accepts up to 200 MB) | R2, optional (Install) | yes, Cloudflare asks for one to enable R2 |
Routes
GET /: health.GET /status: the queue, the history and the token’s expiry (Authorization: Bearer <ADMIN_TOKEN>).POST /check: run the minute check now (same header).
Limits
- The LinkedIn token lasts 60 days and cannot be refreshed automatically outside LinkedIn’s partner
programs.
/statuswarns 7 days beforeLINKEDIN_TOKEN_EXPIRES; renewing takes 2 minutes. - Personal profile only. Company pages need a LinkedIn partner permission.
- To the minute, and at most one post per minute.
Development
pnpm install
cp .dev.vars.example .dev.vars # add DRY_RUN=1: never posts for real
pnpm db:migrate:local && pnpm sync:local
pnpm dev # then: curl "localhost:8787/__scheduled?cron=*+*+*+*+*"
pnpm test && pnpm typecheck
src/core/ holds everything that does not depend on Cloudflare (posts, scheduling rules, LinkedIn);
src/cloudflare/ is the Worker (Hono routes, cron, D1, KV/R2). scripts/ has the sync, the deploy
script and the setup.
v1
The first version (Python, run by cron on your own machine, with image cards) is on the
v1 branch and the v1.0.0 tag.
License
MIT, see LICENSE.