No description
  • JavaScript 79.5%
  • HTML 9.7%
  • CSS 9.3%
  • Batchfile 0.9%
  • Shell 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
nuclear314 af51697b96
All checks were successful
Build desktop apps / build-linux (push) Successful in 47s
Build desktop apps / build-windows (push) Successful in 2m43s
Build desktop apps / publish-release (push) Successful in 12s
Clean up ignore
2026-08-21 17:36:02 +02:00
.forgejo/workflows Fix title issue with different workflow 2026-08-15 17:28:08 +02:00
build Initial linux code readying 2026-08-14 12:08:10 +02:00
electron Initial linux code readying 2026-08-14 12:08:10 +02:00
public Add model permission flag option to app 2026-08-21 17:34:16 +02:00
src Add model permission flag option to app 2026-08-21 17:34:16 +02:00
.dockerignore Add windows install builder (Electron frontend) 2026-07-23 20:33:28 +02:00
.env.example Add env variable for com or red + add direct delete option from app to site 2026-07-22 20:55:08 +02:00
.gitignore Clean up ignore 2026-08-21 17:36:02 +02:00
build-linux.sh Initial linux code readying 2026-08-14 12:08:10 +02:00
build-win.bat Add windows install builder (Electron frontend) 2026-07-23 20:33:28 +02:00
CLAUDE.md Add auto-release 2026-08-15 17:09:46 +02:00
Dockerfile Initial commit 2026-07-20 19:49:34 +02:00
package-lock.json Fix linux issues 2026-08-15 16:39:13 +02:00
package.json Bump version 2026-08-21 17:35:00 +02:00
README.md Fix title issue with different workflow 2026-08-15 17:28:08 +02:00

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

  1. Get a Civitai API key. civitai.com (or civitai.red) → Account Settings → API Keys. A key with the MediaWrite scope is enough to create posts; a "Full" key is simplest if you don't want to think about scopes.

  2. Install dependencies:

    npm install
    
  3. Configure:

    cp .env.example .env
    # then edit .env and paste your key into CIVITAI_API_KEY
    
  4. Verify the connection and check tool schemas before trusting it with a real post:

    npm run inspect-tools
    

    This prints the live input schema for create_post / upload_image / publish_post / whoami straight 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, and src/mcpClient.js is only as correct as the last time someone checked it against them. (It's already been caught once: upload_image expects data/contentType for base64 uploads, not base64/mimeType as Civitai's public docs imply — fixed in src/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 on whoami.

  5. Run it:

    npm start
    

    Then 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 .safetensors file becomes the model file, any images become showcase images for the new version, and an optional trigger_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 no trigger_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.

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 with pm2 (pm2 start src/server.js) or a systemd unit.
  • Docker: docker build -t civitai-scheduler . then docker 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.example in 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 .env file. 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 .env you use for npm 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's data/ 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), set DASHBOARD_PASSWORD in .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