UNI is still under development — Please check back at the end of March
Skip to content

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:

Settings page registration
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:

Settings page HTML
{% 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

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():

Overriding page header
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
Serving user-uploaded files safely
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:

Client-side socket usage
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:

Server-side socket 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)