Quick Start Guide

Get Up and Running

This guide walks you from zero to a live Mini-App in three phases: deploy NodeRouter on your server, build your first tool, then publish it to your team.

~20 minutes Docker + a text editor Basic HTML / Python knowledge

Phase 1

Set Up NodeRouter

Quick Deploy — Run one command on your server to set up NodeRouter automatically:

$ bash <(curl -fsSL https://raw.githubusercontent.com/noderoutercom/noderouter-deploy/main/scripts/setup.sh)

Or follow the manual steps below for full control over configuration.

1
Prerequisites

You need two things installed on the server where NodeRouter will run. Nothing else is required — no Node.js, no Python, no cloud account.

Docker Engine

Version 24 or later. Any OS that runs Docker will work.

Docker Compose

Version 2.x (the docker compose plugin, not the legacy docker-compose binary).

2
Run the setup script

Run the one-line command below on your server. The script downloads all compose files, walks you through a short set of prompts, generates every secret automatically, and starts all containers in the correct order.

terminal
bash <(curl -fsSL https://raw.githubusercontent.com/noderoutercom/noderouter-deploy/main/scripts/setup.sh)

Answer the prompts — one by one:

Prompt What to do
Service selection
Configure nginx HTTPS proxy? [y] Type n — skip nginx for now
Configure PostgreSQL? [y] Press Enter — yes, include it
Configure Core? [y] Press Enter — yes, include it
Configure Runner? [y] Press Enter — yes, include it
Secrets
Customize any secret? [n] Press Enter — auto-generate all (JWT, runner secret, admin password, DB password)
PostgreSQL
Deploy bundled PostgreSQL? [y] Press Enter — use the built-in container
POSTGRES_PORT [5432] Press Enter — keep default port
POSTGRES_USER [noderouter] Press Enter — keep default username
POSTGRES_DB [noderouter] Press Enter — keep default database name
Core
CORE_IMAGE [noderouter-core:latest] Press Enter — use the official image
DATABASE_URL [postgresql://…] Press Enter — auto-filled from PostgreSQL step
CORE_PORT [3000] Press Enter — expose on port 3000
Runner
Runner name [node1] Press Enter — or type a custom name
RUNNER_IMAGE [noderouter-runner:latest] Press Enter — use the official image
RUNNER_SECRET [auto-filled] Press Enter — pre-filled from Core
ASYNC_MAX_WORKERS [4] Press Enter — keep default
SYNC_MAX_WORKERS [8] Press Enter — keep default
The script starts all containers in order and prints a summary box with the access URL and all generated credentials. Note down ADMIN_PASSWORD and RUNNER_SECRET before closing the terminal.
3
Open the Admin Panel

Navigate to http://<your-server-ip>:3000/admin in your browser. Log in with the ADMIN_PASSWORD printed in the summary box at the end of setup.

Admin panel  →  http://<your-server-ip>:3000/admin
Password     →  the ADMIN_PASSWORD from the summary box at the end of setup
Phase 1 complete. NodeRouter is running on your server. Your team can already log in — there are just no Mini-Apps installed yet.
Phase 2

Build Your First Mini-App

4
Create the Folder Structure

A Mini-App is a plain folder with six files. Create the folder app_hello_world/ on your local machine and add each file below.

app_hello_world/
├── manifest.json   # app identity, version, data space
├── main.py         # business logic — def execute(data):
├── index.html      # UI template
├── index.js        # component logic & data binding
├── style.css       # component-scoped styles
└── libs/           # local dependencies (optional)
5
Write manifest.json

The manifest tells NodeRouter how to register and run your app. The db_schema field gives your app its own isolated database space — no other app can read or write to it.

{
  "name":         "app_hello_world",
  "version":      "1.0.0",
  "display_name": "Hello World",
  "description":  "My first Mini-App",
  "execution":    "sync",
  "db_schema":    "schema_app_hello_world"
}
execution can be "sync" (instant responses, < 5 seconds) or "async" (background jobs for heavy calculations or batch reports).
6
Write index.html — The UI Template

This file defines what your users see. It is a standard <template> element — NodeRouter injects it directly into the page without an iframe. Use any Bootstrap utility class here; Bootstrap is already loaded by the platform shell.

<!-- index.html -->
<template id="app-hello-world-tpl">
  <div class="p-4">

    <h4 class="fw-bold mb-1">Hello World</h4>
    <p class="text-muted mb-4">My first NodeRouter Mini-App.</p>

    <div class="d-flex gap-2 mb-4">
      <input
        id="inp-name"
        class="form-control"
        style="max-width:240px"
        placeholder="Enter your name"
      />
      <button id="btn-run" class="btn btn-primary">Run</button>
    </div>

    <div id="result" class="p-3 bg-light rounded text-muted">
      Result will appear here…
    </div>

  </div>
</template>
No iframes. Your HTML is mounted directly inside the platform shell, so Bootstrap's grid, utilities, and components all work out of the box — no setup needed.
7
Write index.js — Component Logic

This file registers your app as a W3C Custom Element. All queries use this.querySelector() — scoped strictly inside your component so nothing conflicts with other Mini-Apps running in the same shell.

// index.js
class AppHelloWorld extends HTMLElement {

  connectedCallback() {
    // Clone the template and mount it inside this component
    const tpl = document.getElementById('app-hello-world-tpl');
    this.appendChild(tpl.content.cloneNode(true));

    // Bind the Run button — always scope with this.querySelector()
    this.querySelector('#btn-run')
        .addEventListener('click', () => this.run());
  }

  async run() {
    const name = this.querySelector('#inp-name').value || 'World';
    const resultEl = this.querySelector('#result');

    resultEl.textContent = 'Loading…';

    // Call the platform sync API — NodeRouter routes to main.py
    const res = await fetch('/api/sync/app_hello_world', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name })
    });

    const data = await res.json();
    resultEl.textContent = data.message;
  }
}

customElements.define('app-hello-world', AppHelloWorld);
Sync call: POST /api/sync/app_hello_world — NodeRouter forwards the request body to main.py → execute(data) and returns the result in the same HTTP response.
8
Write main.py — Business Logic

Every Mini-App backend implements one function: execute(data). NodeRouter calls it with the JSON body from the frontend and returns whatever you return from this function. Keep it focused — pure logic, no web framework needed.

# main.py
# The platform calls execute(data) for every sync request.
# 'data' is the JSON body sent from index.js.

def execute(data: dict) -> dict:
    name = data.get("name", "World")

    # Your business logic lives here.
    # You can query your database, run calculations,
    # call external APIs, read files — anything Python can do.

    return {
        "message": f"Hello, {name}! Your Mini-App is live.",
        "status":  "ok"
    }
Always return a dict. Whatever you return from execute() is serialised as JSON and sent back to your frontend component automatically.
9
Write style.css — Component Styles

Optional but recommended. Scope every rule under your component's tag name to avoid any conflict with other Mini-Apps or the platform shell.

/* style.css */
/* Prefix every rule with your component tag to stay scoped */

app-hello-world #result {
  font-family: monospace;
  font-size: 0.9rem;
  min-height: 56px;
}

app-hello-world .btn-primary {
  min-width: 80px;
}
10
Package as a Zip

Zip the contents of the folder — not the folder itself. The zip root must contain the files directly, with no extra wrapping directory.

terminal
# Mac / Linux — zip the contents of the folder
cd app_hello_world
zip -r ../app_hello_world.zip .

# Windows PowerShell
Compress-Archive -Path app_hello_world\* -DestinationPath app_hello_world.zip
Check the zip root — opening app_hello_world.zip should show manifest.json, main.py, etc. at the top level, not inside another folder.
Phase 3

Deploy & Go Live

11
Upload via the Admin Panel

Open the NodeRouter Admin Panel, go to Mini-Apps → Install, and drop your app_hello_world.zip file into the upload area.

admin panel log
 Received  app_hello_world.zip
 Validated manifest.json
 Unpacked  → /shared-file-system/plugins/app_hello_world/
 Database  schema_app_hello_world created
 Runner    hot-reloaded app_hello_world into RAM (0.3s)
 Live      — available to all users now
No restart required. Every other Mini-App keeps running uninterrupted while the new one is being installed.
12
Open Your Mini-App

Your app now appears in the platform menu for every user. Click on Hello World, type a name, hit Run — the response comes back from main.py in real time.

Instant

No CI/CD, no build step, no server restart.

Team-Wide

All logged-in users see the new app immediately.

Updatable

Upload a new zip anytime to update — same instant process.

Troubleshoot

Common Issues

The setup script exits with Docker daemon is not running.

Fix:

  • Mac / Windows: open Docker Desktop from the applications menu and wait for the whale icon to stop animating.
  • Linux: run sudo systemctl start docker, then re-run the setup script.

Core fails to start with bind: address already in use.

Fix: Edit core/.env and change CORE_PORT to a free port (e.g. 3001), then restart core:

docker compose -f core/docker-compose.yml --project-name noderouter-core up -d

Access the admin panel on the new port: http://<server-ip>:3001/admin.

Browser times out on http://<server-ip>:3000/admin even though the container is running.

Common causes:

  • Firewall: open port 3000 on your server (e.g. ufw allow 3000/tcp on Ubuntu).
  • Wrong address: if running locally, use http://localhost:3000/admin, not the external IP.
  • Container not healthy yet: wait 15–20 seconds and try again, or check docker logs noderouter-core.

The runner container is running but shows as offline or disconnected in Admin → Runners.

Fix:

  • Confirm RUNNER_SECRET in runner/.env.node1 exactly matches RUNNER_SECRET in core/.env.
  • Check runner logs for connection errors: docker logs noderouter-runner.
  • If core was not healthy when the runner started, restart the runner: docker restart noderouter-runner.

You can reset it by editing core/.env and restarting the container — no data is lost.

# 1 — set a new password in core/.env
ADMIN_PASSWORD=your-new-password

# 2 — restart core to pick it up
docker restart noderouter-core

The most common cause is a wrong zip structure — the files are inside a sub-folder inside the zip instead of at the root.

Fix: zip the contents of the folder, not the folder itself:

# correct — files are at the zip root
cd app_hello_world
zip -r ../app_hello_world.zip .

Opening the zip should show manifest.json at the top level, not inside another folder.

The script is safe to re-run. Any service whose .env file already exists is skipped automatically — only missing services are configured again.

To reconfigure a specific service, delete its .env file and re-run:

# example: reconfigure core only
rm core/.env
bash scripts/setup.sh

Usually a missing or blank required environment variable.

Fix: check the container logs to see the exact error, then correct the value in the relevant .env file and restart:

# view the last 50 log lines for core
docker logs --tail 50 noderouter-core

# restart after fixing the .env
docker restart noderouter-core