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; each method mirrors a DSL keyword: route, under, api, docs, static, mount, websocket, sse, session, logging, views, database, model_class, hook, middleware, on_error, on_not_found, helper, plugin, config, secret, host, favicon. Two read back rather than record: databases, auth_config and, with no argument, host. on_compile registers a callback for to_app.
$app->host with no argument 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'). install_kw gives a plugin a keyword of its own. model_auto toggles auto-discovery of MyApp::Model::* (on unless models are named explicitly). caller_class and config_object give a plugin the app's controller namespace and its Punk::Config; new and the compile-time helpers (compile, model_instance, render_view) are called by the framework, not apps.
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.
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.
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.
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), dynamic buckets, 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.nonblockingservers (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 answers500 {"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)