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.
- 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
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
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 |
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.
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
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.
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.filesHelpers:
req.header("Authorization")
req.query("page", default="1")
req.json()
req.form()
req.file("avatar")@app.post("/api/device")
def device(req):
data = req.json()
return {
"received": data
}Invalid JSON results in a validation-style error.
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.
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.
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:
dictlistbytes- strings
ResponseNone
Dictionaries and lists are automatically returned as JSON.
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 responseCookies can also be deleted:
response.delete_cookie("token")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.
@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
@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:
Authorizationheadertokencookietokenquery parameter- WebSocket
Sec-WebSocket-Protocolwhen using thetoken.<token>form
For security-sensitive applications, prefer the Authorization header or an appropriately protected cookie rather than query parameters.
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.
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:
breakThe 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.
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:
breakPossible 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.
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()The WebSocketManager maintains active connections.
count = app.ws_manager.get_client_count()Get a client:
client = app.ws_manager.get_client(client_id)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
)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.
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
)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.
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 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.
UWeb includes an APIRefrence component that generates an OpenAPI 3.0.3 specification from registered HTTP routes.
Note: the class name is intentionally
APIRefrencefor 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
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.
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.
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.
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.
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.
UWeb maintains an in-memory IP blacklist.
Blocked clients receive:
403 IP blocked
Blacklist management is available through the application API:
app.blacklisted_ipsThe blacklist is intentionally in-memory and therefore should not be treated as persistent firewall policy.
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.
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.
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.
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 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
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.
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
)
)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
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.
Recommended environment:
- MicroPython 1.22+
uasyncioujsonuhashlibubinascii
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.
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:
passFor HTTPS, also create:
cert/
and copy the certificate and private key into the configured paths.
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
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.0MIT License.
UWeb can be used in personal and commercial projects under the terms of the MIT License.
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.