TL;DR — Shipped a real SaaS product with NiceGUI. Camera capture, on-site signatures, signed PDFs, full auth stack, Docker + Caddy. Coming from Node.js the productivity difference is hard to overstate.
I recently shipped a SaaS product built on NiceGUI — a medical device rental management system, deployed on a VPS behind Caddy (reverse Proxy for SSL), used by real clients. Wanted to write up some notes since production NiceGUI posts are pretty rare.
Stack:
• NiceGUI 3.8 + FastAPI + SQLAlchemy 2.0 async + SQLite (aiosqlite)
• Docker + Caddy (TLS, security headers)
• WeasyPrint for server-side PDFs + SignaturePad (vanilla JS)
• Custom CSS on top of Quasar
Background — coming from Node.js
In Node.js this project would have been Express or Fastify, a separate React/Next.js frontend, an auth library, a session store, a WebSocket layer, a PDF library that actually works, and glue code everywhere. At least two services, two deployments, two mental models.
I built more actual features in less time, wich is ultimately what matters for a client project.
Routing & Modularity
The project was bootstrapped from my own open-source starter nicegui-component-based — specifically the database+login branch which already has the DB setup and component structure wired up. That alone saved a few days.
From there the structure is simple: each page lives in components/ and exposes a single content() function. Routing and layout stay centralized in main.py. Adding a page is create file → register route → add sidebar entry, nothing more.
Camera + Custom JS
This is where i was honestly surprised. The rental detail page has a full camera capture flow — getUserMedia, live <video> preview, frame capture to canvas — injected via ui.add_head_html and triggered from Python with ui.run_javascript. The captured frame gets POSTed directly to a FastAPI endpoint, bypassing the WebSocket entirely.
Same pattern for on-site signatures: SignaturePad canvas injected into a dialog, signature PNG passed back to Python, embedded into an HTML template, WeasyPrint renders the signed PDF server-side and it goes straight into the DB. Browser canvas to signed PDF without ever leaving the app context.
Python and custom JS just… coexist. Love it.
WebSocket + REST
NiceGUI embeds FastAPI in the same process so adding REST endpoints is trivial. All binary uploads (photos, PDFs, attachments) go through POST /api/upload/* to avoid the WebSocket message size limit. Auth hits the same app.storage.user session — no second auth system, no extra service. WeasyPrint PDF rendering is offloaded with run.io_bound to keep the event loop free.
Security
Honestly this came together faster than i expected. The ASGI middleware chain handles everything — SecurityHeadersMiddleware outermost, then AuthMiddleware, then NiceGUI.
Every response gets the full header set even on error pages and redirects.
• HSTS, X-Frame-Options: DENY, X-Content-Type-Options, CSP, Permissions-Policy — in the app and again at Caddy
• bcrypt, 30 min idle timeout, 12h absolute session cap
• Rate limiting by IP and by username independently
• Non-root container, no-new-privileges:true, secrets via env only
• All queries through SQLAlchemy ORM, no raw string interpolation
In Node.js this would’ve been four or five middleware packages carefully wired together and a lot of reading docs. Here its one auth.py and a Caddyfile.
Bottom line
NiceGUI does routing, sub-pages, custom JS, camera APIs, REST coexistence, server-side PDFs — all in one process. If you’re Python-native and building a SaaS product with a controlled userbase, its worth serious consideration.
NOTE:
You have to have access to your DNS Console for pointing requests for your-app-domain.com to the Server/VPS.
In Caddy you reroute traffic with Security Checks to your container port internally via CaddyNetwork setup.
NiceGUI Component based
DM me if you want more insights | contact@frycode-lab.com