Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ data.db-shm
.dev.vars
.wrangler/
.clawnify/
test/.server.mjs
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Think of it as an open-source alternative to **Buffer**, **Hypefury**, **Typeful
- **Dashboard** -- at-a-glance stats, upcoming posts, and recent drafts
- **Native previews** -- see the post as it will look on each selected platform before it goes out
- **Direct publishing** -- publish to X, LinkedIn, Instagram, Facebook Pages, TikTok and Bluesky through the accounts connected in Clawnify
- **Publish once, exactly once** -- a channel that has already gone out is never sent again, so retrying a half-delivered post, editing a post that is already live, or a scheduled delivery arriving twice all do the right thing instead of double-posting
- **URL routing** -- bookmarkable pages (`/compose`, `/calendar`, `/queue`, `/drafts`, `/channels`, `/analytics`)

### Supported Platforms
Expand Down Expand Up @@ -56,10 +57,29 @@ pnpm dev

Open `http://localhost:5173` in your browser. The database schema is applied automatically on startup.

### Tests

```bash
pnpm test
```

Runs the real API against an in-memory SQLite database, in process -- no dev
server and nothing to deploy. Covers the publishing rules above by counting the
calls the app actually makes to each platform. Needs Node 22.5+ (for
`node:sqlite`).

### Publishing

Publishing runs through the accounts connected in Clawnify -- no API keys in the app. Locally there is no credential service, so posts save and schedule but publishing reports each channel as not connected.

Each channel on a post is delivered and tracked on its own: one platform rejecting
the content marks that channel failed, with the reason, while the rest still go
out. The post then reads `partial`, and **Retry** sends only the channels that
did not make it -- a published channel is never posted to twice, whether the
retry comes from you, from an edit, or from a scheduled delivery that arrives
more than once. There is no undo on a live post, so this is enforced in the
database rather than left to the caller.

## Tech Stack

| Layer | Technology |
Expand Down Expand Up @@ -104,6 +124,9 @@ src/
analytics-view.tsx -- Bar charts and daily activity
post-card.tsx -- Reusable post preview card
error-banner.tsx -- Error display
test/
harness.mjs -- Boots the real API over an in-memory SQLite database
publish-idempotency.mjs -- Publishing is safe to run twice
```

### API Endpoints
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "wrangler d1 execute open-post-db --local --file=src/server/schema.sql && concurrently -n ui,api -c cyan,green \"vite\" \"wrangler dev --port 8787\"",
"build": "vite build"
"build": "vite build",
"test": "esbuild src/server/index.ts --bundle --format=esm --platform=node --outfile=test/.server.mjs && node test/publish-idempotency.mjs"
},
"dependencies": {
"@clawnify/app": "^0.1.0",
Expand All @@ -21,6 +22,7 @@
"devDependencies": {
"@preact/preset-vite": "^2.9.0",
"concurrently": "^9.0.0",
"esbuild": "^0.28.2",
"typescript": "^5.7.0",
"vite": "^6.0.0",
"wrangler": "^4.0.0"
Expand Down
Loading