genropy-asgi for Dummies

Run your GenroPy sites on ASGI — same config, same workflow, modern server

The Problem

You have a GenroPy site. It works. It runs on werkzeug (WSGI), which has been the standard for years. But WSGI is a synchronous protocol from 2003. It cannot handle WebSockets, it blocks on every I/O operation, and adding modern features means fighting the protocol itself.

You want to add a REST API with auto-generated Swagger docs, or a WebSocket endpoint for real-time updates, or serve an MCP bridge for AI agents. With pure WSGI, each of these requires a separate server or an awkward hack.

genropy-asgi lets you keep your GenroPy site untouched while running it on a modern ASGI server. Your existing pages, RPCs, and workflows work exactly as before. New ASGI-native features (REST APIs, WebSocket, MCP) run alongside them on the same port.

Browser / Client uvicorn (ASGI server) WsgiGatewayMiddleware "Is this path an ASGI route?" NO → WSGI YES → ASGI GnrWsgiSite your GenroPy site (unchanged) ASGI Applications REST API, Swagger, MCP, WebSocket /index, /mypage, /rpc → GenroPy /api/*, /docs, /_server → ASGI

What changes for you

AspectBefore (gnrwsgiserve)After (gnrasgiserve)
Your GenroPy codeUnchangedUnchanged
Your config filesUnchangedUnchanged
Your workflowgnrwsgiserve mysitegnrasgiserve mysite
WebSocket supportNoNative
REST APIs alongside siteSeparate serverSame port
Server technologywerkzeug (WSGI)uvicorn (ASGI)

Up and Running in 2 Minutes

Install

# From the genro-asgi repository
pip install -e /path/to/genro-asgi
pip install -e /path/to/genro-asgi/contrib/genropy_asgi

This gives you the gnrasgiserve command. GenroPy must already be installed and configured (~/.gnr/environment.xml must exist).

Run your site

# Exactly like gnrwsgiserve — same name, same options
gnrasgiserve mysite

Open http://localhost:8080/index in your browser. That's it. Your site is running on ASGI.

Common variations

# Custom port
gnrasgiserve mysite -p 9000

# Auto-reload on file changes (development)
gnrasgiserve mysite --reload

# Open browser automatically
gnrasgiserve mysite -o

# Remote database (via gnrdaemon SSH tunnel)
gnrasgiserve mysite --remotedb myremote

# All together
gnrasgiserve mysite -p 9000 --reload -o

Key insight: gnrasgiserve replicates steps 1–6 of gnrwsgiserve identically (argument parsing, config resolution, site path lookup, siteconfig loading, option merging, GnrWsgiSite creation). Only step 7 differs: uvicorn instead of werkzeug.

Verify it works

# Your GenroPy pages
curl http://localhost:8080/index

# Server meta endpoint (new, ASGI-native)
curl http://localhost:8080/_server/info

Core Concepts

genropy-asgi has three moving parts. Understanding how they connect is all you need.

GenropyAsgiServer

An AsgiServer that knows how to create GenroPy database connections from config.yaml. It reads databases.*.genropy_app entries, creates GnrApp instances, and registers their .db in the server's db_registry.

WsgiGatewayMiddleware

Sits in the middleware chain. For each HTTP request, it checks if the first path segment matches an ASGI app. If yes, the request flows to the ASGI Dispatcher. If no, it's converted to a WSGI call and executed against the GenroPy site in a thread.

gnrasgiserve CLI

Drop-in replacement for gnrwsgiserve. Same arguments, same config resolution, same site creation. Only the server engine changes from werkzeug to uvicorn.

The two worlds

WSGI WORLD (synchronous, threaded) GnrWsgiSite • pages • RPCs werkzeug patterns • SCRIPT_NAME ASGI WORLD (async, native) REST APIs • Swagger • MCP WebSocket • WSX • Middleware bridge

The gateway is transparent. GenroPy never knows it's running behind ASGI. It receives a standard WSGI environ dict and returns a standard WSGI response. The conversion happens entirely inside the middleware.

Rule of thumb: everything that already works in GenroPy keeps working through the WSGI gateway. Everything new (APIs, WebSocket, MCP) is built natively on the ASGI side.

gnrasgiserve Command Reference

The command-line interface is a drop-in replacement for gnrwsgiserve. If you've used the GenroPy development server before, you already know how to use it.

gnrasgiserve <site_name> [options]

All options

OptionDescriptionDefault
site_nameGenroPy instance name (positional)
-s, --siteSite name (alternative to positional)
-p, --portListening port8080
-H, --hostListening address0.0.0.0
--reloadAuto-restart on file changesoff
--noreloadExplicitly disable file monitor
--nodebugDisable debug mode
-o, --openOpen browser on startupoff
-c, --configPath to gnrserve directory~/.gnr
-n, --nocleanSkip clean restartoff
--restoreRestore from a backup path
--source_instanceImport data from another instance
--remote_editEnable remote editingoff
--remotedb [NAME]Use remote database (via gnrdaemon)
--sslEnable SSLoff
--ssl_certSSL certificate path
--ssl_keySSL key path

Option resolution order

When the same option is set in multiple places, the highest priority wins:

1. CLI arguments         # -p 9000
2. Environment variables  # GNR_WSGI_OPT_port=9000
3. Site siteconfig        # wsgi?port in siteconfig.xml
4. Defaults               # host=0.0.0.0, port=8080, debug=true

Remote database

To develop locally while connecting to a remote database (e.g. production on Hetzner):

# gnrdaemon must be running (it creates the SSH tunnel)
gnrasgiserve mysite --remotedb myremote

The remote DB must be configured in ~/.gnr/instanceconfig/default.xml:

<GenRoBag>
    <remote_db>
        <myremote ssh_host="1.2.3.4" ssh_user="root"
                  host="127.0.0.1" port="5432"
                  dbname="mydb" user="dbuser"
                  password="secret"/>
    </remote_db>
</GenRoBag>

Tip: If you pass --remotedb without a name, the site name is used as the remote name. So gnrasgiserve mysite --remotedb looks for <mysite> in the remote_db config.

Environment variable shortcut

# Set once, then omit the site name
export GNR_CURRENT_SITE=mysite
gnrasgiserve          # uses "mysite"

Configuration

genropy-asgi supports two modes: CLI mode (gnrasgiserve, for existing GenroPy sites) and config.yaml mode (for new ASGI-native projects with GenroPy databases).

CLI mode (gnrasgiserve)

No config.yaml needed. The command reads your existing GenroPy configuration (~/.gnr/environment.xml, siteconfig) exactly like gnrwsgiserve.

gnrasgiserve mysite -p 9000

Config.yaml mode (GenropyAsgiServer)

When building a project that combines GenroPy databases with ASGI-native apps (REST APIs, MCP endpoints), use config.yaml:

# config.yaml
server:
  host: "0.0.0.0"
  port: 8082

databases:
  default:
    genropy_app: "sourcerer"    # creates GnrApp("sourcerer")
  analytics:
    genropy_app: "analytics"    # second database

middleware:
  cors: on
  errors: on

apps:
  api:
    module: "my_project.api:MyAPI"
    db_name: default
    docs: swagger

Minimal Python script

# run.py — fully declarative, config.yaml does everything
from genropy_asgi import GenropyAsgiServer

server = GenropyAsgiServer(server_dir=".")
server.run()

databases section reference

KeyTypeDescription
genropy_appstringGenroPy instance name → creates GnrApp(name) and registers gnr_app.db

Each key under databases becomes a name in the server's db_registry. Apps access it via server.get_db("default") or request.db.

apps section — db_name

Declare which database an app uses:

apps:
  api:
    module: "my_project.api:MyAPI"
    db_name: default          # resolved from db_registry
    docs: swagger             # swagger | redoc | off

When request.db is accessed, it looks up the app's db_name in the registry.

The WSGI Gateway

This is the core innovation of genropy-asgi: a middleware that transparently bridges the WSGI and ASGI worlds. Understanding how it decides where to send requests helps you reason about your application's behavior.

Decision logic

Incoming HTTP Request Is first segment an ASGI prefix? NO WSGI Bridge 1. Read full body 2. Build PEP 3333 environ 3. Run in thread pool YES ASGI Dispatcher Routes to mounted app Fully async, native ASGI WebSocket supported

What counts as an "ASGI prefix"

The gateway builds the list of ASGI prefixes lazily (once, on first request) by scanning:

  • All mounted apps where app_protocol == "asgi"
  • All children of the server's main router

If you mount an app named "api", then any request starting with /api/... goes to ASGI. Everything else goes to GenroPy.

ASGI → WSGI conversion

When a request goes to the WSGI side, the gateway:

# 1. Read the full request body from ASGI
body = await _read_body(receive)

# 2. Build a PEP 3333 environ dict
environ = _build_environ(scope, body)
#   REQUEST_METHOD, PATH_INFO, QUERY_STRING,
#   SERVER_NAME/PORT, wsgi.input, HTTP_* headers...

# 3. Run the WSGI callable in a thread (non-blocking)
status, headers, body = await smartasync(_run_wsgi)(wsgi_app, environ)

# 4. Send ASGI response
await send({"type": "http.response.start", ...})
await send({"type": "http.response.body", ...})

Thread safety: the WSGI callable (GnrWsgiSite) runs in a thread pool via smartasync, so it never blocks the async event loop. Each request gets its own thread, just like werkzeug.

Database Integration

When you need GenroPy's GnrApp database in your ASGI apps (REST APIs, MCP endpoints), GenropyAsgiServer handles the setup.

How it flows

config.yaml genropy_app: "sourcerer" GnrApp("sourcerer") GenroPy app instance db_registry["default"] gnr_app.db registered server.get_db("default") request.db server.db (shortcut)

Three ways to access the database

AccessWhereWhat it does
server.get_db("default")Anywhere with server referenceExplicit lookup by name
request.dbInside request handlersAuto-resolves via app's db_name
server.dbAnywhere with server referenceShortcut for get_db("default")

Script mode (no config.yaml)

from gnr.app.gnrapp import GnrApp
from genro_asgi import AsgiServer, OpenApiApplication
from my_project.api import MyAPI

gnr_app = GnrApp("myapp")
api = MyAPI(gnr_app.db)

server = AsgiServer()
app = OpenApiApplication(routing_class=api, docs="swagger")
server.mount("api", app)
server.run()

Config mode (recommended)

# config.yaml handles database creation
from genropy_asgi import GenropyAsgiServer

server = GenropyAsgiServer(server_dir=".")
server.run()  # databases, apps, middleware all from config

Multiple databases

databases:
  main:
    genropy_app: "myapp"
  reporting:
    genropy_app: "reports"

apps:
  api:
    module: "my_project.api:MyAPI"
    db_name: main
  reports:
    module: "my_project.reports:ReportsAPI"
    db_name: reporting

Advanced Usage

GenropyProxy — mount GenroPy on a prefix

If you don't want GenroPy at root but on a specific path (e.g. /legacy/), use GenropyProxy as a mountable ASGI application:

from genropy_asgi.genropy_proxy import GenropyProxy
from genro_asgi import AsgiServer

server = AsgiServer()

# Mount GenroPy site on /legacy/
proxy = GenropyProxy(site_name="mysite")
server.mount("legacy", proxy)

# Mount your ASGI API on /api/
server.mount("api", my_api_app)

server.run()

GenropyProxy sets app_protocol = "wsgi", so the gateway knows not to intercept requests for it.

Subclassing GenropyAsgiServer

For project-specific needs (custom auth, shared resources):

from genropy_asgi import GenropyAsgiServer

class MyServer(GenropyAsgiServer):
    __slots__ = ("api_key",)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.api_key = self._load_api_key()

    def authenticate(self, scope):
        # Custom auth using GenroPy database
        db = self.get_db("default")
        token = extract_token(scope)
        return validate(db, token)

Startup flow internals

Step 1 Parse CLI args Step 2 Read ~/.gnr/ config Step 3 Resolve site path Step 4 Load siteconfig Step 5 Merge options Step 6 Create site Steps 1-6: identical to gnrwsgiserve gnrwsgiserve (Step 7) werkzeug.run_simple(site) gnrasgiserve (Step 7) AsgiServer + uvicorn server = AsgiServer(host, port) server.wsgi_app = gnr_site server.run() WsgiGateway auto-routes to site

Cheat Sheet

I want to…

I want to…How
Run my GenroPy site on ASGIgnrasgiserve mysite
Use a custom portgnrasgiserve mysite -p 9000
Enable auto-reloadgnrasgiserve mysite --reload
Open browser on startgnrasgiserve mysite -o
Use a remote databasegnrasgiserve mysite --remotedb myremote
Set site via env varexport GNR_CURRENT_SITE=mysite
Override port via env varexport GNR_WSGI_OPT_port=9000
Add a REST API alongside my siteUse config.yaml with apps section
Access GenroPy db in ASGI appserver.get_db("default") or request.db
Mount GenroPy on a prefixUse GenropyProxy(site_name="mysite")
Run fully from configGenropyAsgiServer(server_dir=".").run()
Use multiple databasesMultiple entries in databases: section

Architecture at a glance

ComponentWhat it does
GenropyAsgiServerAsgiServer + GenroPy database loading from config
GenropyProxyMountable ASGI app that wraps a GnrWsgiSite
WsgiGatewayMiddlewareRoutes requests to WSGI (GenroPy) or ASGI (new apps)
gnrasgiserveCLI command (drop-in for gnrwsgiserve)
db_registryNamed database connections, accessible from apps

Files at a glance

FileContains
genropy_asgi/server.pyGenropyAsgiServer class
genropy_asgi/genropy_proxy.pyGenropyProxy (mountable WSGI app)
genropy_asgi/cli.pygnrasgiserve CLI entry point
genro_asgi/middleware/wsgi_gateway.pyWSGI Gateway middleware (genro-asgi core)

Troubleshooting

ProblemSolution
"site name is required"Pass site name: gnrasgiserve mysite
"no ~/.gnr/ found"Create ~/.gnr/environment.xml with GenroPy paths
"no root.py in site"Check path: gnr app sitepath mysite
Port already in uselsof -i :8080 to find the process
--remotedb silent failureCheck gnrdaemon is running + config in default.xml
ASGI app not reachableVerify app is mounted: check /_server/info