- JavaScript 77.9%
- CSS 13.2%
- HTML 7.4%
- Batchfile 1.4%
- Dockerfile 0.1%
| build | ||
| electron | ||
| public | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| build-win.bat | ||
| CLAUDE.md | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
civitai-scheduler
A personal, self-hosted scheduler for Civitai (civitai.com / civitai.red). Queue up image/video posts with a title, tags, and a publish time, and a background job auto-publishes them through your own account when the time comes — no browser tab needs to stay open.
It works by driving Civitai's own MCP server (https://mcp.civitai.com/mcp),
which is the officially supported way to post programmatically — the plain
REST API (civitai.com/api/v1/...) is read-only.
How it works
you (browser) ──POST /api/posts──▶ Express server ──▶ SQLite queue (data/scheduler.db)
│
node-cron polls every minute
│
▼
uploads images, then calls create_post
on the Civitai MCP server (your API key)
│
▼
post goes live on civitai.red
Everything — the API key, the queue, the scheduler — lives on the server side. The web UI (a little "departures board") is just a thin client for adding posts and watching their status.
Setup
-
Get a Civitai API key. civitai.com (or civitai.red) → Account Settings → API Keys. A key with the
MediaWritescope is enough to create posts; a "Full" key is simplest if you don't want to think about scopes. -
Install dependencies:
npm install -
Configure:
cp .env.example .env # then edit .env and paste your key into CIVITAI_API_KEY -
Verify the connection and check tool schemas before trusting it with a real post:
npm run inspect-toolsThis prints the live input schema for
create_post/upload_image/publish_post/whoamistraight from Civitai's server, plus your account status. Run this again any time scheduling starts failing with an argument-validation error — Civitai can evolve these schemas, andsrc/mcpClient.jsis only as correct as the last time someone checked it against them. (It's already been caught once:upload_imageexpectsdata/contentTypefor base64 uploads, notbase64/mimeTypeas Civitai's public docs imply — fixed insrc/mcpClient.js.)Note:
whoami's account-status lookup (user.getSelfStatus) currently errors on Civitai's end (Invalid input) even with a perfectly valid key — this is a bug on their server, not something wrong with your setup. Because of that, the app's own connection check (below) doesn't rely onwhoami. -
Run it:
npm startThen open http://localhost:4173.
Or run it as a Windows desktop app instead — see Windows desktop app below.
The connection status pill
The dashboard header shows whether your API key is working. Since whoami
is unreliable server-side (see above), the check instead uploads a throwaway
1×1 pixel image through the same upload_image tool real posts use — a
green pill means the key can actually authenticate and write, not just that
the server responded.
That also means it's a real (tiny, orphaned) write against your Civitai account, so it's not polled automatically — it only runs once when the dashboard loads, and whenever you click "recheck". A red pill means the key is likely invalid or expired.
Keeping it running unattended
This is the part that can't live in a browser tab or a static page — it needs a process that's alive at the scheduled time. Options, roughly easiest first:
- A cheap always-on VPS or a Raspberry Pi / spare machine at home:
install Node,
npm start, keep it alive withpm2(pm2 start src/server.js) or a systemd unit. - Docker:
docker build -t civitai-scheduler .thendocker run -d -p 4173:4173 --env-file .env -v $(pwd)/data:/app/data civitai-scheduler. - A small hosting platform (Railway, Fly.io, Render, etc.) — deploy the
Dockerfile, attach a persistent volume for
/app/data, set the env vars from.env.examplein their dashboard. - The Windows desktop app (see below) — installs a tray-resident app that can auto-launch at Windows login, so the scheduler survives reboots without a terminal or Docker container to babysit.
If you only run this on your laptop and close it, scheduled posts simply
won't fire until you reopen it — node-cron also does a catch-up check on
startup, so anything overdue publishes right away rather than being skipped.
Windows desktop app (Electron)
The same app also ships as a Windows desktop app — a tray-resident Electron wrapper around the exact same Express server, SQLite queue, and scheduler described above. No separate setup, no browser tab, no terminal window.
Run it in dev mode:
npm run electron:rebuild # one-time: rebuild better-sqlite3 for Electron's ABI
npm run electron:dev
Build a Windows installer:
build-win.bat
Produces an NSIS installer under release/ (gitignored — it's a build artifact, not something to
commit) and automatically restores better-sqlite3 to the plain-Node ABI afterward, so npm start/
npm run dev keep working without any manual fixup (see the gotcha below for why that's needed). Run
it from a real cmd.exe or by double-clicking it in Explorer — not from a Git-Bash/MSYS shell, which
mangles the /c flag it needs internally.
Under the hood this just runs npm run dist:win plus the ABI-restore steps; you can still run
npm run dist:win directly if you're going to rebuild for Electron again right after anyway (e.g.
repeated electron:dev testing).
What's different from the plain npm start flow:
- Configuration lives in a Settings window, not a hand-edited
.envfile. On first launch (or whenever no API key is set), a Settings window opens automatically — fill in your Civitai API key and the rest, hit Save, and confirm the restart it asks for. Everything is written to%APPDATA%\civitai-scheduler\.env, separate from any repo-root.envyou use fornpm start. - Close-to-tray vs. quit, and launch at Windows startup, are both toggles in that same Settings window (also under "App behavior") — nothing is silently registered without your say-so.
- Data lives under
%APPDATA%\civitai-scheduler\data(SQLite DB + uploaded images) instead of the repo'sdata/folder, so a packaged install doesn't need write access to its own installation directory. - Changing any Civitai-related setting (API key, MCP/tRPC URL, dashboard password, poll cron, auto-upload window, port) requires restarting the app to take effect — the Settings window will offer to do this for you.
Gotcha: better-sqlite3 is a compiled native module, and plain Node vs.
Electron need it built against different ABIs — but they share the same
node_modules/better-sqlite3 on disk. After electron:dev/dist:win
rebuilds it for Electron, npm start/npm run dev will error until it's
rebuilt back for plain Node — build-win.bat does this for you automatically.
Plain npm rebuild better-sqlite3 isn't reliable enough to recommend doing by
hand: it can get shadowed into rebuilding for Electron again, or (depending on
your installed Node version) misdetect the compiler toolset entirely. Docker
is unaffected — it always does a fresh npm ci inside the container.
Security notes
- Your Civitai API key never reaches the browser — it's only used server-side.
- If you deploy this anywhere reachable from the internet (not just
localhost), setDASHBOARD_PASSWORDin.env. Without it, anyone who finds the URL can post to your account. The app refuses to stay silent about this — it logs a warning on startup if it's unset. - Uploaded images are stored under
data/images/<post-id>/until published; nothing is deleted automatically after publishing (delete a post's row in the UI to clean it up).
Status
Both upload_image (URL and base64 paths) and create_post have now been
exercised for real against a live account — a real scheduled post has
published successfully end-to-end. The field names in src/mcpClient.js
match Civitai's live MCP schema. If scheduling ever starts failing with an
argument-validation error in the future (Civitai's schema can still evolve),
paste the last_error text back and check it against what npm run inspect-tools prints.
Retries: a failed post is retried automatically on the next poll (up to 3
attempts) before being marked failed for good; you can also hit "retry" in
the UI to reset the attempt counter.
Project layout
src/server.js Express app + API routes + optional basic auth
src/db.js SQLite queue (better-sqlite3)
src/scheduler.js node-cron polling loop; uploads images, creates the post
src/mcpClient.js Thin wrapper around the Civitai MCP server (upload, create_post, connection check)
src/inspectTools.js Standalone: dumps live tool schemas + account status
public/ The departures-board web UI (static, no build step)
electron/ Windows desktop app wrapper (Electron) — main process, tray, Settings window
build-win.bat Builds the Windows installer + restores better-sqlite3's plain-Node ABI