Running the API as a Background Service

If an agent drives your board through the local API all day — building boards, importing media, rendering video projects — you do not need the desktop window open at all. The API can run on its own, start automatically, and restart itself if it ever stops. This page explains how the app is put together, how to run the API as a background service on macOS, Windows and Linux, and how to stop it again.

This is an advanced setup. If you use the app normally — opening boards, editing in the window — you do not need any of it.

How the app is put together

GreenLight DashBoard is two programs, not one. Launching it starts the desktop shell, which then starts a second, separate program — the backend:

ProcessWhat it isWhat it does
GreenLight DashBoardThe desktop shellThe window, the menus, the dock/taskbar icon, the built-in terminal
moodboard-backendA self-contained binaryListens on 127.0.0.1:18000, answers every /api/… call, stores your boards, runs renders — and serves the interface itself

That last point is the one worth remembering: the window is a browser view pointed at http://localhost:18000. The interface you look at is served by the backend, not by the desktop shell.

Two things follow from this.

  • An open window is not proof that the API is alive. If the backend stops, the window keeps showing the last thing it painted while every API call fails. The app looks perfectly healthy and answers nothing.
  • The backend can run without the desktop app. The app is a window around it, and that window is optional. Everything below is built on this.

One owner at a time

Never run the service while the desktop app is running. Both want the same port, the same database and the same board folders — and a board folder that is open twice goes read-only. Quit the desktop app before starting the service.

You do not lose the interface by doing this. With the service running, open http://localhost:18000 in any browser and you get the full app.

Where the pieces live

You need four locations: the backend binary, the built interface, the bundled agent skills, and your data folder. They sit in fixed places inside the installation.

macOS

RESOURCES="/Applications/GreenLight DashBoard.app/Contents/Resources"
BACKEND="$RESOURCES/backend/moodboard-backend/moodboard-backend"
DATA="$HOME/Library/Application Support/greenlight-dashboard"

Windows

The installer lets you choose the folder, so check where yours went. The per-user default is %LOCALAPPDATA%\Programs\GreenLight DashBoard; a machine-wide install lands in C:\Program Files\GreenLight DashBoard.

$Resources = "$env:LOCALAPPDATA\Programs\GreenLight DashBoard\resources"
$Backend   = "$Resources\backend\moodboard-backend\moodboard-backend.exe"
$Data      = "$env:APPDATA\greenlight-dashboard"

Linux (.deb)

RESOURCES="/opt/GreenLight DashBoard/resources"
BACKEND="$RESOURCES/backend/moodboard-backend/moodboard-backend"
DATA="$HOME/.config/greenlight-dashboard"

Linux (AppImage)

An AppImage keeps its files inside the image and only mounts them while the app runs, so the backend cannot be started from it directly. Extract it once and use the extracted copy:

./GreenLight-DashBoard-*.AppImage --appimage-extract
RESOURCES="$PWD/squashfs-root/resources"
BACKEND="$RESOURCES/backend/moodboard-backend/moodboard-backend"

Re-extract after every app update, or install the .deb instead, where the files sit on disk permanently.

If you moved your media folder (Options ▸ Show/Change Media Folder), use that path rather than the default when you set MEDIA_DIR below — otherwise the service starts against an empty media root and your boards look like they lost their pictures.

The environment it needs

The backend takes the port as its single command-line argument, and everything else through environment variables. These are exactly what the desktop app passes:

VariableValueWhat it is
MEDIA_DIR<data>/mediaBoard folders, media files, renders
DB_PATH<data>/moodboard.dbThe database
LIBRARY_DIR<data>/librariesSound, asset and music packs
LOG_DIR<data>/logsBackend logs
FRONTEND_DIR<resources>/frontend-distThe interface it serves
GREENLIGHT_SKILLS_DIR<resources>/skillsBundled agent skills
BOARD_AGENT_SKILL_PATH<resources>/skills/greenlight-dash/SKILL.mdThe board skill agents load
GREENLIGHT_RENDER_BASE_URLhttp://localhost:<port>Where render jobs load their render page from
MOCKUP_RENDER_BASE_URLhttp://localhost:<port>Same, for mockup and video renders
ALLOWED_ORIGINShttp://localhost:<port>Browser origin allowed to call the API

The three URL variables must point at the port the service itself is listening on — renders load their render page over HTTP from the same backend.

Try it by hand first

Quit the desktop app, then run the backend directly. macOS and Linux:

RESOURCES="/Applications/GreenLight DashBoard.app/Contents/Resources"   # Linux: /opt/GreenLight DashBoard/resources
DATA="$HOME/Library/Application Support/greenlight-dashboard"          # Linux: $HOME/.config/greenlight-dashboard
PORT=18000

export MEDIA_DIR="$DATA/media"
export DB_PATH="$DATA/moodboard.db"
export LIBRARY_DIR="$DATA/libraries"
export LOG_DIR="$DATA/logs"
export FRONTEND_DIR="$RESOURCES/frontend-dist"
export GREENLIGHT_SKILLS_DIR="$RESOURCES/skills"
export BOARD_AGENT_SKILL_PATH="$RESOURCES/skills/greenlight-dash/SKILL.md"
export GREENLIGHT_RENDER_BASE_URL="http://localhost:$PORT"
export MOCKUP_RENDER_BASE_URL="http://localhost:$PORT"
export ALLOWED_ORIGINS="http://localhost:$PORT"

"$RESOURCES/backend/moodboard-backend/moodboard-backend" "$PORT"

Windows PowerShell:

$Resources = "$env:LOCALAPPDATA\Programs\GreenLight DashBoard\resources"
$Data      = "$env:APPDATA\greenlight-dashboard"
$Port      = 18000

$env:MEDIA_DIR              = "$Data\media"
$env:DB_PATH                = "$Data\moodboard.db"
$env:LIBRARY_DIR            = "$Data\libraries"
$env:LOG_DIR                = "$Data\logs"
$env:FRONTEND_DIR           = "$Resources\frontend-dist"
$env:GREENLIGHT_SKILLS_DIR  = "$Resources\skills"
$env:BOARD_AGENT_SKILL_PATH = "$Resources\skills\greenlight-dash\SKILL.md"
$env:GREENLIGHT_RENDER_BASE_URL = "http://localhost:$Port"
$env:MOCKUP_RENDER_BASE_URL     = "http://localhost:$Port"
$env:ALLOWED_ORIGINS            = "http://localhost:$Port"

& "$Resources\backend\moodboard-backend\moodboard-backend.exe" $Port

It should be answering within a second or two. Check it from another terminal:

curl http://127.0.0.1:18000/api/moodboards

Then open http://localhost:18000 in a browser — the whole interface is there. Press Ctrl+C in the terminal to stop it again.

Running it as a service

Running it by hand works, but nothing brings it back if it stops. Each system has a built-in supervisor that starts the backend at login and restarts it if it ever exits.

macOS — launchd

Save this as ~/Library/LaunchAgents/pro.greenlightdash.backend.plist, with your own paths filled in:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>pro.greenlightdash.backend</string>

  <key>ProgramArguments</key>
  <array>
    <string>/Applications/GreenLight DashBoard.app/Contents/Resources/backend/moodboard-backend/moodboard-backend</string>
    <string>18000</string>
  </array>

  <key>EnvironmentVariables</key>
  <dict>
    <key>MEDIA_DIR</key><string>/Users/you/Library/Application Support/greenlight-dashboard/media</string>
    <key>DB_PATH</key><string>/Users/you/Library/Application Support/greenlight-dashboard/moodboard.db</string>
    <key>LIBRARY_DIR</key><string>/Users/you/Library/Application Support/greenlight-dashboard/libraries</string>
    <key>LOG_DIR</key><string>/Users/you/Library/Application Support/greenlight-dashboard/logs</string>
    <key>FRONTEND_DIR</key><string>/Applications/GreenLight DashBoard.app/Contents/Resources/frontend-dist</string>
    <key>GREENLIGHT_SKILLS_DIR</key><string>/Applications/GreenLight DashBoard.app/Contents/Resources/skills</string>
    <key>BOARD_AGENT_SKILL_PATH</key><string>/Applications/GreenLight DashBoard.app/Contents/Resources/skills/greenlight-dash/SKILL.md</string>
    <key>GREENLIGHT_RENDER_BASE_URL</key><string>http://localhost:18000</string>
    <key>MOCKUP_RENDER_BASE_URL</key><string>http://localhost:18000</string>
    <key>ALLOWED_ORIGINS</key><string>http://localhost:18000</string>
    <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
  </dict>

  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>10</integer>
  <key>ProcessType</key><string>Interactive</string>
  <key>StandardOutPath</key><string>/Users/you/Library/Application Support/greenlight-dashboard/logs/backend-service.log</string>
  <key>StandardErrorPath</key><string>/Users/you/Library/Application Support/greenlight-dashboard/logs/backend-service.log</string>
</dict>
</plist>

Then:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/pro.greenlightdash.backend.plist   # start
launchctl bootout   gui/$(id -u)/pro.greenlightdash.backend                                # stop
launchctl kickstart -k gui/$(id -u)/pro.greenlightdash.backend                             # restart
launchctl print     gui/$(id -u)/pro.greenlightdash.backend | grep -E 'state|pid'          # status

ProcessType Interactive keeps macOS from throttling it as a background job, which matters while renders run. KeepAlive is what restarts it after a crash — which also means a plain kill gets undone about ten seconds later. Use bootout to stop it for real.

Linux — systemd user service

Save as ~/.config/systemd/user/greenlight-backend.service:

[Unit]
Description=GreenLight DashBoard backend
After=network.target

[Service]
Type=simple
ExecStart="/opt/GreenLight DashBoard/resources/backend/moodboard-backend/moodboard-backend" 18000
Environment=MEDIA_DIR=%h/.config/greenlight-dashboard/media
Environment=DB_PATH=%h/.config/greenlight-dashboard/moodboard.db
Environment=LIBRARY_DIR=%h/.config/greenlight-dashboard/libraries
Environment=LOG_DIR=%h/.config/greenlight-dashboard/logs
Environment="FRONTEND_DIR=/opt/GreenLight DashBoard/resources/frontend-dist"
Environment="GREENLIGHT_SKILLS_DIR=/opt/GreenLight DashBoard/resources/skills"
Environment="BOARD_AGENT_SKILL_PATH=/opt/GreenLight DashBoard/resources/skills/greenlight-dash/SKILL.md"
Environment=GREENLIGHT_RENDER_BASE_URL=http://localhost:18000
Environment=MOCKUP_RENDER_BASE_URL=http://localhost:18000
Environment=ALLOWED_ORIGINS=http://localhost:18000
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStopSec=30

[Install]
WantedBy=default.target

Then:

systemctl --user daemon-reload
systemctl --user enable --now greenlight-backend    # start now + at login
systemctl --user stop greenlight-backend            # stop
systemctl --user restart greenlight-backend         # restart
systemctl --user status greenlight-backend          # status
journalctl --user -u greenlight-backend -f          # follow the log

loginctl enable-linger $USER                        # keep it running when you are logged out

Paths with spaces must be quoted as a whole in Environment= lines, exactly as shown. If you run from an extracted AppImage, point ExecStart and the three resource paths at your squashfs-root/resources instead.

Windows — Task Scheduler

Windows has no per-user service manager, so use a small wrapper script plus a scheduled task. Save the wrapper as C:\GreenLight\start-backend.ps1:

$Resources = "$env:LOCALAPPDATA\Programs\GreenLight DashBoard\resources"
$Data      = "$env:APPDATA\greenlight-dashboard"
$Port      = 18000

$env:MEDIA_DIR              = "$Data\media"
$env:DB_PATH                = "$Data\moodboard.db"
$env:LIBRARY_DIR            = "$Data\libraries"
$env:LOG_DIR                = "$Data\logs"
$env:FRONTEND_DIR           = "$Resources\frontend-dist"
$env:GREENLIGHT_SKILLS_DIR  = "$Resources\skills"
$env:BOARD_AGENT_SKILL_PATH = "$Resources\skills\greenlight-dash\SKILL.md"
$env:GREENLIGHT_RENDER_BASE_URL = "http://localhost:$Port"
$env:MOCKUP_RENDER_BASE_URL     = "http://localhost:$Port"
$env:ALLOWED_ORIGINS            = "http://localhost:$Port"

# restart the backend whenever it exits
while ($true) {
  & "$Resources\backend\moodboard-backend\moodboard-backend.exe" $Port
  Start-Sleep -Seconds 10
}

Register it to run at logon:

schtasks /Create /TN "GreenLight Backend" /SC ONLOGON /RL HIGHEST ^
  /TR "powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File C:\GreenLight\start-backend.ps1"
schtasks /Run  /TN "GreenLight Backend"     :: start now
schtasks /End  /TN "GreenLight Backend"     :: stop the wrapper
schtasks /Query /TN "GreenLight Backend"    :: status

Ending the task stops the wrapper loop but can leave the backend itself running, so follow it with Stop-Process -Name moodboard-backend in PowerShell. If you prefer a real Windows service, a service wrapper such as NSSM can run the same executable with the same environment.

Stopping it properly

On a clean stop the backend flushes every pending board file to disk and releases its board-folder locks. A forced kill skips both: you can lose the last few seconds of changes, and a stale lock file is left behind. Always try the graceful stop first.

SituationDo this
Running in a terminalCtrl+C
macOS servicelaunchctl bootout gui/$(id -u)/pro.greenlightdash.backend
Linux servicesystemctl --user stop greenlight-backend
Windows taskschtasks /End /TN "GreenLight Backend" then Stop-Process -Name moodboard-backend
Started by hand, macOS/Linuxkill <pid> — never kill -9 unless it refuses to exit
Started by hand, Windowstaskkill /PID <pid>, adding /F only if that does not work

Renders run a headless browser as a child of the backend. A graceful stop lets it close; a forced kill during a render can leave an orphaned browser process behind.

Checking that it is actually running

The important habit: check the listener, not the app. A visible window proves nothing about the API.

# macOS / Linux
lsof -nP -iTCP:18000 -sTCP:LISTEN
ss -ltnp | grep 18000

:: Windows
netstat -ano | findstr :18000

Or just ask the API:

curl http://127.0.0.1:18000/api/moodboards

If that returns a list, everything downstream — agents, renders, the interface in your browser — will work. If it fails while the app window is open, the backend has stopped and only a restart brings it back.

Renders need a browser

Video, mockup and template renders are performed in a headless Chromium that the backend launches. The app ships the automation library but not the browser itself, so the machine needs one of Google Chrome, Microsoft Edge or Chromium available — on a bare Linux box, sudo apt install chromium or sudo snap install chromium. If your browser lives somewhere unusual, point PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH at it. This requirement is the same for the desktop app; running headlessly does not add it.

Pointing your agent at it

Inside the desktop app the port is assigned at launch (18000 when it is free) and injected into every terminal the app opens as GREENLIGHT_API_BASE_URL, which is what agents are told to read. A service you run yourself has no such luxury and no such problem: you chose the port, it never changes, and an agent can rely on it. Set the same variable for your agent so the bundled skill keeps working unmodified:

export GREENLIGHT_API_BASE_URL=http://localhost:18000

See Using the Board with Hermes, OpenClaw and External Agent systems for what to hand an agent once the API is reachable.

Troubleshooting

SymptomCause and fix
The service will not start, or exits immediatelySomething already holds the port — usually the desktop app. Quit it, or run the service on a different port (remember to change the three URL variables too).
Reads work, but every write returns 423Two backends have the same board folder open, so it went read-only. Make sure only one is running; the lock lives at <board folder>/.database/lock.json.
The agent gets “connection refused” while the app window is openThe backend has stopped. Check the listener as above and restart the service; the window will keep looking normal regardless.
Boards open, but every card has lost its mediaMEDIA_DIR points somewhere other than your real media folder. Check Options ▸ Show/Change Media Folder in the app.
Renders fail with “could not launch a browser”No Chrome, Edge or Chromium on the machine — install one, or set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH.

Backend logs are written to your LOG_DIR, and the service files above add a backend-service.log next to them with everything the process printed.

Was this helpful?

0

Updated

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *