Backend & Server Layer
The server layer is the central entry point for all Python backend logic in an ESDK application. Every message sent from the JavaScript frontend passes through server/api.py before being routed to the appropriate module.
How the Bridge Works
ESDK connects the JavaScript UI to Python via a C++ bridge. The flow for every call is:
JavaScript (UI)
└─▶ window.invokeBridge({ action: "...", ...params })
│
▼
C++ Engine (engine/main.cpp)
│ strips webview's JSON array wrapper
│ passes raw JSON string to Python
▼
server/api.py → handle_message(message_str)
│
├─▶ public/ (non-sensitive helpers)
└─▶ private/ (sensitive operations)
The C++ side returns whatever string Python returns, and the bridge resolves the JavaScript Promise with the parsed JSON object.
The handle_message Function
server/api.py exports a single function that the engine calls for every bridge request:
import json
from public import utils
from private import secret_processor
def handle_message(message_str):
try:
req = json.loads(message_str)
action = req.get("action")
if action == "ping":
data = req.get("data", "")
return json.dumps({"status": "ok", "result": f"Pong! I received: {data}"})
elif action == "public_demo":
greeting = utils.generate_greeting(req.get("name", ""))
return json.dumps({"status": "ok", "result": greeting})
elif action == "private_demo":
result = secret_processor.process_secure_data(req.get("secret_data", ""))
return json.dumps({"status": "ok", "result": result})
return json.dumps({"status": "error", "reason": "Unknown action"})
except Exception as e:
return json.dumps({"status": "error", "reason": str(e)})
Every response must be a valid JSON string. The convention is {"status": "ok", "result": ...} on success and {"status": "error", "reason": "..."} on failure.
Request & Response Format
Request (JavaScript → Python)
Pass a plain JavaScript object to invokeBridge. It must contain an action string and any additional parameters your handler needs:
window.invokeBridge({ action: "my_action", param1: "value", param2: 42 })
The C++ layer wraps this in a JSON array for transport and then strips the wrapper before handing the raw object string to Python. Python receives it as:
{"action": "my_action", "param1": "value", "param2": 42}
Response (Python → JavaScript)
Always return a json.dumps string. The bridge resolves the Promise with the parsed object, so in JavaScript res will be a plain object:
window.invokeBridge({ action: "my_action", param1: "hello" })
.then(res => {
if (res.status === "ok") {
console.log(res.result); // your return value
} else {
console.error(res.reason); // error message
}
})
.catch(err => console.error("Bridge error:", err));
Adding a New Action
To expose new Python functionality to the UI, add an elif block inside handle_message:
elif action == "get_version":
version = req.get("channel", "stable")
return json.dumps({"status": "ok", "result": f"1.0.0-{version}"})
Then call it from JavaScript:
window.invokeBridge({ action: "get_version", channel: "beta" })
.then(res => console.log(res.result)); // "1.0.0-beta"
For anything beyond a few lines of logic, keep handle_message thin and delegate to a module in public/ or private/.
Routing to Modules
Import your modules at the top of server/api.py and call them inside the relevant action handler:
from public import utils # non-sensitive helpers
from private import secret_processor # sensitive operations
elif action == "greet":
return json.dumps({"status": "ok", "result": utils.generate_greeting(req.get("name"))})
elif action == "save":
return json.dumps({"status": "ok", "result": secret_processor.process_secure_data(req.get("data"))})
See Public Modules and Private Modules for guidance on what belongs in each layer.