Vibecode Typefully
track this build6 phases, 15 steps, beginner friendly0%For personal thread drafting and scheduling, the core loop is vibecodable; the caveat is social API access, analytics, and platform-policy upkeep.
You are building a lean indie version of Typefully.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Typefully · indie build
A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | drafts, queue and scheduler loop in one process |
| Counting | twitter-text (config v3) | the only correct way to count 280 weighted characters |
| Publishing | X API v2, optional, behind an interface | pay-per-use pricing means the fallback comes first |
| Notifications | ntfy or desktop notifications | a reminder at slot time is the free publishing path |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **Your timezone, written as an IANA name** · free
- Why: The scheduler stores UTC and renders in your zone. Getting this wrong posts at the wrong hour twice a year.
- Get it: Look it up: Europe/London, America/New_York, Asia/Karachi. Put it in .env as TIMEZONE.
- [ ] **Your posting slots** · free
- Why: Phase 3 fills slots you define, such as weekdays at 09:00 and 16:00.
- Get it: Write them as a JSON file: days and times in your timezone.
- [ ] **An ntfy topic (or another notification channel)** · free
- Why: The free publishing path is a notification at slot time. ntfy needs no account: pick a long random topic name.
- Get it: Install the ntfy app on your phone, subscribe to a topic like posts-<random>, and put the topic in .env. Test with curl -d 'hello' ntfy.sh/<topic>.
- [ ] **An X developer account with pay-per-use billing (optional)** (optional) · pay per use, about $0.015 per post, $0.20 with a link
- Why: Only needed for automatic posting. As of 2026 there is no free tier for new developers: roughly $0.015 per post and about $0.20 per post containing a link. A link-heavy habit costs more than Typefully.
- Get it: developer.x.com > sign in > create a project and app > User authentication settings with Read and Write > generate API Key, API Secret, Access Token and Access Token Secret. Add a payment method for pay-per-use. Verify the current prices in the console; they have changed repeatedly.
- [ ] **A small always-on server (VPS)** (optional) · about $5 a month
- Why: This needs one process running all the time with a public address.
- Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first.
- [ ] **Caddy on the server** (optional) · free
- Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work.
- Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config.
- Verify: caddy version prints a version on the server
## Quick start
```sh
mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module
mkdir -p data/media && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- LinkedIn and other networks in v1. Each is its own API, auth model and content format.
- Analytics on published posts: reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
- posting without paying X for API access
- multi-account and team features
- analytics on published posts
- AI rewrite suggestions
If one of those is essential to you, that is the reason to keep paying for Typefully, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Typefully
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a post composer and scheduler like Typefully. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase. Read the
Phase 4 pricing note before you choose to build Phase 4 at all.
### Stack (fixed, do not substitute)
- Node 22 with `node:http` and `node:sqlite`. No Express, no framework.
- `twitter-text` for character counting. Do not hand-roll it, see Phase 2.
- Server-rendered HTML with small islands of vanilla JS for the editor.
- One process: web server and scheduler loop together.
### Data model (create this before Phase 1)
- `drafts`: id, title, created_at, updated_at, status ('draft' | 'queued' |
'posted' | 'failed')
- `posts`: id, draft_id, position, body, media (JSON array of paths)
- `queue`: id, draft_id, scheduled_for, attempts, last_error, posted_at,
remote_ids (JSON)
- Store `scheduled_for` as UTC epoch milliseconds and render in the user's
timezone from a `.env` setting. A scheduler that stores local time will post at
the wrong hour twice a year, and you will not notice for months.
### Phase 1 · Composer and drafts
Build: the writing surface · a distraction-free editor where `---` on its own
line splits a thread into posts, showing each post as a separate card. Autosave
to SQLite on a debounce, and keep a small revision history so a bad autosave is
recoverable. Reorder posts by drag. No character counting yet.
Done when: typing splits and re-joins threads correctly as `---` is added and
removed, a browser refresh loses nothing, and reordering persists.
Do not build yet: counting, queue, publishing.
### Phase 2 · Correct character counting
Build: per-post counts using `twitter-text` with config v3, not `string.length`.
The rules that make a naive counter wrong:
- The limit is 280 weighted characters, not 280 code points.
- Every URL counts as 23 characters regardless of its real length, because all
links are wrapped by t.co.
- CJK characters and emoji count as 2.
Show the weighted count, mark over-limit posts clearly, and block queueing a
draft that contains one.
Done when: a post with a 100-character URL counts that URL as 23, an emoji counts
as 2, a 280-weighted-character post is accepted and a 281 is refused, and the
counts match X's own counter on a handful of pasted real examples.
### Phase 3 · Queue and calendar
Build: slots defined in a config file (for example weekdays at 09:00 and 16:00),
a "next free slot" action, an explicit datetime picker, and a calendar view
grouped by day showing queued drafts. Prevent double-booking one slot.
Done when: queueing three drafts into two daily slots spreads them across two
days, "next free slot" never returns a past time, and a slot already taken is not
offered again.
### Phase 4 · Publishing, and the honest cost
Build: the scheduler loop, running every minute, publishing due items via the X
API v2 with keys from `.env`, threading each post as a reply to the previous one,
recording remote ids, and marking failures with the API error. Retry a transient
failure three times with backoff; never retry a rejected post, or you will
publish it four times.
Before building this, read and repeat in the README what it costs. As of 2026 X
has no usable free API tier: new developers get pay-per-use at roughly $0.015 per
post and about $0.20 per post containing a link. A link-free posting habit is a
few dollars a month, but a marketer posting links daily is paying more than
Typefully's subscription for a worse product. Verify the current numbers in the
developer console before relying on them · this pricing has changed repeatedly.
Because of that, build the fallback first and treat the API as optional:
- A "copy thread to clipboard" button that copies the whole thread formatted for
manual posting.
- A desktop notification at slot time reminding you to post it.
- The app must be fully useful with no API key configured at all.
Done when: with no API key set, a due item fires a notification and the copy
button yields the complete thread; with a key set, a two-post thread publishes as
a genuine reply chain; and a rejected post is marked failed exactly once with the
API's error text visible.
### Phase 5 · Media
Build: image attachments per post, stored on disk, resized and stripped of EXIF
before upload, respecting the platform limit of four images per post.
Done when: a post with four images publishes or copies correctly, a fifth is
refused in the UI, and no uploaded file retains GPS EXIF.
### Phase 6 · Deploy
Build: a `/healthz` endpoint, a nightly backup of the database and media, a
systemd unit, and the README.
Done when: a restart leaves the queue intact and nothing due-while-down is lost ·
overdue items publish or notify on the next loop rather than being skipped.
### Out of scope (and why)
- LinkedIn and other networks in v1. Each is its own API, auth model and content
format, and doing two badly is worse than one properly.
- Analytics on published posts · reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
### README must contain
- The current X API cost per post and per link post, with the date checked and a
link to the developer console.
- A plain sentence: if you post links daily, this build costs more to run than
the subscription it replaces. Use the clipboard fallback instead.
- The timezone setting, and a warning to verify it before the first scheduled run.
===== AGENTS.md =====
# Agent instructions · Typefully indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, twitter-text (config v3), X API v2, optional, behind an interface, ntfy or desktop notifications. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
## Known traps
- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
===== BUILD_PLAN.md =====
# Build plan · Typefully
A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Composer and drafts
Write threads, split them with ---, never lose a word.
### Steps
1. Create the project and the drafts, posts and revisions tables
drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at).
```sh
mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module
mkdir -p data/media && cp .env.example .env
```
2. Build the editor page
A distraction-free textarea; --- on its own line splits into post cards rendered beside it. Autosave on a debounce, keeping a small revision history.
3. Add drag to reorder posts within a thread
### Done when
- [ ] Adding and removing --- splits and re-joins correctly
- [ ] A browser refresh loses nothing
- [ ] Reordering persists
- [ ] A previous revision can be restored
## Phase 2 · Correct character counting
Counts that match X exactly.
### Steps
1. Install twitter-text and count with config v3
280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length.
```sh
npm install twitter-text@3
```
2. Show the weighted count per post and block queueing an over-limit thread
### Done when
- [ ] A post with a 100-character URL counts it as 23
- [ ] An emoji counts as 2
- [ ] A 280-weighted post is accepted and 281 is refused
- [ ] Counts match X's own composer on pasted real examples
## Phase 3 · Queue and calendar
Slots from your file, next-free-slot, a calendar by day, no double booking.
### Steps
1. Create the queue table and load SLOTS_FILE
queue (id, draft_id, scheduled_for UTC ms, attempts, last_error, posted_at, remote_ids JSON). Convert slots from TIMEZONE to UTC when scheduling.
2. Add next free slot, an explicit picker, and a calendar view grouped by day
### Done when
- [ ] Three drafts into two daily slots spread across two days
- [ ] Next free slot never returns a past time
- [ ] A taken slot is not offered again
- [ ] A slot at 09:00 local shows as 09:00 across a DST change
## Phase 4 · Publishing, fallback first
Useful with no API key at all; automatic posting only when configured, with the cost stated.
### Steps
1. Build the clipboard fallback
A copy-thread button that copies the whole thread formatted for manual posting.
2. Send an ntfy notification at slot time
The scheduler loop, every minute: for each due item with no API key, post to ntfy.sh/NTFY_TOPIC with the first line and a link to the draft.
```sh
curl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC
```
3. Add the X API publisher behind an interface
OAuth 1.0a user context. Post each item as a reply to the previous one; record remote ids; three retries on transient errors, never on a rejected post.
4. Show the running cost
Count published posts and link posts this month and estimate the bill from the prices you verified.
### Done when
- [ ] With no key set, a due item fires a notification and the copy button yields the full thread
- [ ] With a key set, a two-post thread publishes as a real reply chain
- [ ] A rejected post is marked failed exactly once with the API's error visible
- [ ] Overdue items after a restart publish or notify on the next loop
### Watch out
- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
## Phase 5 · Media
Up to four images per post, resized and stripped.
### Steps
1. Attach images per post with sharp: resize, strip EXIF, store in MEDIA_DIR
```sh
npm install sharp@0.35.3
```
2. Enforce four per post and upload media before posting when the API path is on
### Done when
- [ ] A four-image post publishes or copies correctly
- [ ] A fifth is refused in the UI
- [ ] No stored file retains GPS EXIF
## Phase 6 · Deploy
Live, backed up, queue survives restarts.
### Steps
1. Add /healthz, systemd, Caddy and a backup of data/
Files: `deploy/composer.service`, `Caddyfile`
2. Write the README
The X cost per post and per link post with the date checked and a link to the console; the sentence that daily link posting costs more than the subscription; the timezone check.
Files: `README.md`
### Done when
- [ ] A restart leaves the queue intact and nothing due-while-down is lost
- [ ] The README states the current X prices with a date
## Not in this build
- LinkedIn and other networks in v1. Each is its own API, auth model and content format.
- Analytics on published posts: reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
## After v1, if you want it
- Bluesky as a second publisher: its API is free and the interface is already there
- A browser bookmarklet that opens the composer with a quoted link
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Any free port.
PORT=3000
# Required. SQLite file.
DATABASE_PATH=./data/posts.db
# Required. IANA zone name. Verify before the first scheduled run.
TIMEZONE=Europe/London
# Required. Your posting slots.
SLOTS_FILE=./slots.json
# Required. Attached images, resized and stripped of EXIF.
MEDIA_DIR=./data/media
# Optional. The topic you subscribed to in the ntfy app.
NTFY_TOPIC=posts-a8f3k2
# Optional · secret. X developer portal, only if you publish automatically.
X_API_KEY=...
# Optional · secret. X developer portal.
X_API_SECRET=...
# Optional · secret. X developer portal, generated with Read and Write.
X_ACCESS_TOKEN=...
# Optional · secret. X developer portal.
X_ACCESS_SECRET=...
# Required. Any username for the basic-auth admin pages.
ADMIN_USER=admin
# Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password.
ADMIN_PASS=change-me-to-a-long-random-string
You are building a lean indie version of Typefully.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Typefully · indie build
A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | drafts, queue and scheduler loop in one process |
| Counting | twitter-text (config v3) | the only correct way to count 280 weighted characters |
| Publishing | X API v2, optional, behind an interface | pay-per-use pricing means the fallback comes first |
| Notifications | ntfy or desktop notifications | a reminder at slot time is the free publishing path |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **Your timezone, written as an IANA name** · free
- Why: The scheduler stores UTC and renders in your zone. Getting this wrong posts at the wrong hour twice a year.
- Get it: Look it up: Europe/London, America/New_York, Asia/Karachi. Put it in .env as TIMEZONE.
- [ ] **Your posting slots** · free
- Why: Phase 3 fills slots you define, such as weekdays at 09:00 and 16:00.
- Get it: Write them as a JSON file: days and times in your timezone.
- [ ] **An ntfy topic (or another notification channel)** · free
- Why: The free publishing path is a notification at slot time. ntfy needs no account: pick a long random topic name.
- Get it: Install the ntfy app on your phone, subscribe to a topic like posts-<random>, and put the topic in .env. Test with curl -d 'hello' ntfy.sh/<topic>.
- [ ] **An X developer account with pay-per-use billing (optional)** (optional) · pay per use, about $0.015 per post, $0.20 with a link
- Why: Only needed for automatic posting. As of 2026 there is no free tier for new developers: roughly $0.015 per post and about $0.20 per post containing a link. A link-heavy habit costs more than Typefully.
- Get it: developer.x.com > sign in > create a project and app > User authentication settings with Read and Write > generate API Key, API Secret, Access Token and Access Token Secret. Add a payment method for pay-per-use. Verify the current prices in the console; they have changed repeatedly.
- [ ] **A small always-on server (VPS)** (optional) · about $5 a month
- Why: This needs one process running all the time with a public address.
- Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first.
- [ ] **Caddy on the server** (optional) · free
- Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work.
- Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config.
- Verify: caddy version prints a version on the server
## Quick start
```sh
mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module
mkdir -p data/media && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- LinkedIn and other networks in v1. Each is its own API, auth model and content format.
- Analytics on published posts: reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
- posting without paying X for API access
- multi-account and team features
- analytics on published posts
- AI rewrite suggestions
If one of those is essential to you, that is the reason to keep paying for Typefully, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Typefully
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a post composer and scheduler like Typefully. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase. Read the
Phase 4 pricing note before you choose to build Phase 4 at all.
### Stack (fixed, do not substitute)
- Node 22 with `node:http` and `node:sqlite`. No Express, no framework.
- `twitter-text` for character counting. Do not hand-roll it, see Phase 2.
- Server-rendered HTML with small islands of vanilla JS for the editor.
- One process: web server and scheduler loop together.
### Data model (create this before Phase 1)
- `drafts`: id, title, created_at, updated_at, status ('draft' | 'queued' |
'posted' | 'failed')
- `posts`: id, draft_id, position, body, media (JSON array of paths)
- `queue`: id, draft_id, scheduled_for, attempts, last_error, posted_at,
remote_ids (JSON)
- Store `scheduled_for` as UTC epoch milliseconds and render in the user's
timezone from a `.env` setting. A scheduler that stores local time will post at
the wrong hour twice a year, and you will not notice for months.
### Phase 1 · Composer and drafts
Build: the writing surface · a distraction-free editor where `---` on its own
line splits a thread into posts, showing each post as a separate card. Autosave
to SQLite on a debounce, and keep a small revision history so a bad autosave is
recoverable. Reorder posts by drag. No character counting yet.
Done when: typing splits and re-joins threads correctly as `---` is added and
removed, a browser refresh loses nothing, and reordering persists.
Do not build yet: counting, queue, publishing.
### Phase 2 · Correct character counting
Build: per-post counts using `twitter-text` with config v3, not `string.length`.
The rules that make a naive counter wrong:
- The limit is 280 weighted characters, not 280 code points.
- Every URL counts as 23 characters regardless of its real length, because all
links are wrapped by t.co.
- CJK characters and emoji count as 2.
Show the weighted count, mark over-limit posts clearly, and block queueing a
draft that contains one.
Done when: a post with a 100-character URL counts that URL as 23, an emoji counts
as 2, a 280-weighted-character post is accepted and a 281 is refused, and the
counts match X's own counter on a handful of pasted real examples.
### Phase 3 · Queue and calendar
Build: slots defined in a config file (for example weekdays at 09:00 and 16:00),
a "next free slot" action, an explicit datetime picker, and a calendar view
grouped by day showing queued drafts. Prevent double-booking one slot.
Done when: queueing three drafts into two daily slots spreads them across two
days, "next free slot" never returns a past time, and a slot already taken is not
offered again.
### Phase 4 · Publishing, and the honest cost
Build: the scheduler loop, running every minute, publishing due items via the X
API v2 with keys from `.env`, threading each post as a reply to the previous one,
recording remote ids, and marking failures with the API error. Retry a transient
failure three times with backoff; never retry a rejected post, or you will
publish it four times.
Before building this, read and repeat in the README what it costs. As of 2026 X
has no usable free API tier: new developers get pay-per-use at roughly $0.015 per
post and about $0.20 per post containing a link. A link-free posting habit is a
few dollars a month, but a marketer posting links daily is paying more than
Typefully's subscription for a worse product. Verify the current numbers in the
developer console before relying on them · this pricing has changed repeatedly.
Because of that, build the fallback first and treat the API as optional:
- A "copy thread to clipboard" button that copies the whole thread formatted for
manual posting.
- A desktop notification at slot time reminding you to post it.
- The app must be fully useful with no API key configured at all.
Done when: with no API key set, a due item fires a notification and the copy
button yields the complete thread; with a key set, a two-post thread publishes as
a genuine reply chain; and a rejected post is marked failed exactly once with the
API's error text visible.
### Phase 5 · Media
Build: image attachments per post, stored on disk, resized and stripped of EXIF
before upload, respecting the platform limit of four images per post.
Done when: a post with four images publishes or copies correctly, a fifth is
refused in the UI, and no uploaded file retains GPS EXIF.
### Phase 6 · Deploy
Build: a `/healthz` endpoint, a nightly backup of the database and media, a
systemd unit, and the README.
Done when: a restart leaves the queue intact and nothing due-while-down is lost ·
overdue items publish or notify on the next loop rather than being skipped.
### Out of scope (and why)
- LinkedIn and other networks in v1. Each is its own API, auth model and content
format, and doing two badly is worse than one properly.
- Analytics on published posts · reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
### README must contain
- The current X API cost per post and per link post, with the date checked and a
link to the developer console.
- A plain sentence: if you post links daily, this build costs more to run than
the subscription it replaces. Use the clipboard fallback instead.
- The timezone setting, and a warning to verify it before the first scheduled run.
===== AGENTS.md =====
# Agent instructions · Typefully indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, twitter-text (config v3), X API v2, optional, behind an interface, ntfy or desktop notifications. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
## Known traps
- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
===== BUILD_PLAN.md =====
# Build plan · Typefully
A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Composer and drafts
Write threads, split them with ---, never lose a word.
### Steps
1. Create the project and the drafts, posts and revisions tables
drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at).
```sh
mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module
mkdir -p data/media && cp .env.example .env
```
2. Build the editor page
A distraction-free textarea; --- on its own line splits into post cards rendered beside it. Autosave on a debounce, keeping a small revision history.
3. Add drag to reorder posts within a thread
### Done when
- [ ] Adding and removing --- splits and re-joins correctly
- [ ] A browser refresh loses nothing
- [ ] Reordering persists
- [ ] A previous revision can be restored
## Phase 2 · Correct character counting
Counts that match X exactly.
### Steps
1. Install twitter-text and count with config v3
280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length.
```sh
npm install twitter-text@3
```
2. Show the weighted count per post and block queueing an over-limit thread
### Done when
- [ ] A post with a 100-character URL counts it as 23
- [ ] An emoji counts as 2
- [ ] A 280-weighted post is accepted and 281 is refused
- [ ] Counts match X's own composer on pasted real examples
## Phase 3 · Queue and calendar
Slots from your file, next-free-slot, a calendar by day, no double booking.
### Steps
1. Create the queue table and load SLOTS_FILE
queue (id, draft_id, scheduled_for UTC ms, attempts, last_error, posted_at, remote_ids JSON). Convert slots from TIMEZONE to UTC when scheduling.
2. Add next free slot, an explicit picker, and a calendar view grouped by day
### Done when
- [ ] Three drafts into two daily slots spread across two days
- [ ] Next free slot never returns a past time
- [ ] A taken slot is not offered again
- [ ] A slot at 09:00 local shows as 09:00 across a DST change
## Phase 4 · Publishing, fallback first
Useful with no API key at all; automatic posting only when configured, with the cost stated.
### Steps
1. Build the clipboard fallback
A copy-thread button that copies the whole thread formatted for manual posting.
2. Send an ntfy notification at slot time
The scheduler loop, every minute: for each due item with no API key, post to ntfy.sh/NTFY_TOPIC with the first line and a link to the draft.
```sh
curl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC
```
3. Add the X API publisher behind an interface
OAuth 1.0a user context. Post each item as a reply to the previous one; record remote ids; three retries on transient errors, never on a rejected post.
4. Show the running cost
Count published posts and link posts this month and estimate the bill from the prices you verified.
### Done when
- [ ] With no key set, a due item fires a notification and the copy button yields the full thread
- [ ] With a key set, a two-post thread publishes as a real reply chain
- [ ] A rejected post is marked failed exactly once with the API's error visible
- [ ] Overdue items after a restart publish or notify on the next loop
### Watch out
- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
## Phase 5 · Media
Up to four images per post, resized and stripped.
### Steps
1. Attach images per post with sharp: resize, strip EXIF, store in MEDIA_DIR
```sh
npm install sharp@0.35.3
```
2. Enforce four per post and upload media before posting when the API path is on
### Done when
- [ ] A four-image post publishes or copies correctly
- [ ] A fifth is refused in the UI
- [ ] No stored file retains GPS EXIF
## Phase 6 · Deploy
Live, backed up, queue survives restarts.
### Steps
1. Add /healthz, systemd, Caddy and a backup of data/
Files: `deploy/composer.service`, `Caddyfile`
2. Write the README
The X cost per post and per link post with the date checked and a link to the console; the sentence that daily link posting costs more than the subscription; the timezone check.
Files: `README.md`
### Done when
- [ ] A restart leaves the queue intact and nothing due-while-down is lost
- [ ] The README states the current X prices with a date
## Not in this build
- LinkedIn and other networks in v1. Each is its own API, auth model and content format.
- Analytics on published posts: reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
## After v1, if you want it
- Bluesky as a second publisher: its API is free and the interface is already there
- A browser bookmarklet that opens the composer with a quoted link
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Any free port.
PORT=3000
# Required. SQLite file.
DATABASE_PATH=./data/posts.db
# Required. IANA zone name. Verify before the first scheduled run.
TIMEZONE=Europe/London
# Required. Your posting slots.
SLOTS_FILE=./slots.json
# Required. Attached images, resized and stripped of EXIF.
MEDIA_DIR=./data/media
# Optional. The topic you subscribed to in the ntfy app.
NTFY_TOPIC=posts-a8f3k2
# Optional · secret. X developer portal, only if you publish automatically.
X_API_KEY=...
# Optional · secret. X developer portal.
X_API_SECRET=...
# Optional · secret. X developer portal, generated with Read and Write.
X_ACCESS_TOKEN=...
# Optional · secret. X developer portal.
X_ACCESS_SECRET=...
# Required. Any username for the basic-auth admin pages.
ADMIN_USER=admin
# Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password.
ADMIN_PASS=change-me-to-a-long-random-string
You are building a production product version of Typefully.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# Typefully · product brief
## Problem
For personal thread drafting and scheduling, the core loop is vibecodable; the caveat is social API access, analytics, and platform-policy upkeep.
## Product outcome
A scheduler that is honest about what publishing costs, works fully without paying X, and can be extended to other networks behind the same publisher interface.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- X/LinkedIn API access
- hosted scheduler
- database
- auth
- optional LLM API
## Explicit non-goals for v1
- LinkedIn and other networks in v1. Each is its own API, auth model and content format.
- Analytics on published posts: reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
- posting without paying X for API access
- multi-account and team features
- analytics on published posts
- AI rewrite suggestions
## Success criteria
- Works end to end with no X key configured
- Character counts verified against X's composer on ten examples
- One restore drill performed and dated
- README states the X costs with a date
===== BRIEF.md =====
# Build brief · Typefully
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a post composer and scheduler like Typefully. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase. Read the
Phase 4 pricing note before you choose to build Phase 4 at all.
### Stack (fixed, do not substitute)
- Node 22 with `node:http` and `node:sqlite`. No Express, no framework.
- `twitter-text` for character counting. Do not hand-roll it, see Phase 2.
- Server-rendered HTML with small islands of vanilla JS for the editor.
- One process: web server and scheduler loop together.
### Data model (create this before Phase 1)
- `drafts`: id, title, created_at, updated_at, status ('draft' | 'queued' |
'posted' | 'failed')
- `posts`: id, draft_id, position, body, media (JSON array of paths)
- `queue`: id, draft_id, scheduled_for, attempts, last_error, posted_at,
remote_ids (JSON)
- Store `scheduled_for` as UTC epoch milliseconds and render in the user's
timezone from a `.env` setting. A scheduler that stores local time will post at
the wrong hour twice a year, and you will not notice for months.
### Phase 1 · Composer and drafts
Build: the writing surface · a distraction-free editor where `---` on its own
line splits a thread into posts, showing each post as a separate card. Autosave
to SQLite on a debounce, and keep a small revision history so a bad autosave is
recoverable. Reorder posts by drag. No character counting yet.
Done when: typing splits and re-joins threads correctly as `---` is added and
removed, a browser refresh loses nothing, and reordering persists.
Do not build yet: counting, queue, publishing.
### Phase 2 · Correct character counting
Build: per-post counts using `twitter-text` with config v3, not `string.length`.
The rules that make a naive counter wrong:
- The limit is 280 weighted characters, not 280 code points.
- Every URL counts as 23 characters regardless of its real length, because all
links are wrapped by t.co.
- CJK characters and emoji count as 2.
Show the weighted count, mark over-limit posts clearly, and block queueing a
draft that contains one.
Done when: a post with a 100-character URL counts that URL as 23, an emoji counts
as 2, a 280-weighted-character post is accepted and a 281 is refused, and the
counts match X's own counter on a handful of pasted real examples.
### Phase 3 · Queue and calendar
Build: slots defined in a config file (for example weekdays at 09:00 and 16:00),
a "next free slot" action, an explicit datetime picker, and a calendar view
grouped by day showing queued drafts. Prevent double-booking one slot.
Done when: queueing three drafts into two daily slots spreads them across two
days, "next free slot" never returns a past time, and a slot already taken is not
offered again.
### Phase 4 · Publishing, and the honest cost
Build: the scheduler loop, running every minute, publishing due items via the X
API v2 with keys from `.env`, threading each post as a reply to the previous one,
recording remote ids, and marking failures with the API error. Retry a transient
failure three times with backoff; never retry a rejected post, or you will
publish it four times.
Before building this, read and repeat in the README what it costs. As of 2026 X
has no usable free API tier: new developers get pay-per-use at roughly $0.015 per
post and about $0.20 per post containing a link. A link-free posting habit is a
few dollars a month, but a marketer posting links daily is paying more than
Typefully's subscription for a worse product. Verify the current numbers in the
developer console before relying on them · this pricing has changed repeatedly.
Because of that, build the fallback first and treat the API as optional:
- A "copy thread to clipboard" button that copies the whole thread formatted for
manual posting.
- A desktop notification at slot time reminding you to post it.
- The app must be fully useful with no API key configured at all.
Done when: with no API key set, a due item fires a notification and the copy
button yields the complete thread; with a key set, a two-post thread publishes as
a genuine reply chain; and a rejected post is marked failed exactly once with the
API's error text visible.
### Phase 5 · Media
Build: image attachments per post, stored on disk, resized and stripped of EXIF
before upload, respecting the platform limit of four images per post.
Done when: a post with four images publishes or copies correctly, a fifth is
refused in the UI, and no uploaded file retains GPS EXIF.
### Phase 6 · Deploy
Build: a `/healthz` endpoint, a nightly backup of the database and media, a
systemd unit, and the README.
Done when: a restart leaves the queue intact and nothing due-while-down is lost ·
overdue items publish or notify on the next loop rather than being skipped.
### Out of scope (and why)
- LinkedIn and other networks in v1. Each is its own API, auth model and content
format, and doing two badly is worse than one properly.
- Analytics on published posts · reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
### README must contain
- The current X API cost per post and per link post, with the date checked and a
link to the developer console.
- A plain sentence: if you post links daily, this build costs more to run than
the subscription it replaces. Use the clipboard fallback instead.
- The timezone setting, and a warning to verify it before the first scheduled run.
===== ARCHITECTURE.md =====
# Architecture · Typefully
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22, node:http and node:sqlite | drafts, queue and scheduler loop in one process |
| Counting | twitter-text (config v3) | the only correct way to count 280 weighted characters |
| Publishing | X API v2, optional, behind an interface | pay-per-use pricing means the fallback comes first |
| Notifications | ntfy or desktop notifications | a reminder at slot time is the free publishing path |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Editor | drafts, posts, revisions, counting | Any front end writing the same rows |
| Scheduler | slots, queue, the minute loop | A cron job calling one function |
| Publisher | the interface with X, ntfy and clipboard implementations | Add LinkedIn or Bluesky as another implementation |
| Media | sharp pipeline and MEDIA_DIR | Object storage behind the same functions |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `PORT` · required · Any free port.
- `DATABASE_PATH` · required · SQLite file.
- `TIMEZONE` · required · IANA zone name. Verify before the first scheduled run.
- `SLOTS_FILE` · required · Your posting slots.
- `MEDIA_DIR` · required · Attached images, resized and stripped of EXIF.
- `NTFY_TOPIC` · optional · The topic you subscribed to in the ntfy app.
- `X_API_KEY` · optional, secret · X developer portal, only if you publish automatically.
- `X_API_SECRET` · optional, secret · X developer portal.
- `X_ACCESS_TOKEN` · optional, secret · X developer portal, generated with Read and Write.
- `X_ACCESS_SECRET` · optional, secret · X developer portal.
- `ADMIN_USER` · required · Any username for the basic-auth admin pages.
- `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · Typefully product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, twitter-text (config v3), X API v2, optional, behind an interface, ntfy or desktop notifications.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
## Known traps
- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
===== MILESTONES.md =====
# Delivery milestones · Typefully
Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Composer and drafts
Write threads, split them with ---, never lose a word.
### Steps
1. Create the project and the drafts, posts and revisions tables
drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at).
```sh
mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module
mkdir -p data/media && cp .env.example .env
```
2. Build the editor page
A distraction-free textarea; --- on its own line splits into post cards rendered beside it. Autosave on a debounce, keeping a small revision history.
3. Add drag to reorder posts within a thread
### Done when
- [ ] Adding and removing --- splits and re-joins correctly
- [ ] A browser refresh loses nothing
- [ ] Reordering persists
- [ ] A previous revision can be restored
## M2 · Correct character counting
Counts that match X exactly.
### Steps
1. Install twitter-text and count with config v3
280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length.
```sh
npm install twitter-text@3
```
2. Show the weighted count per post and block queueing an over-limit thread
### Done when
- [ ] A post with a 100-character URL counts it as 23
- [ ] An emoji counts as 2
- [ ] A 280-weighted post is accepted and 281 is refused
- [ ] Counts match X's own composer on pasted real examples
## M3 · Queue and calendar
Slots from your file, next-free-slot, a calendar by day, no double booking.
### Steps
1. Create the queue table and load SLOTS_FILE
queue (id, draft_id, scheduled_for UTC ms, attempts, last_error, posted_at, remote_ids JSON). Convert slots from TIMEZONE to UTC when scheduling.
2. Add next free slot, an explicit picker, and a calendar view grouped by day
### Done when
- [ ] Three drafts into two daily slots spread across two days
- [ ] Next free slot never returns a past time
- [ ] A taken slot is not offered again
- [ ] A slot at 09:00 local shows as 09:00 across a DST change
## M4 · Publishing, fallback first
Useful with no API key at all; automatic posting only when configured, with the cost stated.
### Steps
1. Build the clipboard fallback
A copy-thread button that copies the whole thread formatted for manual posting.
2. Send an ntfy notification at slot time
The scheduler loop, every minute: for each due item with no API key, post to ntfy.sh/NTFY_TOPIC with the first line and a link to the draft.
```sh
curl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC
```
3. Add the X API publisher behind an interface
OAuth 1.0a user context. Post each item as a reply to the previous one; record remote ids; three retries on transient errors, never on a rejected post.
4. Show the running cost
Count published posts and link posts this month and estimate the bill from the prices you verified.
### Done when
- [ ] With no key set, a due item fires a notification and the copy button yields the full thread
- [ ] With a key set, a two-post thread publishes as a real reply chain
- [ ] A rejected post is marked failed exactly once with the API's error visible
- [ ] Overdue items after a restart publish or notify on the next loop
### Watch out
- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
## M5 · Media
Up to four images per post, resized and stripped.
### Steps
1. Attach images per post with sharp: resize, strip EXIF, store in MEDIA_DIR
```sh
npm install sharp@0.35.3
```
2. Enforce four per post and upload media before posting when the API path is on
### Done when
- [ ] A four-image post publishes or copies correctly
- [ ] A fifth is refused in the UI
- [ ] No stored file retains GPS EXIF
## M6 · Deploy
Live, backed up, queue survives restarts.
### Steps
1. Add /healthz, systemd, Caddy and a backup of data/
Files: `deploy/composer.service`, `Caddyfile`
2. Write the README
The X cost per post and per link post with the date checked and a link to the console; the sentence that daily link posting costs more than the subscription; the timezone check.
Files: `README.md`
### Done when
- [ ] A restart leaves the queue intact and nothing due-while-down is lost
- [ ] The README states the current X prices with a date
## M7 · Operate it like a product (production only)
Only for the product-builder path: know when the scheduler is down, never lose the database, and keep the server patched.
### Steps
1. Add a /healthz endpoint and an external uptime check against it
Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices.
2. Write structured request logs and rotate them
One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight.
3. Back the SQLite file up off the machine nightly and test a restore
SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it.
```sh
sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'"
rclone copy /tmp/app-$(date +%F).db remote:backups/
```
4. Lock the box down
Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure.
### Done when
- [ ] Stopping the service triggers an uptime alert within a few minutes
- [ ] A restore from last night's backup contains yesterday's data
- [ ] A port scan from another machine shows only 22, 80 and 443
===== OPERATIONS.md =====
# Operations · Typefully
## Backup
tar of data/ nightly off the box.
## Restore
Extract, start, confirm the calendar shows the queue.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
Uptime on /healthz; alert if a due item is more than 15 minutes late.
## Incident checklist
If X keys leak, revoke them in the developer portal and rotate. If the API rejects everything, the fallback notifications keep the queue useful while you investigate.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] Works end to end with no X key configured
- [ ] Character counts verified against X's composer on ten examples
- [ ] One restore drill performed and dated
- [ ] README states the X costs with a date
## Launch constraint
Do not market omitted Typefully capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Any free port.
PORT=3000
# Required. SQLite file.
DATABASE_PATH=./data/posts.db
# Required. IANA zone name. Verify before the first scheduled run.
TIMEZONE=Europe/London
# Required. Your posting slots.
SLOTS_FILE=./slots.json
# Required. Attached images, resized and stripped of EXIF.
MEDIA_DIR=./data/media
# Optional. The topic you subscribed to in the ntfy app.
NTFY_TOPIC=posts-a8f3k2
# Optional · secret. X developer portal, only if you publish automatically.
X_API_KEY=...
# Optional · secret. X developer portal.
X_API_SECRET=...
# Optional · secret. X developer portal, generated with Read and Write.
X_ACCESS_TOKEN=...
# Optional · secret. X developer portal.
X_ACCESS_SECRET=...
# Required. Any username for the basic-auth admin pages.
ADMIN_USER=admin
# Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password.
ADMIN_PASS=change-me-to-a-long-random-string
# Typefully · indie build A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | drafts, queue and scheduler loop in one process | | Counting | twitter-text (config v3) | the only correct way to count 280 weighted characters | | Publishing | X API v2, optional, behind an interface | pay-per-use pricing means the fallback comes first | | Notifications | ntfy or desktop notifications | a reminder at slot time is the free publishing path | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **Your timezone, written as an IANA name** · free - Why: The scheduler stores UTC and renders in your zone. Getting this wrong posts at the wrong hour twice a year. - Get it: Look it up: Europe/London, America/New_York, Asia/Karachi. Put it in .env as TIMEZONE. - [ ] **Your posting slots** · free - Why: Phase 3 fills slots you define, such as weekdays at 09:00 and 16:00. - Get it: Write them as a JSON file: days and times in your timezone. - [ ] **An ntfy topic (or another notification channel)** · free - Why: The free publishing path is a notification at slot time. ntfy needs no account: pick a long random topic name. - Get it: Install the ntfy app on your phone, subscribe to a topic like posts-<random>, and put the topic in .env. Test with curl -d 'hello' ntfy.sh/<topic>. - [ ] **An X developer account with pay-per-use billing (optional)** (optional) · pay per use, about $0.015 per post, $0.20 with a link - Why: Only needed for automatic posting. As of 2026 there is no free tier for new developers: roughly $0.015 per post and about $0.20 per post containing a link. A link-heavy habit costs more than Typefully. - Get it: developer.x.com > sign in > create a project and app > User authentication settings with Read and Write > generate API Key, API Secret, Access Token and Access Token Secret. Add a payment method for pay-per-use. Verify the current prices in the console; they have changed repeatedly. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module mkdir -p data/media && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - LinkedIn and other networks in v1. Each is its own API, auth model and content format. - Analytics on published posts: reads cost money per call under pay-per-use. - Multi-account, teams and AI rewrite suggestions. - posting without paying X for API access - multi-account and team features - analytics on published posts - AI rewrite suggestions If one of those is essential to you, that is the reason to keep paying for Typefully, and the README should say so rather than pretend.
# Build brief · Typefully
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a post composer and scheduler like Typefully. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase. Read the
Phase 4 pricing note before you choose to build Phase 4 at all.
### Stack (fixed, do not substitute)
- Node 22 with `node:http` and `node:sqlite`. No Express, no framework.
- `twitter-text` for character counting. Do not hand-roll it, see Phase 2.
- Server-rendered HTML with small islands of vanilla JS for the editor.
- One process: web server and scheduler loop together.
### Data model (create this before Phase 1)
- `drafts`: id, title, created_at, updated_at, status ('draft' | 'queued' |
'posted' | 'failed')
- `posts`: id, draft_id, position, body, media (JSON array of paths)
- `queue`: id, draft_id, scheduled_for, attempts, last_error, posted_at,
remote_ids (JSON)
- Store `scheduled_for` as UTC epoch milliseconds and render in the user's
timezone from a `.env` setting. A scheduler that stores local time will post at
the wrong hour twice a year, and you will not notice for months.
### Phase 1 · Composer and drafts
Build: the writing surface · a distraction-free editor where `---` on its own
line splits a thread into posts, showing each post as a separate card. Autosave
to SQLite on a debounce, and keep a small revision history so a bad autosave is
recoverable. Reorder posts by drag. No character counting yet.
Done when: typing splits and re-joins threads correctly as `---` is added and
removed, a browser refresh loses nothing, and reordering persists.
Do not build yet: counting, queue, publishing.
### Phase 2 · Correct character counting
Build: per-post counts using `twitter-text` with config v3, not `string.length`.
The rules that make a naive counter wrong:
- The limit is 280 weighted characters, not 280 code points.
- Every URL counts as 23 characters regardless of its real length, because all
links are wrapped by t.co.
- CJK characters and emoji count as 2.
Show the weighted count, mark over-limit posts clearly, and block queueing a
draft that contains one.
Done when: a post with a 100-character URL counts that URL as 23, an emoji counts
as 2, a 280-weighted-character post is accepted and a 281 is refused, and the
counts match X's own counter on a handful of pasted real examples.
### Phase 3 · Queue and calendar
Build: slots defined in a config file (for example weekdays at 09:00 and 16:00),
a "next free slot" action, an explicit datetime picker, and a calendar view
grouped by day showing queued drafts. Prevent double-booking one slot.
Done when: queueing three drafts into two daily slots spreads them across two
days, "next free slot" never returns a past time, and a slot already taken is not
offered again.
### Phase 4 · Publishing, and the honest cost
Build: the scheduler loop, running every minute, publishing due items via the X
API v2 with keys from `.env`, threading each post as a reply to the previous one,
recording remote ids, and marking failures with the API error. Retry a transient
failure three times with backoff; never retry a rejected post, or you will
publish it four times.
Before building this, read and repeat in the README what it costs. As of 2026 X
has no usable free API tier: new developers get pay-per-use at roughly $0.015 per
post and about $0.20 per post containing a link. A link-free posting habit is a
few dollars a month, but a marketer posting links daily is paying more than
Typefully's subscription for a worse product. Verify the current numbers in the
developer console before relying on them · this pricing has changed repeatedly.
Because of that, build the fallback first and treat the API as optional:
- A "copy thread to clipboard" button that copies the whole thread formatted for
manual posting.
- A desktop notification at slot time reminding you to post it.
- The app must be fully useful with no API key configured at all.
Done when: with no API key set, a due item fires a notification and the copy
button yields the complete thread; with a key set, a two-post thread publishes as
a genuine reply chain; and a rejected post is marked failed exactly once with the
API's error text visible.
### Phase 5 · Media
Build: image attachments per post, stored on disk, resized and stripped of EXIF
before upload, respecting the platform limit of four images per post.
Done when: a post with four images publishes or copies correctly, a fifth is
refused in the UI, and no uploaded file retains GPS EXIF.
### Phase 6 · Deploy
Build: a `/healthz` endpoint, a nightly backup of the database and media, a
systemd unit, and the README.
Done when: a restart leaves the queue intact and nothing due-while-down is lost ·
overdue items publish or notify on the next loop rather than being skipped.
### Out of scope (and why)
- LinkedIn and other networks in v1. Each is its own API, auth model and content
format, and doing two badly is worse than one properly.
- Analytics on published posts · reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
### README must contain
- The current X API cost per post and per link post, with the date checked and a
link to the developer console.
- A plain sentence: if you post links daily, this build costs more to run than
the subscription it replaces. Use the clipboard fallback instead.
- The timezone setting, and a warning to verify it before the first scheduled run.# Agent instructions · Typefully indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, twitter-text (config v3), X API v2, optional, behind an interface, ntfy or desktop notifications. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Never retry a rejected post. You will publish it four times. - Verify X's prices in the developer console before relying on them; they have changed repeatedly.
# Build plan · Typefully A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Composer and drafts Write threads, split them with ---, never lose a word. ### Steps 1. Create the project and the drafts, posts and revisions tables drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at). ```sh mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module mkdir -p data/media && cp .env.example .env ``` 2. Build the editor page A distraction-free textarea; --- on its own line splits into post cards rendered beside it. Autosave on a debounce, keeping a small revision history. 3. Add drag to reorder posts within a thread ### Done when - [ ] Adding and removing --- splits and re-joins correctly - [ ] A browser refresh loses nothing - [ ] Reordering persists - [ ] A previous revision can be restored ## Phase 2 · Correct character counting Counts that match X exactly. ### Steps 1. Install twitter-text and count with config v3 280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length. ```sh npm install twitter-text@3 ``` 2. Show the weighted count per post and block queueing an over-limit thread ### Done when - [ ] A post with a 100-character URL counts it as 23 - [ ] An emoji counts as 2 - [ ] A 280-weighted post is accepted and 281 is refused - [ ] Counts match X's own composer on pasted real examples ## Phase 3 · Queue and calendar Slots from your file, next-free-slot, a calendar by day, no double booking. ### Steps 1. Create the queue table and load SLOTS_FILE queue (id, draft_id, scheduled_for UTC ms, attempts, last_error, posted_at, remote_ids JSON). Convert slots from TIMEZONE to UTC when scheduling. 2. Add next free slot, an explicit picker, and a calendar view grouped by day ### Done when - [ ] Three drafts into two daily slots spread across two days - [ ] Next free slot never returns a past time - [ ] A taken slot is not offered again - [ ] A slot at 09:00 local shows as 09:00 across a DST change ## Phase 4 · Publishing, fallback first Useful with no API key at all; automatic posting only when configured, with the cost stated. ### Steps 1. Build the clipboard fallback A copy-thread button that copies the whole thread formatted for manual posting. 2. Send an ntfy notification at slot time The scheduler loop, every minute: for each due item with no API key, post to ntfy.sh/NTFY_TOPIC with the first line and a link to the draft. ```sh curl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC ``` 3. Add the X API publisher behind an interface OAuth 1.0a user context. Post each item as a reply to the previous one; record remote ids; three retries on transient errors, never on a rejected post. 4. Show the running cost Count published posts and link posts this month and estimate the bill from the prices you verified. ### Done when - [ ] With no key set, a due item fires a notification and the copy button yields the full thread - [ ] With a key set, a two-post thread publishes as a real reply chain - [ ] A rejected post is marked failed exactly once with the API's error visible - [ ] Overdue items after a restart publish or notify on the next loop ### Watch out - Never retry a rejected post. You will publish it four times. - Verify X's prices in the developer console before relying on them; they have changed repeatedly. ## Phase 5 · Media Up to four images per post, resized and stripped. ### Steps 1. Attach images per post with sharp: resize, strip EXIF, store in MEDIA_DIR ```sh npm install sharp@0.35.3 ``` 2. Enforce four per post and upload media before posting when the API path is on ### Done when - [ ] A four-image post publishes or copies correctly - [ ] A fifth is refused in the UI - [ ] No stored file retains GPS EXIF ## Phase 6 · Deploy Live, backed up, queue survives restarts. ### Steps 1. Add /healthz, systemd, Caddy and a backup of data/ Files: `deploy/composer.service`, `Caddyfile` 2. Write the README The X cost per post and per link post with the date checked and a link to the console; the sentence that daily link posting costs more than the subscription; the timezone check. Files: `README.md` ### Done when - [ ] A restart leaves the queue intact and nothing due-while-down is lost - [ ] The README states the current X prices with a date ## Not in this build - LinkedIn and other networks in v1. Each is its own API, auth model and content format. - Analytics on published posts: reads cost money per call under pay-per-use. - Multi-account, teams and AI rewrite suggestions. ## After v1, if you want it - Bluesky as a second publisher: its API is free and the interface is already there - A browser bookmarklet that opens the composer with a quoted link
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/posts.db # Required. IANA zone name. Verify before the first scheduled run. TIMEZONE=Europe/London # Required. Your posting slots. SLOTS_FILE=./slots.json # Required. Attached images, resized and stripped of EXIF. MEDIA_DIR=./data/media # Optional. The topic you subscribed to in the ntfy app. NTFY_TOPIC=posts-a8f3k2 # Optional · secret. X developer portal, only if you publish automatically. X_API_KEY=... # Optional · secret. X developer portal. X_API_SECRET=... # Optional · secret. X developer portal, generated with Read and Write. X_ACCESS_TOKEN=... # Optional · secret. X developer portal. X_ACCESS_SECRET=... # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
# Typefully · product brief ## Problem For personal thread drafting and scheduling, the core loop is vibecodable; the caveat is social API access, analytics, and platform-policy upkeep. ## Product outcome A scheduler that is honest about what publishing costs, works fully without paying X, and can be extended to other networks behind the same publisher interface. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - X/LinkedIn API access - hosted scheduler - database - auth - optional LLM API ## Explicit non-goals for v1 - LinkedIn and other networks in v1. Each is its own API, auth model and content format. - Analytics on published posts: reads cost money per call under pay-per-use. - Multi-account, teams and AI rewrite suggestions. - posting without paying X for API access - multi-account and team features - analytics on published posts - AI rewrite suggestions ## Success criteria - Works end to end with no X key configured - Character counts verified against X's composer on ten examples - One restore drill performed and dated - README states the X costs with a date
# Build brief · Typefully
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a post composer and scheduler like Typefully. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase. Read the
Phase 4 pricing note before you choose to build Phase 4 at all.
### Stack (fixed, do not substitute)
- Node 22 with `node:http` and `node:sqlite`. No Express, no framework.
- `twitter-text` for character counting. Do not hand-roll it, see Phase 2.
- Server-rendered HTML with small islands of vanilla JS for the editor.
- One process: web server and scheduler loop together.
### Data model (create this before Phase 1)
- `drafts`: id, title, created_at, updated_at, status ('draft' | 'queued' |
'posted' | 'failed')
- `posts`: id, draft_id, position, body, media (JSON array of paths)
- `queue`: id, draft_id, scheduled_for, attempts, last_error, posted_at,
remote_ids (JSON)
- Store `scheduled_for` as UTC epoch milliseconds and render in the user's
timezone from a `.env` setting. A scheduler that stores local time will post at
the wrong hour twice a year, and you will not notice for months.
### Phase 1 · Composer and drafts
Build: the writing surface · a distraction-free editor where `---` on its own
line splits a thread into posts, showing each post as a separate card. Autosave
to SQLite on a debounce, and keep a small revision history so a bad autosave is
recoverable. Reorder posts by drag. No character counting yet.
Done when: typing splits and re-joins threads correctly as `---` is added and
removed, a browser refresh loses nothing, and reordering persists.
Do not build yet: counting, queue, publishing.
### Phase 2 · Correct character counting
Build: per-post counts using `twitter-text` with config v3, not `string.length`.
The rules that make a naive counter wrong:
- The limit is 280 weighted characters, not 280 code points.
- Every URL counts as 23 characters regardless of its real length, because all
links are wrapped by t.co.
- CJK characters and emoji count as 2.
Show the weighted count, mark over-limit posts clearly, and block queueing a
draft that contains one.
Done when: a post with a 100-character URL counts that URL as 23, an emoji counts
as 2, a 280-weighted-character post is accepted and a 281 is refused, and the
counts match X's own counter on a handful of pasted real examples.
### Phase 3 · Queue and calendar
Build: slots defined in a config file (for example weekdays at 09:00 and 16:00),
a "next free slot" action, an explicit datetime picker, and a calendar view
grouped by day showing queued drafts. Prevent double-booking one slot.
Done when: queueing three drafts into two daily slots spreads them across two
days, "next free slot" never returns a past time, and a slot already taken is not
offered again.
### Phase 4 · Publishing, and the honest cost
Build: the scheduler loop, running every minute, publishing due items via the X
API v2 with keys from `.env`, threading each post as a reply to the previous one,
recording remote ids, and marking failures with the API error. Retry a transient
failure three times with backoff; never retry a rejected post, or you will
publish it four times.
Before building this, read and repeat in the README what it costs. As of 2026 X
has no usable free API tier: new developers get pay-per-use at roughly $0.015 per
post and about $0.20 per post containing a link. A link-free posting habit is a
few dollars a month, but a marketer posting links daily is paying more than
Typefully's subscription for a worse product. Verify the current numbers in the
developer console before relying on them · this pricing has changed repeatedly.
Because of that, build the fallback first and treat the API as optional:
- A "copy thread to clipboard" button that copies the whole thread formatted for
manual posting.
- A desktop notification at slot time reminding you to post it.
- The app must be fully useful with no API key configured at all.
Done when: with no API key set, a due item fires a notification and the copy
button yields the complete thread; with a key set, a two-post thread publishes as
a genuine reply chain; and a rejected post is marked failed exactly once with the
API's error text visible.
### Phase 5 · Media
Build: image attachments per post, stored on disk, resized and stripped of EXIF
before upload, respecting the platform limit of four images per post.
Done when: a post with four images publishes or copies correctly, a fifth is
refused in the UI, and no uploaded file retains GPS EXIF.
### Phase 6 · Deploy
Build: a `/healthz` endpoint, a nightly backup of the database and media, a
systemd unit, and the README.
Done when: a restart leaves the queue intact and nothing due-while-down is lost ·
overdue items publish or notify on the next loop rather than being skipped.
### Out of scope (and why)
- LinkedIn and other networks in v1. Each is its own API, auth model and content
format, and doing two badly is worse than one properly.
- Analytics on published posts · reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
### README must contain
- The current X API cost per post and per link post, with the date checked and a
link to the developer console.
- A plain sentence: if you post links daily, this build costs more to run than
the subscription it replaces. Use the clipboard fallback instead.
- The timezone setting, and a warning to verify it before the first scheduled run.# Architecture · Typefully ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | drafts, queue and scheduler loop in one process | | Counting | twitter-text (config v3) | the only correct way to count 280 weighted characters | | Publishing | X API v2, optional, behind an interface | pay-per-use pricing means the fallback comes first | | Notifications | ntfy or desktop notifications | a reminder at slot time is the free publishing path | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Editor | drafts, posts, revisions, counting | Any front end writing the same rows | | Scheduler | slots, queue, the minute loop | A cron job calling one function | | Publisher | the interface with X, ntfy and clipboard implementations | Add LinkedIn or Bluesky as another implementation | | Media | sharp pipeline and MEDIA_DIR | Object storage behind the same functions | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `TIMEZONE` · required · IANA zone name. Verify before the first scheduled run. - `SLOTS_FILE` · required · Your posting slots. - `MEDIA_DIR` · required · Attached images, resized and stripped of EXIF. - `NTFY_TOPIC` · optional · The topic you subscribed to in the ntfy app. - `X_API_KEY` · optional, secret · X developer portal, only if you publish automatically. - `X_API_SECRET` · optional, secret · X developer portal. - `X_ACCESS_TOKEN` · optional, secret · X developer portal, generated with Read and Write. - `X_ACCESS_SECRET` · optional, secret · X developer portal. - `ADMIN_USER` · required · Any username for the basic-auth admin pages. - `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Typefully product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, twitter-text (config v3), X API v2, optional, behind an interface, ntfy or desktop notifications. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Never retry a rejected post. You will publish it four times. - Verify X's prices in the developer console before relying on them; they have changed repeatedly.
# Delivery milestones · Typefully Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Composer and drafts Write threads, split them with ---, never lose a word. ### Steps 1. Create the project and the drafts, posts and revisions tables drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at). ```sh mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module mkdir -p data/media && cp .env.example .env ``` 2. Build the editor page A distraction-free textarea; --- on its own line splits into post cards rendered beside it. Autosave on a debounce, keeping a small revision history. 3. Add drag to reorder posts within a thread ### Done when - [ ] Adding and removing --- splits and re-joins correctly - [ ] A browser refresh loses nothing - [ ] Reordering persists - [ ] A previous revision can be restored ## M2 · Correct character counting Counts that match X exactly. ### Steps 1. Install twitter-text and count with config v3 280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length. ```sh npm install twitter-text@3 ``` 2. Show the weighted count per post and block queueing an over-limit thread ### Done when - [ ] A post with a 100-character URL counts it as 23 - [ ] An emoji counts as 2 - [ ] A 280-weighted post is accepted and 281 is refused - [ ] Counts match X's own composer on pasted real examples ## M3 · Queue and calendar Slots from your file, next-free-slot, a calendar by day, no double booking. ### Steps 1. Create the queue table and load SLOTS_FILE queue (id, draft_id, scheduled_for UTC ms, attempts, last_error, posted_at, remote_ids JSON). Convert slots from TIMEZONE to UTC when scheduling. 2. Add next free slot, an explicit picker, and a calendar view grouped by day ### Done when - [ ] Three drafts into two daily slots spread across two days - [ ] Next free slot never returns a past time - [ ] A taken slot is not offered again - [ ] A slot at 09:00 local shows as 09:00 across a DST change ## M4 · Publishing, fallback first Useful with no API key at all; automatic posting only when configured, with the cost stated. ### Steps 1. Build the clipboard fallback A copy-thread button that copies the whole thread formatted for manual posting. 2. Send an ntfy notification at slot time The scheduler loop, every minute: for each due item with no API key, post to ntfy.sh/NTFY_TOPIC with the first line and a link to the draft. ```sh curl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC ``` 3. Add the X API publisher behind an interface OAuth 1.0a user context. Post each item as a reply to the previous one; record remote ids; three retries on transient errors, never on a rejected post. 4. Show the running cost Count published posts and link posts this month and estimate the bill from the prices you verified. ### Done when - [ ] With no key set, a due item fires a notification and the copy button yields the full thread - [ ] With a key set, a two-post thread publishes as a real reply chain - [ ] A rejected post is marked failed exactly once with the API's error visible - [ ] Overdue items after a restart publish or notify on the next loop ### Watch out - Never retry a rejected post. You will publish it four times. - Verify X's prices in the developer console before relying on them; they have changed repeatedly. ## M5 · Media Up to four images per post, resized and stripped. ### Steps 1. Attach images per post with sharp: resize, strip EXIF, store in MEDIA_DIR ```sh npm install sharp@0.35.3 ``` 2. Enforce four per post and upload media before posting when the API path is on ### Done when - [ ] A four-image post publishes or copies correctly - [ ] A fifth is refused in the UI - [ ] No stored file retains GPS EXIF ## M6 · Deploy Live, backed up, queue survives restarts. ### Steps 1. Add /healthz, systemd, Caddy and a backup of data/ Files: `deploy/composer.service`, `Caddyfile` 2. Write the README The X cost per post and per link post with the date checked and a link to the console; the sentence that daily link posting costs more than the subscription; the timezone check. Files: `README.md` ### Done when - [ ] A restart leaves the queue intact and nothing due-while-down is lost - [ ] The README states the current X prices with a date ## M7 · Operate it like a product (production only) Only for the product-builder path: know when the scheduler is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · Typefully ## Backup tar of data/ nightly off the box. ## Restore Extract, start, confirm the calendar shows the queue. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on /healthz; alert if a due item is more than 15 minutes late. ## Incident checklist If X keys leak, revoke them in the developer portal and rotate. If the API rejects everything, the fallback notifications keep the queue useful while you investigate. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Works end to end with no X key configured - [ ] Character counts verified against X's composer on ten examples - [ ] One restore drill performed and dated - [ ] README states the X costs with a date ## Launch constraint Do not market omitted Typefully capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/posts.db # Required. IANA zone name. Verify before the first scheduled run. TIMEZONE=Europe/London # Required. Your posting slots. SLOTS_FILE=./slots.json # Required. Attached images, resized and stripped of EXIF. MEDIA_DIR=./data/media # Optional. The topic you subscribed to in the ntfy app. NTFY_TOPIC=posts-a8f3k2 # Optional · secret. X developer portal, only if you publish automatically. X_API_KEY=... # Optional · secret. X developer portal. X_API_SECRET=... # Optional · secret. X developer portal, generated with Read and Write. X_ACCESS_TOKEN=... # Optional · secret. X developer portal. X_ACCESS_SECRET=... # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because the editor reduces friction and posting works without API babysitting.
xposting without paying X for API access
xmulti-account and team features
xanalytics on published posts
xAI rewrite suggestions
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Typefully →· no votes, no pay-to-list · just what's real
Typefully pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0 | $0 | 1 social set, 1 user and 10 published posts/month. |
| creator | — | $12.50 | 1 user and unlimited scheduling; the current numeric social-set cap could not be independently extracted. |
| business | custom | — | Team collaboration; social-set allowance varies by plan, but the current numeric price and cap were not recoverable from the client-rendered live widget. |
free tier1 social set, 1 user and 10 posts per month.
billingCreator shown with annual billing on the live page; official billing help supports monthly/yearly switching generally; 14-day paid trial; Business amount not independently recoverable
hidden costsOptional Extra AI Usage is metered, billed separately and may use a different billing cycle; current rates are not public. A spending cap is available, and usage charges are non-refundable.
verified 2026-08-11 · source ↗
Is Typefully free?
The free plan allows 15 posts a month on one social set. Paid is Creator at $12.5/mo (checked 2026-08-07).
Vibecode Typefully
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Typefully replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Typefully cost?
Typefully costs about $12.5/month (Creator, checked 2026-08-07), which is $150 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Typefully?
Honestly: posting without paying X for API access; multi-account and team features; analytics on published posts; AI rewrite suggestions. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Typefully?
Yes: Postiz (Threads, drafts, schedules and analytics across far more networks; operating it is the unlisted feature.) BrightBean Studio (Draft and schedule threads for X and LinkedIn with analytics; the editor is less lovingly text-obsessed.) Buffer Free (X threads and LinkedIn posts with ten queued items per channel; eight lifetime channel connections is the fine print.) All 4 curated free alternatives are at vibecodeit.com/typefully/alternatives. The prompt is for when you want it exactly your way.