Asyncz Dashboard & Admin¶
This page explains what the Asyncz dashboard is, why you might want it, and exactly how to add it to your ASGI application, with or without login, plus how to extend the auth backend to your needs.
This admin is built on top of Lilya but since its ASGI, you can integrate with your favourite framework like Ravyn, FastAPI, Litestar, Starlette.
We use Lilya in our examples since it was built on top of it but feel free to apply this to your favourite framework.
What is it?¶
The Asyncz Dashboard is a web UI (and tiny management API) for inspecting and controlling an AsyncIOScheduler from the browser.
It ships with:
- A clean, responsive UI (Tailwind + HTMX).
- A tasks table with run/pause/resume/remove and bulk actions.
- Drop-in admin wrapper (
AsynczAdmin) that can optionally protect the dashboard with login. - A simple, pluggable auth system so you can bring your own auth (session, headers, JWT, etc).
The dashboard lives in asyncz.contrib.dashboard and the admin wrapper lives in asyncz.contrib.dashboard.admin.
Very Important
You can implement your own AuthBackend but at the moment the AuthGateMiddleware is using sessions until further versions of the admin will come out. For now it will be limited to sessions.
Why use it?¶
- Visibility. See all scheduled tasks, next run times, and states at a glance.
- Control. Trigger, pause, resume, and delete without touching code.
- Safety. Optional login gate so only authorized users can access it.
- Pluggable auth. Use our simple username/password backend or hook into your own user system.
- Embeddable. Mount it as a child app anywhere in your existing Lilya project.
Installation¶
Install the dashboard extra:
pip install asyncz[dashboard]
This pulls in the dashboard templates/assets and minimal dependencies needed for the UI. It will automatically install the remaining dependencies required to make the dashboard shine.
Configuration (settings)¶
Asyncz exposes settings similar to Lilya.
Defaults are provided by asyncz.conf.global_settings.Settings, and you can override them via the environment
variable ASYNCZ_SETTINGS_MODULE.
Default access¶
from asyncz import settings
# dashboard_config is a property that returns DashboardConfig
config = settings.dashboard_config
# e.g.
prefix = config.dashboard_url_prefix # default UI mount prefix
session_middleware = config.session_middleware # session middleware definition
Override settings (your own settings module)¶
Create a module, e.g. myproject.asyncz_settings:
# myproject/asyncz_settings.py
from typing import Any
from asyncz.conf.global_settings import Settings as BaseSettings
class Settings(BaseSettings):
# You can add overrides or utility properties here.
# Example: if your DashboardConfig reads from these:
# debug: bool = True
@property
def dashboard_config(self) -> Any:
"""
Retrieves the default configuration settings for the Asyncz management dashboard.
This property dynamically imports and returns an instance of the `DashboardConfig`
class, providing access to settings like the authentication backend, template
directory, and static files location.
Returns:
An instance of `DashboardConfig`.
"""
from asyncz.contrib.dashboard.config import DashboardConfig
return DashboardConfig()
Point Asyncz to it:
export ASYNCZ_SETTINGS_MODULE="myproject.asyncz_settings.Settings"
Anywhere in your code you can import:
from asyncz import settings
The dashboard itself reads its config via settings.dashboard_config,
which should return an instance of asyncz.contrib.dashboard.config.DashboardConfig (the default implementation).
You can customize your DashboardConfig to change dashboard_url_prefix, session_middleware, template locations, etc.
The DashboardConfig¶
This is where you can change custom dashboard settings to match your preferences. It must be a Python dataclass.
# myproject/dashboard/config.py
from dataclasses import dataclass
from asyncz.contrib.dashboard.config import DashboardConfig
@dataclass
class MyDashboard(DashboardConfig):
title: str = "Dashboard"
header_title: str = "Asyncz "
description: str = "A simple dashboard for monitoring Asyncz tasks."
dashboard_url_prefix: str = "/my-dashboard"
sidebar_bg_colour: str = "#f06824"
Now you can override this in your custom settings.
from typing import Any
from asyncz.conf.global_settings import Settings as BaseSettings
class Settings(BaseSettings):
# You can add overrides or utility properties here.
# Example: if your DashboardConfig reads from these:
# debug: bool = True
@property
def dashboard_config(self) -> Any:
from myproject.dashboard.config import MyDashboard
return MyDashboard()
And this is it, your dashboard now uses your custom settings instead of the default ones.
AsynczAdmin: the dashboard wrapper¶
AsynczAdmin builds a private Lilya object but any Lilya object is an ASGI app containing the dashboard (and optional login/logout).
You mount it into your existing application at a URL prefix.
If you are using Lilya¶
There is a method that does this automatically for you and only works with Lilya applications.
from lilya.apps import Lilya
from asyncz.contrib.dashboard.admin import AsynczAdmin
from asyncz.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler(stores={"default": {"type": "memory"}})
app = Lilya()
admin = AsynczAdmin(
enable_login=False, # or True to require auth
backend=None, # required if enable_login=True
url_prefix="/dashboard", # default comes from settings.dashboard_config
scheduler=scheduler, # pass your scheduler or let it create a default one
include_session=True, # include session middleware from config
include_cors=True, # include permissive CORS
login_path="/login", # where your login view lives within the child app
allowlist=("/login", "/logout", "/static", "/assets"), # paths bypassing auth gate
)
# This only works for Lilya
admin.include_in(app)
@app.on_event("startup")
async def start():
scheduler.start()
@app.on_event("shutdown")
async def stop():
scheduler.shutdown()
If you are using something else¶
Each framework has its own way of including ASGI applications so you must follow the guidelines of the framework and use
the get_asgi_app() function from the admin to mount.
Let us see an example using Starlette.
from starlette.applications import Starlette
from starlette.routing import Mount
from asyncz.contrib.dashboard.admin import AsynczAdmin
from asyncz.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler(stores={"default": {"type": "memory"}})
admin = AsynczAdmin(
enable_login=False, # or True to require auth
backend=None, # required if enable_login=True
url_prefix="/dashboard", # default comes from settings.dashboard_config
scheduler=scheduler, # pass your scheduler or let it create a default one
include_session=True, # include session middleware from config
include_cors=True, # include permissive CORS
login_path="/login", # where your login view lives within the child app
allowlist=("/login", "/logout", "/static", "/assets"), # paths bypassing auth gate
)
# Starlette has the Mount object to handle these situations
app = Starlette(
routes=[
Mount(path=admin.url_prefix, app=admin.get_asgi_app())
]
)
@app.on_event("startup")
async def start():
scheduler.start()
@app.on_event("shutdown")
async def stop():
scheduler.shutdown()
As you could see, Starlette uses the Mount to assemble ASGI applications. This is how you should do it.
The same is applied to FastAPI, Litestar... Each framework that handles ASGI has its mechanism to integrate other ASGI
applications. Asyncz simply made it easier for you by exposing the get_asgi_app().
Constructor options¶
enable_login: bool: Turn on auth for the dashboard. IfTrue, you must supplybackend.backend: AuthBackend | None: The pluggable auth backend (see §6).url_prefix: str | None: Where to mount the dashboard. Defaults tosettings.dashboard_config.dashboard_url_prefix. We always normalize trailing slashes.scheduler: AsyncIOScheduler | None: Your scheduler instance. If omitted, a default in-memory scheduler is created.include_session: bool: Whether to includesettings.dashboard_config.session_middleware.include_cors: bool: Whether to include a permissive CORS middleware for convenience.login_path: str: Relative path within the child app used by the auth middleware for login redirects.allowlist: tuple[str, ...]: Paths that bypass the auth check (e.g.,/login, static assets).
How it works¶
- Builds a
Lilyaapplication with:- (optional) CORS middleware
- (optional) your configured session middleware
- (optional)
AuthGateMiddleware(see §6.3) - The dashboard routes (root + tasks pages)
- If
enable_login=True, adds/loginand/logoutroutes that delegate to your backend
- If you are using Lilya:
- You mount it by calling
admin.include_in(app)which does:app.add_child_lilya(url_prefix, child_app)
- You mount it by calling
- If something else*:
get_asgi_app()is accessible to be used by other frameworks. See the example for more details.
Authentication backends¶
Built-in backend: SimpleUsernamePasswordBackend¶
A tiny session-backed username/password backend that you can wrap around any verification logic you like.
from asyncz.contrib.dashboard.admin import SimpleUsernamePasswordBackend, User, AsynczAdmin
from lilya.apps import Lilya
def verify(username: str, password: str) -> User | None:
# Real world: check DB, call your identity provider, etc.
if username == "admin" and password == "secret":
return User(id="admin", name="Admin")
return None
app = Lilya()
# renders login template + sets session
backend = SimpleUsernamePasswordBackend(verify=verify)
admin = AsynczAdmin(enable_login=True, backend=backend, url_prefix="/dashboard")
admin.include_in(app)
What it does
GET /login- renders a friendly login page (your dashboard's Tailwind-styled template).POST /login- calls yourverify(username, password). On success, stores the user session and redirects./logout- clears the session and redirects to/login.authenticate(request)- reads the session and returns aUserif present.
Security Note
You can tailor the session payload to your policy. Some teams store a minimal identifier
(e.g., user_id) and load the user on each request; others serialize a small user object.
The backend is intentionally simple so you can adjust easily.
Implement your own backend¶
Conform to the AuthBackend protocol:
from typing import Any
from lilya.requests import Request
from lilya.responses import Response
from asyncz.contrib.dashboard.admin.protocols import AuthBackend, User
class MyTokenBackend(AuthBackend):
async def authenticate(self, request: Request) -> User | None:
token = request.headers.get("authorization")
if not token:
return None
# validate token, then:
return User(id="123", name="Jane Admin")
async def login(self, request: Request) -> Response:
# If using token-only auth, you can render a page explaining how to obtain a token,
# or simply redirect to a provider, or return 405.
...
async def logout(self, request: Request) -> Response:
# If you use stateless tokens, this might just redirect to /login
# or call your IdP logout endpoint.
...
def routes(self) -> list[Any]:
# Optional: return extra Lilya paths if you need them
return []
Then:
admin = AsynczAdmin(enable_login=True, backend=MyTokenBackend(), url_prefix="/dashboard")
admin.include_in(app)
How the gate works (AuthGateMiddleware)¶
- Runs inside the child app at your prefix (e.g.,
/dashboard). - Skips auth for paths in
allowlist(e.g.,/login, static assets). - Calls
backend.authenticate(request)for all other requests. - If not authenticated:
- For HTMX requests, returns 401 with
HX-Redirect: /dashboard/login?next=/dashboard/... - Otherwise, returns 303 redirect to
/dashboard/login?next=...
- For HTMX requests, returns 401 with
This means:
- The full dashboard is protected.
- Partial HTMX updates fail gracefully and trigger a redirect, not a broken fragment.
Integrating in your application¶
Minimal, no-login dashboard¶
from lilya.apps import Lilya
from asyncz.contrib.dashboard.admin import AsynczAdmin
from asyncz.schedulers.asyncio import AsyncIOScheduler
app = Lilya()
scheduler = AsyncIOScheduler(stores={"default": {"type": "memory"}})
admin = AsynczAdmin(enable_login=False, url_prefix="/dashboard", scheduler=sched)
admin.include_in(app)
@app.on_event("startup")
async def start():
scheduler.start()
Visit: http://localhost:8000/dashboard
With login (simple username/password)¶
from lilya.apps import Lilya
from asyncz.contrib.dashboard.admin import AsynczAdmin, SimpleUsernamePasswordBackend, User
def verify(u: str, p: str) -> User | None:
# Replace with DB lookup, password hashing, etc.
if u == "ops" and p == "supersecret":
return User(id="ops", name="Ops Admin")
return None
app = Lilya()
admin = AsynczAdmin(
enable_login=True,
backend=SimpleUsernamePasswordBackend(verify),
url_prefix="/dashboard",
)
admin.include_in(app)
Use a shared scheduler from elsewhere in your app¶
If your app already constructs and starts AsyncIOScheduler, pass that instance to AsynczAdmin
so the UI controls the same one:
# somewhere central
sched = AsyncIOScheduler(stores={"default": {"type": "memory"}})
# in your app factory
admin = AsynczAdmin(scheduler=sched, enable_login=False)
admin.include_in(app)
Customizing session / CORS inclusion¶
admin = AsynczAdmin(
include_session=True, # requires that settings.dashboard_config.session_middleware is configured
include_cors=False, # turn off built-in permissive CORS if you have your own
)
Customizing login path and allowlist¶
admin = AsynczAdmin(
enable_login=True,
backend=SimpleUsernamePasswordBackend(verify),
login_path="/signin",
allowlist=("/signin", "/signout", "/static", "/assets"),
)
Check
The admin will mount /signin and /signout for you if your backend exposes them
(the built-in backend mounts /login and /logout by default, but you can wrap or subclass to change that).
Real-world recipes¶
Use your existing user table¶
from passlib.hash import bcrypt
from myapp.db import get_user_by_username
from asyncz.contrib.dashboard.admin import SimpleUsernamePasswordBackend, User
def verify(username: str, password: str) - User | None:
u = get_user_by_username(username)
if not u or not bcrypt.verify(password, u.password_hash):
return None
return User(id=u.id, name=u.display_name, is_admin=u.is_admin)
Only store a minimal session payload¶
If you prefer to store only user_id in the session and fetch the user each request:
from asyncz.contrib.dashboard.admin.protocols import AuthBackend, User
from lilya.requests import Request
from lilya.responses import Response, RedirectResponse
from myapp.db import get_user
class DBSessionBackend(AuthBackend):
def __init__(self, session_key="asyncz_admin_uid"):
self.session_key = session_key
async def authenticate(self, request: Request) -> User | None:
uid = request.session.get(self.session_key)
if not uid:
return None
u = get_user(uid)
return User(id=u.id, name=u.display_name, is_admin=u.is_admin) if u else None
async def login(self, request: Request) -> Response:
if request.method == "GET":
# render your login template
...
form = await request.form()
username, password = form.get("username"), form.get("password")
# validate, then:
request.session[self.session_key] = user.id
return RedirectResponse(form.get("next") or "/", status_code=303)
async def logout(self, request: Request) -> Response:
request.session.pop(self.session_key, None)
return RedirectResponse("/login", status_code=303)
API-only protection (header token)¶
If your deployment serves the dashboard behind a reverse proxy that injects an auth header:
class ProxyHeaderBackend(AuthBackend):
async def authenticate(self, request: Request) -> User | None:
sub = request.headers.get("X-Authenticated-User")
if not sub:
return None
return User(id=sub, name=sub)
async def login(self, request: Request) -> Response:
# Explain “please login via SSO” or redirect to your provider
...
async def logout(self, request: Request) -> Response:
# Possibly call IdP logout or just redirect
...
Notes & Best Practices¶
- Lifecycle. Start your scheduler on app startup and stop it on shutdown for clean tests and reloads.
- Security. Keep the session payload minimal whenever possible, or sign/encrypt cookies if storing anything sensitive.
Prefer server-side lookups (
user_id- DB fetch). - Prefix awareness. The auth middleware computes redirects correctly under the child mount
(e.g.,
/dashboard/login?next=/dashboard/tasks). - HTMX behavior. Unauthorized HTMX requests return
401withHX-Redirect, so partial updates navigate to the login page gracefully. - Styling. The login page uses the same Tailwind setup as the dashboard.
Customize the template (e.g.,
login.html) to match your brand.
API reference¶
from asyncz.contrib.dashboard import create_dashboard_app # the actual UI app factory
from asyncz.contrib.dashboard.admin import (
AsynczAdmin, # the wrapper you mount
SimpleUsernamePasswordBackend, # built-in backend
AuthGateMiddleware, # the gating middleware
AuthBackend, # protocol for custom backends
User, # user value object
)
That's it! With AsynczAdmin, you get a polished dashboard in minutes and the flexibility to fit your organization's
auth model.