2025 twenty-four merry days of Perl Feed

The Ghost of Web Frameworks Future

PAGI - 2025-12-10

A message from Christmas Yet to Come

It was a cold December night when the Ghost of Web Frameworks Future visited young Petra the Programmer.

"I have something to show you," the spectral figure intoned, gesturing toward Petra's laptop. "The future of Perl web development."

Petra squinted at her screen. She'd been wrestling with a familiar problem: her company's venerable Catalyst application needed real-time features. WebSockets. Server-Sent Events. The kind of persistent, bidirectional connections that made traditional request-response frameworks break out in a cold sweat.

"I've tried everything," she sighed. "I could rewrite the whole thing in Mojolicious, but that's months of work. Or I could bolt on a separate WebSocket server, but then I have two systems to maintain..."

The Ghost smiled knowingly. "What if I told you there was another way? A specification designed from the ground up for asynchronous Perl web applications? One that could run your legacy PSGI apps alongside shiny new async code?"

Petra leaned forward. "Tell me more."

Enter PAGI: The Spiritual Successor to PSGI

PAGI - the Perl Asynchronous Gateway Interface - is a new specification for async-capable Perl web applications. If PSGI was Perl's answer to Python's WSGI, then PAGI is Perl's answer to Python's ASGI.

The key insight is simple: modern web applications need more than request-response. They need:

  • WebSockets for real-time bidirectional communication

  • Server-Sent Events for efficient server push

  • Streaming responses for large files and live data

  • Lifecycle hooks for connection pooling and startup/shutdown

PSGI, brilliant as it was, assumed a synchronous world where each request got a response and that was that. PAGI embraces the asynchronous reality of modern web development.

The PAGI Interface

At its heart, a PAGI application is beautifully simple - an async coderef with three parameters:

use Future::AsyncAwait;
use experimental 'signatures';

async sub app ($scope, $receive, $send) {
    # $scope - hashref of connection metadata
    # $receive - async coderef to get events FROM the client
    # $send - async coderef to send events TO the client
}

The $scope hashref tells you what kind of connection you're dealing with:

  • http - A standard HTTP request/response

  • websocket - A persistent WebSocket connection

  • sse - A Server-Sent Events stream

  • lifespan - Process startup/shutdown lifecycle

Events flow in both directions as hashrefs with a type key. It's event-driven programming, but with the ergonomics of async/await.

Your First PAGI Application

Let's start with the classic: Hello World over HTTP.

# examples/01-hello-http/app.pl
use Future::AsyncAwait;
use experimental 'signatures';

async sub app ($scope, $receive, $send) {
    # We only handle HTTP - throw for anything else
    die "Unsupported: $scope->{type}" if $scope->{type} ne 'http';

    # Send the response headers
    await $send->({
        type => 'http.response.start',
        status => 200,
        headers => [['content-type', 'text/plain']],
    });

    # Send the body and signal we're done
    await $send->({
        type => 'http.response.body',
        body => 'Hello from PAGI!',
        more => 0,
    });
}

Run it with:

pagi-server --app examples/01-hello-http/app.pl --port 5000
curl http://localhost:5000/
# => Hello from PAGI!

Notice how the response is split into two events: http.response.start for headers, and http.response.body for content. This separation is what enables streaming - you can send multiple body chunks with more => 1 before the final more => 0.

Real-Time with WebSockets

Now for the exciting part. Here's a WebSocket echo server:

# examples/04-websocket-echo/app.pl
use Future::AsyncAwait;
use experimental 'signatures';

async sub app ($scope, $receive, $send) {
    if ($scope->{type} eq 'websocket') {
        # Accept the WebSocket connection
        await $send->({ type => 'websocket.accept' });

        # Event loop: receive messages and echo them back
        while (1) {
            my $event = await $receive->();

            if ($event->{type} eq 'websocket.receive') {
                my $message = $event->{text} // $event->{bytes};
                await $send->({
                    type => 'websocket.send',
                    text => "Echo: $message",
                });
            }
            elsif ($event->{type} eq 'websocket.disconnect') {
                last; # Client disconnected, exit loop
            }
        }
    }
    elsif ($scope->{type} eq 'http') {
        # Serve a simple HTML page for testing
        await $send->({
            type => 'http.response.start',
            status => 200,
            headers => [['content-type', 'text/html']],
        });
        await $send->({
            type => 'http.response.body',
            body => '<script>ws=new WebSocket("ws://localhost:5000/ws");'
                  . 'ws.onmessage=e=>console.log(e.data)</script>'
                  . '<p>Open console and type: ws.send("Hello")</p>',
            more => 0,
        });
    }
    else {
        die "Unsupported: $scope->{type}";
    }
}

The pattern is the same: await events from $receive, send responses via $send. But now we have a persistent connection with an event loop that keeps running until the client disconnects.

Test it:

pagi-server --app examples/04-websocket-echo/app.pl --port 5000
websocat ws://localhost:5000/ws
Hello
# => Echo: Hello

The PSGI Bridge: Bringing Legacy Apps to the Future

"But wait," Petra interrupted. "I have thousands of lines of Catalyst code. I can't rewrite everything!"

The Ghost nodded sagely. "You don't have to. PAGI includes a bridge."

One of PAGI's killer features is its ability to wrap existing PSGI applications. Your battle-tested Catalyst, Dancer, or Plack app can run alongside new PAGI code on the same server.

PAGI ships with PAGI::App::WrapPSGI which handles all the translation for you:

use PAGI::App::WrapPSGI;

# Your existing PSGI application (could be Catalyst, Dancer, etc.)
my $legacy_psgi_app = sub {
    my $env = shift;
    return [
        200,
        ['Content-Type' => 'text/plain'],
        ['Hello from legacy PSGI!'],
    ];
};

# Wrap it for PAGI - that's it!
my $wrapper = PAGI::App::WrapPSGI->new(psgi_app => $legacy_psgi_app);
$wrapper->to_app;

The wrapper handles all the complexity: building the PSGI %env from the PAGI scope, collecting request bodies, translating headers, and converting responses back to PAGI events. It even supports PSGI's streaming response interface.

For a real-world Catalyst app, it's just as simple:

use PAGI::App::WrapPSGI;
use MyApp; # Your Catalyst application

my $wrapper = PAGI::App::WrapPSGI->new(
    psgi_app => MyApp->psgi_app
);
$wrapper->to_app;

This means you can:

  • Run your existing Catalyst app under PAGI

  • Add WebSocket endpoints alongside your legacy routes

  • Gradually migrate to async as needed

  • Share connection pools and state between old and new code

The bridge handles the translation between PAGI's event-based model and PSGI's synchronous interface transparently.

PAGI::Simple: For When You Want Express, Not Assembly

"This is powerful," Petra admitted, "but it's also... verbose. Do I really need to manually send response headers every time?"

The Ghost chuckled. "Of course not. Meet PAGI::Simple."

PAGI ships with a micro-framework inspired by Express.js and Sinatra. It handles the low-level event plumbing so you can focus on your application logic:

# examples/simple-01-hello/app.pl
use PAGI::Simple;

my $app = PAGI::Simple->new(name => 'My App');

# Simple text response
$app->get('/' => sub ($c) {
    $c->text('Hello, World!');
});

# Path parameters and JSON
$app->get('/users/:id' => sub ($c) {
    my $id = $c->path_params->{id};
    $c->json({ user_id => $id, name => 'Santa Claus' });
});

# POST with body parsing
$app->post('/api/data' => sub ($c) {
    my $data = $c->json_body;
    $c->json({ received => $data, status => 'ok' });
});

$app->to_app;

WebSockets are just as clean:

# WebSocket chat with PAGI::Simple
use PAGI::Simple;

my $app = PAGI::Simple->new(name => 'Chat');

$app->websocket('/chat' => sub ($ws) {
    $ws->on(open => sub {
        $ws->send('Welcome to the chat!');
    });

    $ws->on(message => sub ($data) {
        # Broadcast to all connected clients
        $ws->broadcast("Someone said: $data");
    });

    $ws->on(close => sub {
        print "Client disconnected\n";
    });
});

$app->to_app;

And Server-Sent Events for real-time dashboards:

# SSE with PAGI::Simple
use PAGI::Simple;

my $app = PAGI::Simple->new(name => 'Dashboard');

$app->sse('/events' => sub ($sse) {
    # Send events periodically
    my $count = 0;
    $sse->on(connect => sub {
        # This would typically be triggered by real events
        $sse->send({ count => ++$count });
    });
});

$app->to_app;

PAGI::Simple includes:

  • Express-style routing with path parameters

  • Built-in JSON parsing and response helpers

  • Session management with pluggable stores

  • Middleware support (CORS, logging, rate limiting, etc.)

  • Static file serving

  • WebSocket rooms and broadcasting

  • SSE channels with pub/sub

The Road Ahead

The Ghost began to fade. "Remember, young programmer: this is a vision of what could be, not what must be. PAGI needs champions to make it real."

PAGI is currently in early beta. It passes its tests, the examples work, but it's not yet battle-tested for production. What it needs now is:

  • Adventurous developers willing to experiment

  • Feedback on the API and specification

  • Testing with real-world applications

  • Framework authors interested in building on PAGI

PAGI is not on CPAN at the time of this writing but the repository is at https://github.com/jjn1056/pagi and it's ready for your experimentation. Just don't use it for production just yet, unless you really, really know what you are doing.

If you're interested in the future of asynchronous Perl web development - if you want WebSockets and SSE without abandoning your existing codebase - PAGI might be exactly what you're looking for.

Petra smiled as dawn broke over her keyboard. She had work to do, but for the first time in months, she felt excited about it.

Maybe the future of Perl web development was brighter than she'd thought.

Getting Started

Ready to try PAGI yourself?

git clone https://github.com/jjn1056/pagi.git
cd pagi
cpanm --installdeps . # Install dependencies
prove -l t/ # Run the tests

# Try the examples
pagi-server --app examples/01-hello-http/app.pl --port 5000
pagi-server --app examples/simple-01-hello/app.pl --port 5000

The repository includes:

  • 9 raw PAGI examples demonstrating the protocol

  • 4 PAGI::Simple examples showing the micro-framework

  • Complete specification documents

  • A reference server implementation

Happy holidays, and happy hacking!

See Also

Gravatar Image This article contributed by: John Napiorkowski <jjnapiork@cpan.org>