Routes & sockets¶
Adding routes¶
UNI's web server uses FastAPI under the hood. If you've used FastAPI (or Flask, Django, etc.), routing in UNI will feel familiar — the SDK just adds plugin-scoped prefixes and device authentication on top.
The @uni.router decorator lets you register a FastAPI router: a new area in the web app where you can define your own HTTP routes. For example, a settings page:
from fastapi import Form
from fastapi.responses import Response
credentials = uni.secure_json_file("credentials")
@uni.router
def register_routes():
router = uni.PluginRouter()
@router.get("/settings")
def show_settings(request: uni.Request) -> Response:
return router.render_template(
request,
"settings.html", # create in <your_plugin_folder>/templates/
values={"has_key": credentials.exists()},
)
@router.post("/save")
async def save_settings(
api_key: str = Form(default=""),
) -> Response:
credentials.save({"api_key": api_key.strip()})
return router.redirect("/settings")
return router
Adding a link to your page
If your router includes a /settings route, it automatically gets linked in the main UNI settings interface. If you need sub-pages, show a menu stack on /settings.
Template¶
UNI uses Jinja2 for templating. If you are building a settings page, extend the sdk/settings.html base template for seamless integration into the main layout:
{% extends "sdk/settings.html" %}
{% block body %}
{% if has_key %}
<div class="alert success">API key configured</div>
{% else %}
<div class="alert warning">No API key configured</div>
{% endif %}
<form method="POST" action="{{ url_for('save_settings') }}" class="form">
<label>
API key
<input type="password" name="api_key" placeholder="Enter your API key" />
<small>Get your free API key at openweathermap.org</small>
</label>
<footer>
<button type="submit" class="gradient" data-icon="check">Save</button>
<a href="/settings/plugins/{{ plugin_name }}" class="button">Cancel</a>
</footer>
</form>
{% endblock body %}
Styling
All base templates include the UNI CSS theme.
If you want to build a standalone page without the settings layout, you can extend sdk/blank.html instead to get a clean slate.
Template context¶
Every template rendered with render_template receives these variables automatically:
| Variable | Type | Description |
|---|---|---|
plugin_name |
str |
Your plugin's directory name (e.g. uni_weather) |
url_for |
func |
Generate URLs for your plugin's routes — {{ url_for('route_name') }} |
Route names passed to url_for are scoped to your plugin automatically. Any extra values you pass via the values dict are also available.
Template blocks¶
Both sdk/settings.html and sdk/blank.html provide these blocks:
| Block | Purpose |
|---|---|
body |
Main page content |
head |
Extra <head> elements (styles, meta tags) |
js |
JavaScript at end of body |
Page header¶
Settings pages automatically display a header with your plugin's name, icon, and description from your module docstring. You can override these in render_template():
return router.render_template(
request,
"settings.html",
title="Custom title", # Override plugin name
icon="settings", # Override plugin icon
description="", # Pass empty string to hide description
)
Router details¶
PluginRouter extends FastAPI's APIRouter and auto-configures:
- URL prefix:
/plugins/{plugin_name} - Template directory:
{plugin_directory}/templates - Static file serving:
{plugin_directory}/static(at/plugins/{name}/static/)
All routes are secured with device authentication. Every route handler that renders templates needs an explicit request: uni.Request parameter.
Utility functions¶
| Function | Purpose |
|---|---|
router.redirect(path) |
Redirect relative to this plugin's mount point |
uni.safe_filename(name) |
Sanitize user-provided filenames |
uni.serve_file(base_dir, path) |
Serve a file with path traversal protection |
MODELS_DIR = uni.user_data_path("models")
@router.get("/models/{filepath:path}")
def serve_model(filepath: str) -> Response:
return uni.serve_file(MODELS_DIR, filepath)
Web sockets¶
Plugins needing real-time networking can use Socket.IO. Each plugin gets its own namespace with automatic device authentication.
Availability
The plugin web socket connection is available in cards, web pages, and avatar providers.
Client handlers¶
Cards and web interfaces interact through uni.socket:
uni.socket.on("update_board", updateBoard);
uni.socket.emit("player_move", { move });
uni.socket.off("update_board", updateBoard);
For avatar providers, the socket is under context.socket.
Server handlers¶
Use @uni.socket to register Socket.IO handlers:
@uni.socket
def register_socket_handlers(socket):
@socket.on("player_move")
def handle_move(data):
# socket.emit("event", args) # to current device
# socket.emit("event", args, to=device_id) # to specific device
game = find_game(uni.current_device.id)
game.make_move(data["move"])
socket.emit("update_board", game.board)
@socket.on_connect
def handle_connect():
socket.emit("welcome")
@socket.on_disconnect
def handle_disconnect():
cleanup_abandoned_games(uni.current_device.id)