genropy-asgi for Dummies

## 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.

### What changes for you

Aspect | Before (gnrwsgiserve) | After (gnrasgiserve)
--- | --- | ---
Your GenroPy code | Unchanged | Unchanged
Your config files | Unchanged | Unchanged
Your workflow | `gnrwsgiserve mysite` | `gnrasgiserve mysite`
WebSocket support | No | Native
REST APIs alongside site | Separate server | Same port
Server technology | werkzeug (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

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

Option | Description | Default
--- | --- | ---
`site_name` | GenroPy instance name (positional) | —
`-s, --site` | Site name (alternative to positional) | —
`-p, --port` | Listening port | 8080
`-H, --host` | Listening address | 0.0.0.0
`--reload` | Auto-restart on file changes | off
`--noreload` | Explicitly disable file monitor | —
`--nodebug` | Disable debug mode | —
`-o, --open` | Open browser on startup | off
`-c, --config` | Path to gnrserve directory | ~/.gnr
`-n, --noclean` | Skip clean restart | off
`--restore` | Restore from a backup path | —
`--source_instance` | Import data from another instance | —
`--remote_edit` | Enable remote editing | off
`--remotedb [NAME]` | Use remote database (via gnrdaemon) | —
`--ssl` | Enable SSL | off
`--ssl_cert` | SSL certificate path | —
`--ssl_key` | SSL 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

Key | Type | Description
--- | --- | ---
`genropy_app` | string | GenroPy 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

### 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

### Three ways to access the database

Access | Where | What it does
--- | --- | ---
`server.get_db("default")` | Anywhere with server reference | Explicit lookup by name
`request.db` | Inside request handlers | Auto-resolves via app's `db_name`
`server.db` | Anywhere with server reference | Shortcut 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

## Cheat Sheet

### I want to…

I want to… | How
--- | ---
Run my GenroPy site on ASGI | `gnrasgiserve mysite`
Use a custom port | `gnrasgiserve mysite -p 9000`
Enable auto-reload | `gnrasgiserve mysite --reload`
Open browser on start | `gnrasgiserve mysite -o`
Use a remote database | `gnrasgiserve mysite --remotedb myremote`
Set site via env var | `export GNR_CURRENT_SITE=mysite`
Override port via env var | `export GNR_WSGI_OPT_port=9000`
Add a REST API alongside my site | Use `config.yaml` with `apps` section
Access GenroPy db in ASGI app | `server.get_db("default")` or `request.db`
Mount GenroPy on a prefix | Use `GenropyProxy(site_name="mysite")`
Run fully from config | `GenropyAsgiServer(server_dir=".").run()`
Use multiple databases | Multiple entries in `databases:` section

### Architecture at a glance

Component | What it does
--- | ---
`GenropyAsgiServer` | AsgiServer + GenroPy database loading from config
`GenropyProxy` | Mountable ASGI app that wraps a GnrWsgiSite
`WsgiGatewayMiddleware` | Routes requests to WSGI (GenroPy) or ASGI (new apps)
`gnrasgiserve` | CLI command (drop-in for gnrwsgiserve)
`db_registry` | Named database connections, accessible from apps

### Files at a glance

File | Contains
--- | ---
`genropy_asgi/server.py` | GenropyAsgiServer class
`genropy_asgi/genropy_proxy.py` | GenropyProxy (mountable WSGI app)
`genropy_asgi/cli.py` | gnrasgiserve CLI entry point
`genro_asgi/middleware/wsgi_gateway.py` | WSGI Gateway middleware (genro-asgi core)

### Troubleshooting

Problem | Solution
--- | ---
"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 use | `lsof -i :8080` to find the process
--remotedb silent failure | Check gnrdaemon is running + config in `default.xml`
ASGI app not reachable | Verify app is mounted: check `/_server/info`