NAME

Punk::App - the per-application registry and boot compiler

DESCRIPTION

use Punk; creates one of these per application class. The DSL keywords and plugins record into it; to_app calls "compile", which resolves every target string, flattens guard chains, freezes the route tables and returns the PSGI coderef. Everything wrong croaks at boot; the returned closure is the only code on the request path.

THE REGISTRAR SURFACE

Plugins receive this object. Most of its methods are the DSL keywords themselves: the keyword in the application class forwards its arguments here, so $app->session(expires => '7d') from a plugin does what session expires => '7d'; does in the class. Each records into the registry and returns the app, so calls chain, unless its entry says otherwise. Two read back rather than record: "databases" and "auth_config". "on_compile" registers a callback for to_app.

The keyword methods

route($method, $path, $target, \@guards?, \%opts?)

What get, post, put, patch, del and any call, with the method spelled out. See "get / post / put / patch / del / any" in Punk.

under($prefix, $guard?)

Returns a Punk::Router::Scope, not the app. See "under" in Punk.

websocket($path, $target, \%opts?, \@guards?)

See "websocket" in Punk.

sse($path, $target, \%opts?, \@guards?)

See "sse" in Punk.

api($spec, \%opts?, $scope?)

Returns the Punk::Mount::OpenAPI it built, not the app. See "api" in Punk.

docs($path, $mount?, \%opts?)

See "docs" in Punk.

static($prefix, $dir, %opts)

See "static" in Punk.

markdown($prefix, $dir, %opts)

See "markdown" in Punk and Punk::Mount::Markdown.

mount($prefix, $psgi_app)

See "mount" in Punk.

favicon($path, %opts)

See "favicon" in Punk.

views($name, \%opts?)

See "views" in Punk.

database(%opts) / database($name => \%opts)

See "database / model" in Punk.

model_class(@names)

What the model keyword calls. Names models to load; with no names it turns auto-discovery on explicitly (see "model_auto($on?)"). See "database / model" in Punk.

hook($phase, $code)

See "hook" in Punk.

middleware($code)

See "middleware" in Punk.

on_not_found($code)

See "on_not_found" in Punk.

plugin($name, \%opts?)

See "plugin" in Punk.

config(...)

With no arguments, the loaded configuration data, or undef when no file was loaded. See "config" in Punk.

secret($path)

Returns the resolved secret, not the app. See "secret" in Punk.

session(%opts)

See "session" in Punk.

logging(%opts)

See "logging" in Punk. A to path is opened here, so a path that cannot be written croaks at boot.

cache(%opts) / cache($name => \%opts)

See "cache" in Punk.

csrf(%opts)

See "csrf" in Punk.

cors(%opts)

See "cors" in Punk.

headers_scoped($prefix, \%pairs)

The scope form of headers: what $scope->headers(...) on an "under" in Punk scope calls, with the scope's prefix. Under that prefix these headers apply ahead of the application-wide policy, the longest prefix first. A value of undef drops that header there; a reference croaks. See "headers" in Punk.

proxy(%opts)

See "proxy" in Punk.

rate_limit(%opts)

See "rate_limit" in Punk and Punk::RateLimit.

max_body($bytes)

See "max_body" in Punk.

auth(%opts)

See "auth" in Punk and Punk::Auth.

auth_guard(%opts)

Returns the guard, not the app. See "auth_guard" in Punk.

ua(%opts) / ua($name => \%opts)

See "ua" in Punk and Punk::UA.

upload_dir($path)

See "upload_dir" in Punk.

host($origin?, allow => \@hosts?)

See "host" in Punk. With no argument it reads the declared origin back (undef when the application never declared one), which is how a plugin defaults its own base-URL option; a plugin supporting older Punk should guard with $app->can('host').

publish($topic, $payload)
subscribe($topic, $code, group => $name?)

The cross-worker bus at the application level, where a subscription belongs: made at boot, in the parent, before the server forks. Each returns what the context methods of the same names return, not the app. See "publish / subscribe" in Punk.

install_kw gives a plugin a keyword of its own; keyword and has_keyword call, and detect, one another plugin installed.

databases

my $dbs = $app->databases;    # { default => {...}, analytics => {...} }

The configured databases read back: what the database keyword recorded and what punk.yml applied, keyed by name with default for the unnamed one. A deep copy - a plugin must not be able to edit the connection options of the application it is installed in. Credentials included, since this is the application's own registrar and a plugin that deploys schema needs them. The registrar's other methods record; this one reads, the way host reads back with no argument.

auth_config

my $cfg = $app->auth_config;    # { model, fields, rank, roles, ... } | undef

The frozen Punk::Auth configuration read back, a deep copy, or undef when no auth keyword ran. The rank ladder and the roles hook are what a plugin deciding "may this user act on this row" needs, and it needs them from here rather than from an option of its own: two ladders, one on the plugin and one on auth_guard, drift.

on_compile

$app->on_compile(sub { my ($app) = @_; ... }, __PACKAGE__);

A callback for to_app: run once, in registration order, after every keyword has recorded and before anything is compiled - so it may still use the registrar (read databases, add a route, a hook, a helper) and what it adds is compiled with the rest. The framework's own compile_extras runs after these. A die is a boot croak naming the owner (the second argument, defaulting to the registering package). Calling it after to_app croaks, since the callback would never run.

This is the moment a plugin needs when its plugin line may sit above the database line it depends on, and the one Punk::Plugin::Queue reached, before this existed, by registering a middleware whose constructor runs once at compile. Not a hook phase: those are request phases, and this runs once per compile, never per request.

helper

$app->helper(rid => sub { my ($c, @args) = @_; ... });

Installed as a real method on the application's context subclass at compile time. Collisions with core context methods or another helper croak, naming both owners.

env

The application environment, resolved once at compile: the loaded config's env when there is one, else PUNK_ENV, else production - the same safe default Punk::Config and punk use. Development is opted into: punk dev sets it for its server, or set PUNK_ENV=development yourself. test is the third name with a meaning: a 500 still carries the die text there, which Punk::Test relies on and sets for you when PUNK_ENV is unset.

compile_extras

A boot hook: compile calls it just before the compiled state freezes, after the router and hooks are assembled. The framework's own extras live here (the Punk::DevError wiring in development), so a subclass that overrides it must call SUPER::compile_extras.

install_kw

$app->install_kw(task => sub { my ($name, $target) = @_; ... },
                 __PACKAGE__);

Installs a declaration keyword into the application class - how a plugin adds to the DSL without assigning to a glob. The keyword is a magic CV named for the class it lands in; it forwards its arguments to the code and returns what the code returns, in the caller's context.

Installing over a core keyword croaks. Two owners claiming one name croak, naming both, as helpers do; the same owner installing twice is a no-op, which is what a plugin that installs from both import and register needs. Chains. See "KEYWORDS OF YOUR OWN" in Punk::Plugin.

keyword($name, @args)

$app->keyword(sitemap => blog => sub { ... });

Calls a keyword a plugin installed, from code rather than from the application's package body. The installed code runs with @args in the caller's context and its return is passed through, exactly as the keyword in the application class would have done. This is how one plugin consumes another's declaration - a blog adding its posts to the sitemap - without reaching into the application class by name.

A name nothing installed croaks naming it, because the plugin that provides it is not loaded and doing nothing would be a silence. A core DSL word croaks too; those are this registrar's own methods.

has_keyword($name)

The owner of an installed keyword - the class that installed it - or undef when nothing did. The way to ask whether this application loaded a plugin whose keyword you want to call, rather than sniffing %INC, which says a module was loaded somewhere and not that this application registered it. Core DSL words answer undef.

log

The application Punk::Logger (cached on the app), for logging outside a request - startup, background work: $app->log->info(...). Its lines have no method or path. See Punk::Logger and the logging keyword.

url_for($name, %args)

my $link = $app->url_for('verify', token => $tok, absolute => 1);

The URL of a named route ("Named routes" in Punk), for code that has no request to reach "url_for" in Punk::Context through: a mail built in a queue worker, a test naming a route rather than typing its path. Same names, same captures, same croaks.

Two things follow from there being no request, and both are the safe direction. The prefix is the path on the host keyword alone, because SCRIPT_NAME belongs to a request and there is not one. And absolute builds on the declared host rather than negotiating the allow list, because a background job has no Host header to be allowed or refused.

Only meaningful after to_app: the names are resolved when the routes are.

markdown_pages($prefix)

my $pages = $app->markdown_pages('/blog');

The pages of the markdown mount at that prefix, in its order, as fresh hashrefs (url, path, title, date, updated, summary, meta); see "LISTING" in Punk::Mount::Markdown. Croaks before to_app, and for a prefix with no markdown mount.

FRAMEWORK METHODS

Called by Punk itself; a plugin may read the first three.

caller_class

The application class this registry belongs to, which is the namespace its controllers, models and to_app live under.

config_object

The loaded Punk::Config, or undef when no configuration file was found.

model_auto($on?)

With no argument, whether models are found by scanning <caller_class>::Model:: at compile: true unless models were named and auto-discovery was not also asked for. With an argument, sets it and returns the app.

new(caller => $class)

Builds an empty registry. use Punk calls it once per application class.

model_instance($name)

The model registered as $name, built on first use with its database's options and cached for the process. The cache is dropped when the pid changes, so a forked worker builds its own instances. Croaks when no model is registered, or for an unknown name, listing those there are.

render_view($c, @args)

Hands $c and @args to the configured view engine's render and returns its response. Croaks when no views keyword ran.

COMPILE

compile

Freezes the configuration and returns the PSGI app. Dispatch order: before_request hooks (when any are registered - they run before anything is matched, so they are the only phase a 404, a 405 or a mount reaches), static table, PSGI/static-file mounts (longest prefix first), the dynamic records (a linear scan in declaration order), then 404/405. Matched requests construct the context, run before_dispatch hooks and the route's frozen guard chain (a reference return short-circuits), call the handler, and coerce the return value:

  • a PSGI triplet passes through untouched;

  • a Punk::Response is finalized;

  • a Future is chained on psgi.nonblocking servers (the server awaits it) and awaited inline on blocking ones;

  • anything else is JSON-encoded as 200 application/json, folding in any status/headers set through the context;

  • a die runs on_error, then answers 500 {"errors":[{"message":...}]}.

"Construct the context" happens once per request, not once per phase: when a before_request hook has already built one, the routed match is stored into it rather than a second context being made, so a stash written before routing is the same stash the handler reads.

after_dispatch hooks see the finalized triplet (mutate it, or return a replacement); HEAD responses are stripped of their body.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)