Skip to content
armanghobadiPublic

About

UWeb brings the power of modern web development to resource-constrained devices. It provides a complete HTTP/HTTPS server with WebSocket support, JWT authentication, role-based access control, and automatic API documentation—all in under 50KB of memory.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UWeb

Ultra Web Framework for MicroPython

Version 1.6.0 — WebSocket Authentication, Roles, TLS/HTTPS & OpenAPI

UWeb is a lightweight asynchronous web framework for MicroPython devices. It provides an application layer for embedded web interfaces, device APIs, configuration portals, dashboards and real-time IoT applications on resource-constrained hardware.

UWeb is built around uasyncio and provides a compact Flask-style developer experience while remaining focused on MicroPython constraints.


Key Features

  • Full HTTP/HTTPS Server – Supports GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
  • WebSocket – Full RFC 6455 implementation with rooms and heartbeat
  • JWT Authentication – Token-based auth with refresh tokens and session management
  • Role-Based Access – Admin/User roles with middleware support
  • Auto-Generated API Docs – Swagger-like interactive documentation at /api-docs
  • File System – Static, media, and template serving with caching
  • SSL/TLS – HTTPS with automatic HTTP→HTTPS redirect
  • Rate Limiting – Protect against abuse (configurable)
  • CORS – Cross-origin resource sharing support
  • Middleware – Custom middleware chain for request processing
  • Lightweight – ~50KB memory footprint on ESP32

What's New in 1.6.0

Version 1.6.0 extends the framework substantially while keeping the API small:

  • Authenticated WebSocket routes with @app.ws(..., auth=True)
  • Role-based authorization for WebSocket endpoints
  • HTTP authentication and role helpers
  • Access and refresh tokens with HMAC-SHA256 signing
  • Token revocation/blacklisting
  • In-memory user sessions
  • HTTPS/TLS server support
  • Automatic HTTP → HTTPS redirect when TLS is enabled
  • wss:// WebSocket operation over HTTPS
  • OpenAPI 3.0.3 API documentation through APIRefrence
  • Automatic OpenAPI route discovery
  • Request validation helpers
  • File upload handling with size and filename validation
  • Static/media serving with path traversal protection
  • Per-IP rate limiting
  • Security response headers
  • CORS configuration
  • WebSocket heartbeat/ping-pong management
  • WebSocket rooms and broadcasting
  • WebSocket fragmentation handling
  • WebSocket message-size and connection limits
  • Template caching
  • Bounded request/header/upload sizes

Core Architecture

UWeb is organized into focused components:

Component Responsibility
UWeb Main application, routing, HTTP server and configuration
Request HTTP request data, JSON/form parsing and uploads
Response HTTP response generation and cookies
Validator Input validation and path/filename safety
Auth Tokens, sessions and token revocation
WebSocket RFC 6455 WebSocket connection handling
WebSocketManager Connection limits, rooms, broadcast and heartbeat
FileSystemManager Templates, static files and media
APIRefrence OpenAPI 3.0.3 API documentation

HTTP Server

UWeb uses uasyncio when available and falls back to standard asyncio when appropriate.

Basic server:

import uasyncio as asyncio
from uweb import UWeb

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET",
    debug=False
)

asyncio.run(
    app.run(
        host="0.0.0.0",
        port=80
    )
)

The server supports bounded request parsing and rejects unsupported or oversized requests.

Default limits include:

Limit Default
Request body 256 KB
HTTP headers 16 KB
WebSocket message 8 KB
Upload 128 KB
WebSocket connections 16
Template cache 16 entries
Rate buckets 128

These values can be overridden through UWeb configuration.


Routing

Routes can be registered using method decorators:

@app.get("/items")
def items(req):
    return {"items": []}

@app.post("/items")
def create_item(req):
    return {"created": True}

@app.put("/items/<id:int>")
def update_item(req, id):
    return {"id": id}

@app.patch("/items/<id:int>")
def patch_item(req, id):
    return {"id": id}

@app.delete("/items/<id:int>")
def delete_item(req, id):
    return {"deleted": True}

@app.options("/items")
def options(req):
    return app.json({"ok": True})

@app.head("/items")
def head(req):
    return app.text("")

Or register multiple methods at once:

@app.route(
    "/users/<id:int>",
    methods=["GET", "PUT", "DELETE"]
)
def user_handler(req, id):
    ...

Supported HTTP methods:

GET
POST
PUT
PATCH
DELETE
OPTIONS
HEAD

Typed Path Parameters

Supported parameter types:

Type Description
str Single path segment
int Integer
float Floating-point value
path Remaining path segments

Example:

@app.get("/device/<device_id:str>")
def device(req, device_id):
    return {"device": device_id}

@app.get("/sensor/<sensor_id:int>")
def sensor(req, sensor_id):
    return {"sensor": sensor_id}

@app.get("/value/<value:float>")
def value(req, value):
    return {"value": value}

@app.get("/files/<path:filename>")
def files(req, filename):
    return {"path": filename}

Path handling includes traversal protection for .. components.


Request API

A request handler receives a Request object:

req.method
req.path
req.query_params
req.headers
req.body
req.client_ip
req.user_agent
req.user
req.files

Helpers:

req.header("Authorization")

req.query("page", default="1")

req.json()

req.form()

req.file("avatar")

JSON Requests

@app.post("/api/device")
def device(req):
    data = req.json()

    return {
        "received": data
    }

Invalid JSON results in a validation-style error.


Form Requests

URL-encoded forms:

@app.post("/login")
def login(req):
    data = req.form()

    return {
        "username": data.get("username")
    }

Multipart form data is also supported for small file uploads.


Input Validation

UWeb includes a lightweight Validator:

Validator.string(
    value,
    min_len=1,
    max_len=100
)

Validator.integer(
    value,
    min_val=1,
    max_val=100
)

Validator.float_value(
    value,
    min_val=0
)

Validator.boolean(value)

Validator.email(value)

Validator.filename(value)

Validator.path(value)

Dictionary schemas can be used with JSON/form input:

schema = {
    "name": {
        "type": "string",
        "min_len": 1,
        "max_len": 80
    },
    "port": {
        "type": "int",
        "min": 1,
        "max": 65535
    },
    "enabled": {
        "type": "bool"
    }
}

data = req.json(schema)

Supported schema types:

string
int
float
bool
email

Framework validation should not replace application-specific business rules.


Response API

UWeb provides explicit response helpers:

app.json({"ok": True})

app.html("<h1>Hello</h1>")

app.text("Device online")

app.file(
    binary_data,
    filename="report.pdf"
)

app.redirect("/login")

app.error(
    404,
    "Not found"
)

app.render_template(
    "index.html",
    {"title": "Dashboard"}
)

Handlers may also directly return:

  • dict
  • list
  • bytes
  • strings
  • Response
  • None

Dictionaries and lists are automatically returned as JSON.


Cookies

Responses can create secure cookie attributes:

response = app.json({
    "ok": True
})

response.set_cookie(
    "token",
    access_token,
    max_age=3600,
    http_only=True,
    secure=True,
    same_site="Strict"
)

return response

Cookies can also be deleted:

response.delete_cookie("token")

Authentication

UWeb includes HMAC-SHA256 signed access and refresh tokens.

Create the application with a stable secret:

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET"
)

A stable production secret should be explicitly configured rather than regenerated on every boot.


Login

@app.post("/login")
def login(req):
    data = req.json()

    # Verify credentials in application code.

    return app.login(
        user_id=123,
        extra_data={
            "roles": ["admin"]
        }
    )

The returned structure contains:

access_token
refresh_token
token_type
expires_in
user_id
session_id

Protecting HTTP Routes

@app.get("/profile")
def profile(req):
    result = app.require_auth(req)

    if isinstance(result, Response):
        return result

    return {
        "user_id": req.user["user_id"]
    }

Bearer tokens are accepted through:

Authorization: Bearer <access_token>

The framework also supports extracting a token from:

  • Authorization header
  • token cookie
  • token query parameter
  • WebSocket Sec-WebSocket-Protocol when using the token.<token> form

For security-sensitive applications, prefer the Authorization header or an appropriately protected cookie rather than query parameters.


Role-Based Authorization

Roles are stored in the token's data object.

Example login:

app.login(
    user_id=123,
    extra_data={
        "roles": ["admin", "operator"]
    }
)

Protect a route:

@app.get("/admin")
def admin(req):
    result = app.require_role(
        req,
        ["admin"]
    )

    if isinstance(result, Response):
        return result

    return {
        "message": "Authorized"
    }

A user must be authenticated and have at least one of the required roles.


WebSocket Authentication

Version 1.6.0 adds authentication directly to WebSocket route registration.

@app.ws(
    "/events",
    auth=True
)
async def events(ws, req):
    print(
        "Authenticated user:",
        req.user["user_id"]
    )

    await ws.send({
        "type": "welcome",
        "user_id": req.user["user_id"]
    })

    while ws.connected:
        message = await ws.receive()

        if message is None:
            break

The important point is that authentication happens before the WebSocket upgrade is accepted.

An unauthenticated client receives HTTP 401 Unauthorized instead of being allowed to establish the WebSocket connection.


WebSocket Role Protection

A WebSocket can also require roles:

@app.ws(
    "/admin-events",
    auth=True,
    roles=["admin"]
)
async def admin_events(ws, req):
    await ws.send({
        "type": "authorized",
        "user_id": req.user["user_id"]
    })

    while ws.connected:
        message = await ws.receive()

        if message is None:
            break

Possible authorization failures:

Condition Result
Authentication missing/invalid 401 Unauthorized
Authenticated but role missing 403 Forbidden
Valid authentication and role WebSocket upgrade allowed

This avoids implementing a second competing authentication flow inside the WebSocket handler.


WebSocket Protocol

UWeb implements WebSocket framing with support for:

  • Text messages
  • Binary messages
  • Fragmented messages
  • Ping
  • Pong
  • Close handshake
  • Client masking validation
  • Control-frame validation
  • UTF-8 validation
  • Maximum message size enforcement

Example:

await ws.send("hello")

await ws.send({
    "type": "status",
    "online": True
})

await ws.send_bytes(b"\x01\x02\x03")

message = await ws.receive()

await ws.ping()

await ws.close()

WebSocket Manager

The WebSocketManager maintains active connections.

count = app.ws_manager.get_client_count()

Get a client:

client = app.ws_manager.get_client(client_id)

Rooms

Clients can be assigned to rooms:

app.ws_manager.join(
    client_id,
    "dashboard"
)

Get room members:

clients = app.ws_manager.get_room_clients(
    "dashboard"
)

Leave a room:

app.ws_manager.leave(
    client_id,
    "dashboard"
)

Broadcast:

await app.ws_manager.broadcast(
    "dashboard",
    {
        "type": "telemetry",
        "temperature": 24.5
    }
)

A client can be excluded from a broadcast:

await app.ws_manager.broadcast(
    "dashboard",
    message,
    exclude=client_id
)

WebSocket Heartbeat

The manager periodically checks connections using ping/pong.

Default heartbeat interval:

30 seconds

Connections that fail heartbeat checks are removed.

This is useful for embedded devices where Wi-Fi links may disappear without a clean TCP/WebSocket close.


TLS / HTTPS

UWeb 1.6.0 supports HTTPS through the MicroPython SSL implementation when the target MicroPython build provides the required TLS APIs.

Enable TLS:

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET",
    ssl=True,
    cert_dir="cert"
)

Default certificate paths:

cert/cert.pem
cert/key.pem

You can also provide explicit files:

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET",
    ssl=True,
    cert_file="cert/device-cert.pem",
    key_file="cert/device-key.pem",
    https_port=443
)

HTTP → HTTPS Redirect

When TLS is enabled and the application starts on a different HTTP port:

asyncio.run(
    app.run(
        host="0.0.0.0",
        port=80
    )
)

UWeb starts:

HTTP  → redirect server on port 80
HTTPS → application server on port 443

Requests received over HTTP are redirected to HTTPS while preserving the requested path and query string.

The HTTP redirect port and HTTPS port must be different.


HTTPS WebSockets

When HTTPS is enabled, WebSocket clients should use:

wss://device-ip/events

instead of:

ws://device-ip/events

Example browser client:

const ws = new WebSocket(
    "wss://192.168.1.50/events"
);

ws.onmessage = (event) => {
    console.log(event.data);
};

For authenticated WebSockets, the client must provide the token using one of the supported request mechanisms before the upgrade is accepted.


TLS Requirements

TLS support depends on the MicroPython build.

UWeb expects support for APIs equivalent to:

ssl.SSLContext
ssl.PROTOCOL_TLS_SERVER
context.load_cert_chain(...)

If the platform lacks these capabilities, UWeb raises a clear runtime error rather than silently starting an insecure HTTPS configuration.

Always test TLS support on the exact board and MicroPython firmware used in production.


API Reference / OpenAPI

UWeb includes an APIRefrence component that generates an OpenAPI 3.0.3 specification from registered HTTP routes.

Note: the class name is intentionally APIRefrence for compatibility with the current UWeb API.

Basic configuration:

from uweb import UWeb, APIRefrence

api_docs = APIRefrence(
    enabled=True,
    title="Device API",
    description="Embedded device REST API",
    version="1.0.0"
)

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET",
    api_refrence=api_docs
)

The built-in endpoints are:

/api-docs
/api-docs/openapi.json

The OpenAPI generator includes:

  • HTTP routes
  • HTTP methods
  • operation IDs
  • generated summaries
  • path parameters
  • request bodies for POST/PUT/PATCH
  • configured security schemes
  • common response definitions
  • route tags
  • server information
  • contact information
  • reusable error/success schemas

OpenAPI Security Schemes

Security schemes can be supplied:

api_docs = APIRefrence(
    title="Device API",
    version="1.0.0",
    security_schemes={
        "BearerAuth": {
            "type": "http",
            "scheme": "bearer"
        }
    }
)

When security schemes are configured, generated operations receive the first configured security scheme.


Static Files, Media and Templates

Recommended structure:

project/
├── uweb.py
├── main.py
├── templates/
│   └── index.html
│
├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── app.js
│
└── media/
    ├── images/
    └── uploads/

Built-in URL mappings:

URL Directory
/static/... static/
/assets/... static/
/media/... media/

Example:

/static/css/style.css
/static/js/app.js
/assets/logo.svg
/media/device-logo.png

Both GET and HEAD are registered for these built-in file routes.


Template Engine

UWeb intentionally uses a very small template renderer based on variable replacement.

Template:

<!doctype html>
<html>
<head>
    <title>{{title}}</title>
</head>
<body>
    <h1>{{device_name}}</h1>
</body>
</html>

Python:

@app.get("/")
def home(req):
    return app.render_template(
        "index.html",
        {
            "title": "Device Dashboard",
            "device_name": "UWeb Node"
        }
    )

The framework includes a small in-memory template cache to reduce repeated filesystem reads.

It is not intended to be a full Jinja/Django-style template engine.


File Uploads

Multipart uploads are available through:

@app.post("/upload")
def upload(req):
    uploaded = req.file("file")

    if not uploaded:
        return app.error(
            400,
            "No file"
        )

    ok = app.upload_media(
        uploaded["filename"],
        uploaded["data"]
    )

    return {
        "ok": ok
    }

Uploaded file information contains:

{
    "filename": "...",
    "data": b"...",
    "size": 1234
}

Filename validation blocks unsafe path components and unsupported filename characters.

Uploads are bounded by max_upload_size.


Rate Limiting

UWeb includes per-IP request rate limiting.

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET",
    rate_limit=60
)

The default is:

100 requests/minute/IP

Disable rate limiting:

app = UWeb(
    secret_key="YOUR_LONG_RANDOM_SECRET",
    rate_limit=0
)

Rate limiting is applied to normal HTTP requests.

Applications should still implement endpoint-specific throttling for expensive operations where required.


IP Blacklisting

UWeb maintains an in-memory IP blacklist.

Blocked clients receive:

403 IP blocked

Blacklist management is available through the application API:

app.blacklisted_ips

The blacklist is intentionally in-memory and therefore should not be treated as persistent firewall policy.


CORS

Enable CORS explicitly:

app.enable_cors(
    origins=[
        "https://myfrontend.com"
    ],
    methods=[
        "GET",
        "POST",
        "PUT",
        "DELETE"
    ],
    headers=[
        "Content-Type",
        "Authorization"
    ]
)

For production:

  • Avoid wildcard origins unless required.
  • Restrict allowed methods.
  • Restrict allowed headers.
  • Do not expose administrative APIs unnecessarily.

Security Headers

UWeb applies security-oriented response headers including:

X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Cache-Control: no-store

Applications may also provide additional response headers.


Path Traversal Protection

Filesystem paths are validated before access.

The framework rejects unsafe path components such as:

..

and prevents absolute path injection through the static/media helper path handling.

This applies to:

  • Templates
  • Static files
  • Media files
  • Uploaded filenames

Application-specific authorization should still be applied to sensitive files.


Request Limits

UWeb deliberately bounds resource usage.

Constructor options:

app = UWeb(
    max_request_size=256 * 1024,
    max_header_size=16 * 1024,
    max_ws_message_size=8192,
    max_upload_size=128 * 1024,
    max_ws_connections=16
)

On MicroPython, lower limits may be appropriate for devices with limited RAM.


Middleware

Middleware can be registered with:

def middleware(req, app):
    return None

app.use(middleware)

A middleware may return a Response to stop request processing:

def private_api(req, app):
    if req.path.startswith("/api/private"):
        result = app.require_auth(req)

        if isinstance(result, Response):
            return result

    return None

app.use(private_api)

Middleware is useful for:

  • Authentication
  • Logging
  • Request filtering
  • Application-wide access control
  • Device-specific policies

Authentication Token Lifecycle

Access tokens contain:

user_id
iat
exp
jti
data

Refresh tokens contain:

user_id
type=refresh
iat
exp
jti

The framework supports:

app.login(...)

app.refresh_token(refresh_token)

app.logout(
    token=access_token,
    session_id=session_id
)

Token revocation is stored in memory.

Sessions are also stored in memory:

session_id = app.auth.create_session(
    user_id=123,
    session_data={
        "roles": ["admin"]
    }
)

session = app.auth.get_session(
    session_id
)

A reboot clears in-memory token blacklist and session state.


Minimal Complete Example

import uasyncio as asyncio

from uweb import UWeb, Response, APIRefrence

app = UWeb(
    secret_key="REPLACE_WITH_A_LONG_RANDOM_SECRET",
    rate_limit=60,
    debug=False
)


@app.get("/")
def home(req):
    return app.render_template(
        "index.html",
        {
            "title": "UWeb Device"
        }
    )


@app.get("/api/status")
def status(req):
    return {
        "status": "online",
        "framework": "UWeb",
        "version": "1.6.0"
    }


@app.get("/api/hello/<name:str>")
def hello(req, name):
    return {
        "message": "Hello, " + name
    }


@app.post("/api/echo")
def echo(req):
    return {
        "received": req.json()
    }


@app.post("/login")
def login(req):
    data = req.json()

    # Verify credentials here.

    return app.login(
        user_id=data.get("user_id"),
        extra_data={
            "roles": ["operator"]
        }
    )


@app.get("/api/private")
def private(req):
    result = app.require_auth(req)

    if isinstance(result, Response):
        return result

    return {
        "user_id": req.user["user_id"]
    }


@app.ws(
    "/events",
    auth=True
)
async def events(ws, req):
    await ws.send({
        "type": "connected",
        "user_id": req.user["user_id"]
    })

    while ws.connected:
        message = await ws.receive()

        if message is None:
            break

        await ws.send({
            "type": "echo",
            "message": message
        })


asyncio.run(
    app.run(
        host="0.0.0.0",
        port=80
    )
)

HTTPS Example

import uasyncio as asyncio

from uweb import UWeb

app = UWeb(
    secret_key="REPLACE_WITH_A_LONG_RANDOM_SECRET",
    ssl=True,
    cert_dir="cert",
    https_port=443,
    debug=False
)

asyncio.run(
    app.run(
        host="0.0.0.0",
        port=80
    )
)

Required files:

cert/
├── cert.pem
└── key.pem

Expected operation:

HTTP  : http://device-ip:80/
          ↓
       redirect
          ↓
HTTPS : https://device-ip:443/

WebSocket:
wss://device-ip:443/events

Recommended Project Layout

For a complete embedded application:

project/
│
├── main.py
├── uweb.py
├── config.py
├── README.md
│
├── cert/
│   ├── cert.pem
│   └── key.pem
│
├── templates/
│   ├── index.html
│   ├── dashboard.html
│   ├── settings.html
│   └── api_reference.html
│
├── static/
│   ├── css/
│   │   ├── style.css
│   │   └── dashboard.css
│   │
│   └── js/
│       ├── app.js
│       └── dashboard.js
│
└── media/
    ├── images/
    └── uploads/

For production devices, keep private TLS keys outside source-control repositories.


Requirements

Recommended environment:

  • MicroPython 1.22+
  • uasyncio
  • ujson
  • uhashlib
  • ubinascii

UWeb also uses standard MicroPython modules where available, including:

gc
os
re
sys
time
machine
ssl
urandom

Some are optional or have fallbacks depending on the MicroPython port.

Potential target platforms include:

  • ESP32
  • ESP32-S3
  • RP2040 / Raspberry Pi Pico W
  • ESP8266 with appropriately reduced resource limits

Actual compatibility depends on the MicroPython port, networking stack, filesystem and TLS implementation.


Installation

Copy:

uweb.py

to the device application directory or lib/.

Create application directories:

import os

for directory in (
    "templates",
    "static",
    "media"
):
    try:
        os.mkdir(directory)
    except OSError:
        pass

For HTTPS, also create:

cert/

and copy the certificate and private key into the configured paths.


Production Security Checklist

Before deployment:

[ ] Use a long, unique production secret
[ ] Do not regenerate the secret on every reboot
[ ] Keep TLS private keys protected
[ ] Disable debug mode
[ ] Restrict CORS origins
[ ] Protect management HTTP routes
[ ] Protect sensitive WebSocket routes with auth=True
[ ] Use roles for privileged WebSocket endpoints
[ ] Prefer Authorization: Bearer for API authentication
[ ] Tune request/header/upload limits for available RAM
[ ] Tune WebSocket connection/message limits
[ ] Keep upload file types restricted at application level
[ ] Test malformed JSON
[ ] Test oversized requests
[ ] Test invalid WebSocket frames
[ ] Test authentication failure
[ ] Test role authorization failure
[ ] Test token expiration/revocation
[ ] Test HTTP → HTTPS redirect
[ ] Test WSS on the target firmware
[ ] Test Wi-Fi/network loss and reconnect behavior
[ ] Monitor heap usage under realistic load
[ ] Define watchdog/recovery behavior
[ ] Benchmark the exact production board

API Surface

Main exports:

from uweb import (
    UWeb,
    Request,
    Response,
    WebSocket,
    WebSocketManager,
    Auth,
    Validator,
    FileSystemManager,
    APIRefrence,
    VERSION
)

Current framework version:

from uweb import VERSION

print(VERSION)
# 1.6.0

License

MIT License.

UWeb can be used in personal and commercial projects under the terms of the MIT License.


Credits

UWeb is designed as a practical asynchronous web layer for MicroPython, embedded devices and IoT/IIoT systems.

Build the web interface where the device actually runs.

About

UWeb brings the power of modern web development to resource-constrained devices. It provides a complete HTTP/HTTPS server with WebSocket support, JWT authentication, role-based access control, and automatic API documentation—all in under 50KB of memory.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages