- JavaScript 79.5%
- HTML 9.7%
- CSS 9.3%
- Batchfile 0.9%
- Shell 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| build | ||
| electron | ||
| public | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| build-linux.sh | ||
| 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.
A second page (/models.html) does the same for models: schedule the publish
time on a model/version you already created on civitai.com, or skip civitai.com
entirely and create a brand-new model/version plus upload its file straight from
this app. See Model uploads below.
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 desktop app instead — see Windows desktop app or Linux 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.
Model uploads
Open /models.html (linked from the "Models" tab) for two model-side features,
separate from the post scheduler above:
- Schedule a model release — for a model/version you already created (and already uploaded a file for) on civitai.com/civitai.red. Paste its URL or ID, pick a version, and schedule a publish time the same way posts are scheduled. This never creates or deletes anything on Civitai — it's purely a scheduling wrapper around a version that already exists.
- Upload a new release — creates the model (for a first release) and
version, then uploads the file itself in chunks straight to Civitai's storage,
no trip to civitai.com required. A few conveniences:
- Import from a folder: instead of picking the model file by hand, point
at a folder. It's split automatically — the one
.safetensorsfile becomes the model file, any images become showcase images for the new version, and an optionaltrigger_words.txt(one line, comma-separated) fills in the Trigger words field. Re-picking a folder always resets Trigger words to match what's actually in it (empty if there's notrigger_words.txt), so a manually-typed value never lingers by accident. - Showcase images: any images from the folder are uploaded and attached to the new version with a single post, the same way civitai.com's own upload page does it. That post is left as an unpublished draft — you'll find it on the regular Posts board whenever you're ready to publish it.
- Description & trigger words: a model description (first release only), a per-version description, and comma-separated trigger words are all optional and get saved straight to the model/version on Civitai.
- Import from a folder: instead of picking the model file by hand, point
at a folder. It's split automatically — the one
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 or Linux desktop app (see below) — installs a tray-resident app that can auto-launch at 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.
A Forgejo Actions workflow (.forgejo/workflows/build.yml, build-windows job) also builds this on a
Windows runner and uploads it as an artifact on every v* tag push, or on manual dispatch — it just
runs npm ci + npm run dist:win directly (no build-win.bat, since CI runners are throwaway and
don't need the ABI restored afterward). On a tag push specifically, a publish-release job then
downloads this installer plus the Linux AppImage and attaches both to a Forgejo Release for that tag.
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.
Linux desktop app (AppImage)
The same Electron wrapper also ships as a Linux AppImage — same tray-resident app, same Settings
window, same %APPDATA%-equivalent (~/.config/civitai-scheduler/) data/config layout described
above. better-sqlite3 is a compiled native module, so the AppImage has to be built on a real Linux
machine (or WSL2) — it can't be cross-compiled from Windows.
Build it:
npm run dist:linux
or, to also restore better-sqlite3 to the plain-Node ABI afterward (same gotcha as the Windows
build — see above):
./build-linux.sh
Produces release/CivitAI-Scheduler-<version>.AppImage. A Forgejo Actions workflow
(.forgejo/workflows/build.yml, build-linux job) builds this on a Linux runner and uploads it as an
artifact on every v* tag push, or on manual dispatch — the same workflow's build-windows job builds
the Windows installer on a separate Windows runner (see below). On a tag push, both installers are then
attached to a Forgejo Release for that tag automatically (publish-release job).
Running it: chmod +x the AppImage after downloading, then run it directly (double-click in a
file manager, or ./CivitAI-Scheduler-<version>.AppImage from a terminal). It needs FUSE to mount
itself, which most desktop distros ship by default; on a minimal install without FUSE, run it with
--appimage-extract-and-run instead.
Known caveat: the tray icon relies on the desktop environment's system-tray/appindicator support. KDE, XFCE, Cinnamon, and most others show it out of the box; stock GNOME needs the "AppIndicator and KStatusNotifierItem Support" extension installed first, or the tray icon simply won't appear (the app still runs fine — just close the window instead of using the tray to get back to it).
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.
Model uploads have likewise been exercised end-to-end against a live account:
model + version creation, chunked file upload, folder-import splitting, the
showcase-images post via modelVersionId, and the description/trigger-words
fields have all been confirmed to actually land on Civitai's side (read back
via getModelForEdit), not just to return success.
Project layout
src/server.js Express app + API routes + optional basic auth
src/db.js SQLite queue (better-sqlite3) — posts, model_releases, model_uploads
src/scheduler.js node-cron polling loop; uploads images, creates the post
src/mcpClient.js Thin wrapper around the Civitai MCP server + tRPC (upload, create_post,
connection check, model/version upsert, multipart file upload)
src/modelUpload.js Drives a model_uploads row through create + chunked upload + showcase post
src/inspectTools.js Standalone: dumps live tool schemas + account status
public/ The departures-board web UI (static, no build step); models.html/models.js
is the model-releases/model-uploads page
electron/ Desktop app wrapper (Electron, Windows + Linux) — main process, tray, Settings window
build-win.bat Builds the Windows installer + restores better-sqlite3's plain-Node ABI
build-linux.sh Builds the Linux AppImage + restores better-sqlite3's plain-Node ABI
.forgejo/workflows/ CI: builds the Linux AppImage + Windows installer (one job per OS runner), then
attaches both to a Forgejo Release on tag pushes