Skip to main content
GenieHelper is a self-hosted, single-server platform. Every component — LLM inference, browser automation, job processing, and the React frontend — runs on one IONOS dedicated machine. Nothing leaves the VPS.

Web architecture

All inbound traffic arrives at Nginx/Plesk, which reverse-proxies to three internal services based on path prefix. The React SPA is served as a static build; the two backend services (AnythingLLM server and Directus CMS) handle API and data concerns respectively.
All Nginx configuration changes must be made through the Plesk UI. Never add a bare location / block — Plesk manages the root location. WebSocket upgrade headers are required for the AnythingLLM chat stream.

Services (PM2)

All long-running processes are managed by PM2. The table below is the authoritative service registry.

anything-llm

Port: 3001
Tech: Node.js, Express, Prisma
AnythingLLM fork. Mounts all GenieHelper custom endpoints and utilities. Calls bootMCPServers() on startup to launch the unified MCP server as a stdio child process.

agentx-cms

Port: 8055
Tech: Directus 11, PostgreSQL
CMS and primary data layer. Owns all Directus collections, flows, roles, and policies. All server-side Directus writes go through the cms.directus MCP plugin — never via raw fetch from business logic.

stagehand-server

Port: 3002
Tech: Playwright (Stagehand fork)
Browser automation service. Used for platform scraping, HITL login sessions, and stat collection from OnlyFans, Fansly, and other platforms that block headless Chrome.

genie-dashboard

Port: 3100
Tech: serve dashboard/dist/
Serves the pre-built React 18 + Vite SPA. Nginx proxies all /app/* and / routes here. The build output in dashboard/dist/ must be rebuilt after any frontend change.

media-worker

Port: none
Tech: BullMQ + Redis
Background job consumer. Processes 9 job types across media-jobs, scrape-jobs, and onboarding-jobs queues at concurrency:1 to stay within the 16 GB RAM ceiling. Split into operations/ modules as of 2026-03-15.

goose-web

Port: 3003
Tech: Goose AI agent
Secondary agent runtime for experimental task execution. Runs independently of the AnythingLLM agent workspace.

Data flow — Creator session

This is the complete request lifecycle for a creator sending a chat message, from browser open through an ACTION-triggered BullMQ job.
1

JWT validation

The React SPA sends every request with a Directus JWT in the Authorization: Bearer header. unifiedAuth.js validates it against Directus /users/me, then bridges to the AnythingLLM Prisma user via anythingllm_user_id. Both user objects are attached to the request context.
2

JIT skill hydration

Before the chat message reaches the LLM, surgical_context.py activate "<task>" runs stimulus propagation across the DuckDB skill graph. The top-N most relevant skills for the current task are surfaced and injected into the agent system prompt. Skills that are irrelevant to the current task are never loaded — they would waste context budget.
3

LLM streaming

The message is forwarded to the AnythingLLM agent workspace via Server-Sent Events. The agent calls MCP tools (cms.directus, taxonomy.core, memory.recall, etc.) as needed. All Directus reads and writes are row-isolated to the authenticated user by Directus policy.
4

ACTION tag interception

When the LLM emits an [ACTION:slug:{params}] tag in its response stream, the Action Runner intercepts it before the text reaches the client. The tag is parsed, the matching flow is loaded from the action_flows Directus collection, and the 14-step execution engine runs the flow deterministically.
5

BullMQ job enqueue

Heavy operations (scraping, media processing, publishing) are handed off to BullMQ rather than blocking the SSE response. A job record is written to media_jobs in Directus, and the media worker picks it up asynchronously at concurrency:1.
6

SSE response

The final agent response — including any stage_update events that drive the React UI — is streamed back to the SPA over the open SSE connection. Stage events are parsed by realtimeEventRouter.js and dispatched to the StageContext reducer, which updates the UI layout without any explicit navigation.

Middleware stack

Every HTTP request to the Express server passes through this chain in order. A failure at any layer returns immediately; the request never reaches the endpoint.
MiddlewareFileResponsibility
CORSExpress built-inOrigin whitelist enforcement
Rate limitexpress-rate-limit10 req/15min on /api/register
unifiedAuthserver/utils/middleware/unifiedAuth.jsValidates Directus JWT, bridges to Prisma user, falls back to AnythingLLM JWT
validatedRequestserver/utils/middleware/validatedRequest.jsMulti-user guard; in multi-user mode, fully delegates to unifiedAuth
subscriptionValidatorserver/utils/subscriptionValidator.jscanPerform() quota check — user_usage_counts vs tier_rate_limits.json, fail-closed
subscriptionValidator is fail-closed. If the quota check fails for any reason — network error, missing record, unexpected exception — it returns allowed: false, reason: 'quota_check_failed'. The operation is denied, not permitted. Do not change this behavior.

The Action Runner pattern

Small open-weight models (7B–8B) cannot reliably call structured JSON tool schemas. The Action Runner bypasses this limitation entirely by having the LLM emit a plain-text tag that the server intercepts before the response reaches the client.

Tag format

[ACTION:slug:{"param1":"value1","param2":"value2"}]
  • slug — matches a flow definition in the action_flows Directus collection
  • The JSON payload is the input to the first step of the flow

Execution model

When genieChat.js detects an ACTION tag in the LLM stream, it pauses SSE forwarding, loads the matching flow from action_flows, and hands off to the Action Runner (server/utils/actionRunner/). The runner executes the flow step-by-step. Results are injected back into the stream as a structured event before SSE forwarding resumes.

20 step types

Step typeWhat it does
directus_readRead items from a Directus collection
directus_writeCreate a new item in a Directus collection
directus_updatePATCH an existing item by ID
directus_deleteDelete an item from a Directus collection
directus_searchFull-text search across a Directus collection
directus_triggerFire a Directus Flow by UUID via webhook
stagehand_startLaunch a new headless Playwright session
stagehand_navigateNavigate the browser session to a URL
stagehand_actNatural language browser action (click, type, scroll)
stagehand_extractExtract structured data from the current page
stagehand_closeEnd and clean up a browser session
stagehand_cookie_loginDecrypt stored credentials and inject cookies into a session
stagehand_extract_cachedExtract with in-process cache check
ollama_generateSingle-turn LLM completion via local Ollama
ollama_chatMulti-turn chat completion via local Ollama
taxonomy_terms_readCached read of taxonomy terms for a platform
taxonomy_platform_maps_readCached read of platform-to-taxonomy mappings
taxonomy_invalidateBust the in-process taxonomy cache
bullmq_enqueuePush a media job onto a BullMQ queue
http_requestArbitrary HTTP request (webhooks, internal API calls)
conditionBranch the flow based on a boolean expression
http_requestMake an outbound HTTP request (internal services only)
Flows are seeded into the action_flows Directus collection. The six currently seeded flows are: scout-analyze, taxonomy-tag, post-create, message-generate, memory-recall, and media-process. New flows can be added without a server restart — the runner loads the flow definition at execution time.

Nginx / Plesk reverse proxy

The Nginx configuration is managed entirely through the Plesk control panel. The following proxy rules are in effect:
Path prefixProxied toNotes
/api/*http://localhost:3001AnythingLLM server; WebSocket upgrade headers required for /api/genie/stream-chat
/directus/*http://localhost:8055Directus CMS admin and API
/*http://localhost:3100Dashboard SPA catch-all
Never add a location / block directly in the Nginx config file. All location blocks that conflict with Plesk’s generated config will be overwritten on the next Plesk regeneration. Make all changes through the Plesk > Domains > geniehelper.com > Apache & nginx Settings panel.
WebSocket headers required for the SSE/streaming endpoint:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;

Build docs developers (and LLMs) love