Build And Deploy
Upgrade Guide
Upgrade an existing GoLazy application to the latest GoLazy release.
Upgrade from v0.1.22 to v0.1.23
This section describes the release candidate. Install the target CLI and run
this transition only after v0.1.23 is published. Establish a passing baseline
and run pending application migrations before the step; rerun migrations,
application tests, and asset checks afterward. Review and commit the completed
transition before moving to another version.
The bundled PostgreSQL development environment no longer uses Docker. Apps
with the postgres add-on should run:
lazy update postgres
mise install
The update replaces installer-owned lifecycle tasks, adds postgres = "17"
to mise.toml unless the app already chose a version, and uses
.tmp/postgres as DB_DATA. Docker volume data is not copied automatically;
dump any database that must be retained before removing the old container,
then restore it into the native server.
The entrypoint migration removes an obsolete context import left by
ListenAndServeContext, retaining it when another function still uses it.
The generated application entrypoint now delegates to lazyapp.Run, which
lets the application binary serve normally or execute built-in and
application-defined CLI commands. The automated upgrade extracts the existing
lazyapp.Config literal into appinit.Config() and rewrites cmd/<app-name>/main.go in generated apps,
falling back to cmd/app/main.go for the conventional app entrypoint:
func App() *lazyapp.App { return lazyapp.New(Config()) }
func main() { os.Exit(lazyapp.Run(appinit.Config())) }
Keep installed add-on imports in generated init/addons_gen.go. Those blank
imports activate runtime registration, including add-on CLI hooks; do not move
them into cli.go or the main entrypoint.
Local stdio MCP is optional and source-compatible. To add a standalone
integration, compose only the required services, register the existing MCP
modules in a lazymcp.Scope, and call scope.ServeStdio(ctx, os.Stdin, os.Stdout). Use lazymcp.NewViewRenderer when the modules render MCP Apps.
Existing HTTP applications need no source migration. Keep logs on stderr and
make application-owned capability resources describe the chosen transport.
MCP and OAuth changes in this version are source-compatible. Tool output
schemas are inferred automatically from the first return value; use
ToolSpec.OutputSchema only for custom JSON encoders or other results that
reflection cannot describe accurately. For an MCP App intended for ChatGPT
submission, set AppSpec.Domain to its dedicated HTTPS widget origin and
declare outbound origins in AppSpec.CSP. GoLazy publishes that metadata on
both list and read responses.
MCP tools with large or widget-only UI data can now implement
lazymcp.ResultMetadata. Keep the typed structured fields concise, mark the
widget-only fields json:"-", and return them from MCPResultMeta; GoLazy
publishes that map as top-level result _meta. This is additive and does not
require changes to existing tools.
The OAuth login page uses framework defaults unless the application sets
LoginTitle and LoginDescription. No migration is required to keep the
existing page working. If an application replaces the login response or adds
headers around it, do not set form-action 'self': Chromium can enforce that
directive on the external client redirect after a successful credential POST.
Keep the form action same-origin and rely on lazyoauth's exact redirect-URI
validation.
Refresh tokens now use signed, self-contained grants and rotate on every use.
For a single development process, the default in-memory family state works
without source changes. Production applications should provide a shared
lazyoauth.RefreshTokenStateStore through
lazyoauth.Config.RefreshTokenStates and keep the signing key stable across
replicas and rollouts. The store persists only the family ID, current jti,
expiry, and revocation flag; its Rotate method must compare-and-update or
revoke on replay atomically. Expired and revoked rows may be cleaned up
opportunistically as long as missing state continues to return
ErrInvalidGrant.
NewDiskStore also persists the new family state. Previously issued opaque
refresh_... tokens remain accepted when their legacy Store record still
exists and are replaced by a signed token on their next successful refresh.
Access tokens issued before this change do not carry the new explicit access
token type and purpose, so clients may need to authorize once after deployment.
An application that used only in-memory state before the rollout will also
require reconnection because that old state cannot survive replacement.
Multi-version upgrades into v0.1.20 or later repair an existing pre-split
google.golang.org/genproto parent dependency before verification. Newer parent
versions and existing app overrides are retained. A version-specific external
replacement keeps the repaired old parent stable through workspace sync; it
does not override newer parent versions. Apps without that dependency do not
gain it. This
avoids the API/RPC split ambiguity
when older application tools remain in the module.
The CLI bundles the complete sample v0.1.22-to-v0.1.23 file manifest with
source commit IDs and content hashes. Skills and named Go migrations apply
the executable changes; Go commands own module metadata. Merge relevant
changes from the tagged sample README.md and AGENTS.md into your own
project guidance manually, retaining project-specific instructions.
Skill imports keep the application's full Go module path, and command paths use its final name. Unchanged generated skills should upgrade without a local- edit conflict. A customized display-name literal in the app configuration example is preserved if every other byte matches the previous or target template. Other customizations still use the conflict and backup flow.
The generated .skills/golazy-framework guidance now covers optional
top-level clients/ packages. Existing application source does not need to
move. Use the convention when provider authentication, SDK setup, transport,
wire parsing, hooks, or lifecycle behavior would otherwise spread through
domain services.
Generated applications now also include .skills/golazy-upgrade. When an
agent performs an application upgrade, that workflow establishes a passing
baseline, runs pending migrations from the current version, advances with
untargeted lazy upgrade one released version at a time, runs the target
version's migrations, repairs and verifies the resulting application, and
commits the completed transition before starting another version. Do not use
one multi-version --target command when following this workflow.
Use the application's documented migration task against its local or upgrade- test database. A migration command that fails or cannot reach its database blocks the version. When no application migration mechanism is configured—or when it reports no pending migrations—record that explicitly in the upgrade commit and final report rather than silently skipping the gate. Production migrations require separate authorization and the deployment's documented backup and rollout procedure.
The guidance is intentionally not a wrapper mandate. Keep canonical Go types
such as *pgxpool.Pool or an upstream client when an application wrapper would
only shadow a large, stable API. Prefer service-owned types when values carry
business meaning, and normalized client-owned types when another service type
would merely duplicate fields.
Run the normal upgrade to synchronize the generated skill tree while preserving customized files through the existing conflict flow:
lazy upgrade --target v0.1.23
After reaching v0.1.23, running lazy upgrade again rechecks the current
generated skill manifest and installs a missing golazy-upgrade skill while
preserving customized skill files through the normal conflict flow.
Upgrade to v0.1.22
OAuth servers remain RFC 9207-compliant by default. If an affected OAuth
client discards the valid iss callback parameter before validation, set
DisableAuthorizationResponseIssuerMetadata: true in that application's
lazyoauth.Config as a temporary compatibility bridge and remove it once the
client has corrected its callback parser. Existing applications do not need
this option unless they reproduce that client-side failure.
The v0.1.21 upgrade updates the golazy.dev module requirement and
synchronizes the generated .skills/golazy-framework tree with the current
sample application:
lazy upgrade --target v0.1.21
Apps without the skill receive the complete tree. Files that still match the
v0.1.20 template are updated or removed automatically, and files already at
the target version are left alone. If a skill file has local changes,
lazy upgrade uses the same diff, dated-backup, and conflict-file flow as
other template-owned files instead of overwriting it.
Running lazy upgrade again after go.mod already requires v0.1.21 rechecks
the skill tree. This repairs missing or stale generated files while preserving
customized files through the same conflict handling.
The add-on framework is additive. Existing app-owned dependency, migration,
jobs, route, helper, view, public-file, and PostgreSQL wiring continues to
work; lazy upgrade does not convert it automatically. Use
lazy add --dry-run postgres to inspect the new generated wiring before opting
in, and remove or reconcile equivalent hand-written setup before applying it.
New add-on selections live in committed addons.toml and are updated only by
explicit lazy update commands.
Use repeated --config key=value flags for non-secret add-on settings and
--unset-config key to remove one. These values are committed and compiled
into the generated add-on selection, so keep passwords, tokens, and database
URLs in environment variables. For example,
lazy add --config database_url_variable=APP_DATABASE_URL postgres stores only
the environment-variable name.
Upgrade from v0.1.19
GoLazy v0.1.20 is source-compatible for applications already on v0.1.19.
It adds default in-memory auth, PostgreSQL auth, cache byte ceilings, automatic
OpenTelemetry startup, route/controller metric labels, richer route traces,
lazyjobs schedules and queue limits, response-writer and session helpers,
generated BeforeAction arguments, typed form validation helpers, and the
latest cheatsheet snippets. Existing application code can keep running, but
production deployments should review the operational defaults below before
rolling out.
Telemetry also remains source-compatible. lazyapp.New now creates a
lazytelemetry.Telemetry instance automatically and registers it in the app
dependency lifecycle. Deployments that already set
OTEL_TRACES_EXPORTER=otlp or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT will start
exporting request spans through the standard OpenTelemetry trace exporter after
the upgrade, so confirm the endpoint, protocol, and sampler settings before
rolling this into production. Prometheus request metrics now populate the
route label with the matched route pattern. The http_server_requests_total
and http_server_request_duration_seconds series also include controller and
action labels for matched controller routes, so dashboards or alerts that
aggregate those metrics may need to include or drop those labels explicitly.
Cache behavior remains source-compatible. Applications that rely on the
lazyapp.New default in-memory backend now get a 50 MiB cache byte ceiling.
Deployments that need another default-backend ceiling can set
LAZYAPP_CACHE_SIZE, using bare bytes or case-insensitive Kb, Mb, or Gb
units. Applications that need code-owned cache behavior can still provide their
own lazyapp.Config.Cache.Backend with inmemorycache.Options.MaxSizeBytes.
Production deployments with tight memory limits should also set GOMEMLIMIT
below the container memory limit.
The new MCP, auth, OAuth, JWT, and rate-limit packages are additive. Existing
applications do not need source changes unless they opt in to
lazyapp.Config.Auth, lazyapp.Config.OAuth, or lazyapp.Config.MCP.
lazyapp.New now includes a default in-memory lazyauth backend with zero
users. Set LAZYAUTH_DEFAULT_PASS to create the bootstrap admin user, and
set LAZYAUTH_DEFAULT_USER to use a different username. Apps with custom
password, file, PostgreSQL, SSO, or account-service auth should keep using
lazyapp.Config.Auth.
The new lazyjobs scheduling and queue-limit APIs are additive. Existing
Enqueue, EnqueueIn, EnqueueAt, job definitions, and backends continue to
work. Apps that register schedules must use a backend that implements
lazyjobs.SchedulerBackend; the bundled in-memory backend and
golazy.dev/pg/pgjobs do. PostgreSQL apps should include the latest
pgjobs.Migrations() source before enabling scheduled jobs so the
lazy_job_schedules table and schedule_key metadata exist.
lazycontroller.Base.ResponseWriter() is additive and does not require source
changes. Apps with custom base controllers that only copied the current request
or response writer in BindRequest can remove that app-level lifecycle method,
use Request() and ResponseWriter(), and keep request setup in
BeforeAction.
Generated BeforeAction arguments are additive and do not require source
changes. When you next touch a protected controller group, prefer a typed
generator such as GenUser() (*User, error) plus
BeforeAction(user *User) error over a CurrentUser view value or repeated
auth-only user parameters on every action. Keep the login redirect or status
mapping in HandleError.
The controller route redirect, session, flash, and Param helpers are
additive. Existing RedirectTo, Redirect, lazysession.Get(r), and
r.PathValue(...) code keeps working. New or touched controllers should prefer
RedirectToRoute with RedirectStatus(http.StatusSeeOther) after successful
create/update actions, PermanentRedirectTo or PermanentRedirectToRoute for
canonical moves, and SessionGet for read-only session access. Use
SessionSet, SessionDelete, FlashSet, and FlashGet for writes so the
framework can mark the session dirty and save the cookie only when needed.
Application auth wrapper types remain app-owned generated values; no
lazy upgrade source rewrite is required.
Generated form arguments are also additive. Existing direct c.Decode(&form)
calls can keep working, but new or touched form actions should usually move the
request input struct into a nearby file such as password_form.go, implement
GenPasswordForm by allocating the form and calling c.Decode(form), and have
the action receive *PasswordForm. Decode errors returned by the generator
skip the action and go through the same HandleError path as action errors.
Application validation failures after a create or update should come from
ordinary errors returned by lazyerrors.Validator(form) and optional form-owned
Validate() error methods. Render the form view with
http.StatusUnprocessableEntity, set the joined validation error for
field_errors_for / errors_for, and redirect rather than render after
successful creates.
Run the standard module update:
lazy upgrade --target v0.1.20
If an app adopted early migration examples with app-owned files in
migrations/postgres and uses lazymigrate.ForDatabase, move those files to
db/postgres/migrations before relying on the helper. Apps that intentionally
keep another layout can continue using an explicit
lazymigrate.FromFS(files, "migrations/postgres").
Upgrade from v0.1.18
GoLazy v0.1.19 adds opt-in PWA and browser-worker support plus bundled
application migrations through lazyapp.Config.Migrations. Existing
applications do not need source changes unless they want to become installable,
register worker scripts through GoLazy, prepare browser push subscriptions, or
let the app binary run migrations with LAZYAPP_MIGRATE=up or
LAZYAPP_MIGRATE=auto. The matching lazy development panel also adds
Terminal, Docs, and Media tabs. Terminal and Docs do not require application
source changes. The Media tab shows storage, file, and variant data when an app
opts in by passing named storages, *lazyfiles.Files, or *lazymedia.Media
through lazyapp.Config.
GoLazy also starts permanently redirecting GET and HEAD trailing-slash
requests for registered routes to the same route without trailing slashes. That
canonicalization is automatic and does not require application source changes.
Run the standard module update:
lazy upgrade --target v0.1.19
Then add lazyapp.Config.PWA and {{pwa}} in your layout only if the
application should opt in. Use lazyapp.Config.Workers for app-owned workers
that are not part of the PWA setup. Read
PWA, Workers And Notifications.
Add lazyapp.Config.Migrations only when the app should bundle migration
backends. Object-storage deployments can also opt in to
golazy.dev/lazyassets/assetmigrate to upload the current asset registry
during LAZYAPP_MIGRATE=up or LAZYAPP_MIGRATE=auto. This is additive;
lazy upgrade does not rewrite application source for migrations or asset
uploads.
Upgrade from v0.1.17
GoLazy v0.1.18 makes lazyapp.Config.Jobs run after Dependencies with the
dependency-initialized app context. Applications without Jobs do not need
source changes.
Run the automated migration:
lazy upgrade --target v0.1.18
The migration updates the golazy.dev module requirement and wraps static job
configs:
Jobs: lazyapp.Jobs(lazyjobs.Config{
Define: jobs.DefinedJobs,
})
Apps with durable job backends can instead return the config from a function
that reads shared dependencies from context, such as pg.FromContext(ctx) for
PostgreSQL-backed jobs. Read Background Jobs and
PostgreSQL.
Upgrade from v0.1.16
GoLazy v0.1.17 changes two controller APIs:
c.SetLayout("name")becomesc.Layout("name").c.CacheKey(...)andc.CacheKeyF(...)now returntruewhen an existing cached body was written. Returnnilimmediately on that hit, then continue setting controller data and rendering on a miss. Render cache keys now also include the app build version, and active render variants when present, so existing in-memory render cache entries naturally miss after a new build.
Run the automated migration:
lazy upgrade --target v0.1.17
The migration updates the golazy.dev module requirement, rewrites common
SetLayout, CacheKey, and CacheKeyF controller call shapes, and keeps the
normal upgrade checks in place.
Upgrade from v0.1.15
GoLazy v0.1.16 does not require source rewrites for applications that already
run on v0.1.15. The release adds framework cache support, deferred template
values, route-link helpers, richer telemetry, and the DevTools-style lazydev
panel. Use lazy upgrade to update the golazy.dev module requirement and run
the normal follow-up checks.
After updating the CLI, the default lazy development proxy may show a local
HTTPS setup page on the plain HTTP address. Install the GoLazy local
certificate authority once per machine or browser trust store to use the
development panel and proxied app over HTTPS on the same port. This does not
require application source changes.
Install or update the matching CLI:
curl -fsSL https://golazy.dev/install.sh | sh
lazy --version
Run the application migration:
lazy upgrade --target v0.1.16
lazy upgrade applies the framework module update with go get from the
release's go.mod manifest, then runs the standard follow-up checks. It does
not edit application source for the v0.1.15 -> v0.1.16 step.
Apps that adopted the early JSONL repositories for lazyfiles or lazymedia
should update their imports and constructors:
filesRepo, err := filesjsonl.New("storage/files.log.jsonl")
mediaRepo, err := mediajsonl.New("storage/variants.log.jsonl")
Use golazy.dev/lazyfiles/jsonl and golazy.dev/lazymedia/jsonl for those
aliases. The parent packages continue to expose the Repository interfaces
used by lazyfiles.Files and lazymedia.Media.
PostgreSQL-backed apps can now use golazy.dev/pg/pgfiles and
golazy.dev/pg/pgmedia for durable file and media metadata, and
golazy.dev/pg/pgstorage anywhere a lazystorage backend is useful, including
lazyassets.Upload.
Asset commands may change which Node package manager they use after the CLI
upgrade. lazy js and lazy tailwind now prefer active installed mise tools
in pnpm, yarn, bun, node order, with node mapped to npm. If mise
cannot provide one of those tools, the commands fall back to direct npm /
npx. No application source migration is required for this behavior change.
After updating to a CLI with the current lazy js behavior, app-owned files
under app/js regenerate as readable, unbundled, content-hashed assets.
Manifest library entrypoints remain bundled. App-owned importmap specifiers are
now relative to app/js: layouts import app.js, and generated Stimulus code
imports controllers such as controllers/hello_controller.js. Run
lazy upgrade to rewrite old /js/... imports; the v0.1.17 migration runs
lazy js automatically when js.toml is present. Commit the refreshed
importmap and lazyshaft files when the generated output changes.
lazy dump, lazy load, and the managed service preparation flow are
additive. No source migration is required. Apps can opt in by adding service
lifecycle tasks under .mise/tasks/<service>/ and, when explicit ordering is
needed, listing service names in lazy.toml.
If you are upgrading from v0.1.14 or earlier, first run the earlier
automated migrations or read the previous versioned upgrade guides. The
v0.1.14 -> v0.1.15 step still migrates dependency initializers and SEO
defaults before the v0.1.16 module update runs.
Review New Features
Caching
lazyapp.App now owns a default cache and exposes it through
golazy.dev/lazycache. Controller views, partials, and Turbo frame bodies can
use the cache helpers. Render cache keys include the app build version and
active render variants when present. Lazydev exposes cache state, keys, entry
content, and On/Off actions. Read Caching.
Deferred Template Values
Controllers can use SetLater and SetWhenNeeded when a template value is
expensive or only needed in some render paths. Templates resolve the value with
.Value. Read
Template Data And Helpers.
Route Link Helpers
Templates can use link_to, path_for, attr, data, and unless_current
to render escaped links to named routes without handwritten anchor tags. Read
Template Data And Helpers.
Telemetry
GoLazy can enable OpenTelemetry-compatible tracing, logging, request IDs, and
in-memory metrics from meaningful OTEL_* environment variables. Set
OTEL_SDK_DISABLED=true to disable telemetry explicitly. Prometheus metrics
are served from the control plane when OTEL_METRICS_EXPORTER=prometheus and
CONTROL_PLANE_ADDR are configured. Read
Telemetry.
Background Jobs
Applications can opt into golazy.dev/lazyjobs through
lazyapp.Config.Jobs. Existing apps do not need source changes unless they
want background jobs. Read Background Jobs.
lazydev Panel
The lazy development loop now embeds a DevTools-style panel at /_golazy/
with App, Requests, Services, Terminal, Routes, Jobs, BuildInfo, Assets, and
Cache tabs. It also forwards request IDs and trace context to the child app so panel
artifacts line up with framework telemetry. The Requests tab can list captured
request paths, filter by path or category, and open lazy-loaded Headers,
Tracing, and Logs details when detailed request monitoring is enabled. The
Routes tab reads the app's development control plane, so no application source
change is needed to see registered routes there; static GET routes link to the
running app. The BuildInfo tab shows the running app's Go version, command
path, main module, dependencies, replacements, recorded build settings, and the
last Go build trace summary; no application source migration is required. The
Dependencies tab shows the
running app's lazydeps service graph, progressively enhanced into SVG from
server-rendered table rows. It can simulate lazydev shutdown by sending
10 requests per second to the app root for a configured delay, marking
/readyz not ready, showing active requests and connections, and coloring
services as dependency cancellation progresses; no application source migration
is required. The
App tab owns app status,
changed-file groups, and rebuild, restart, and open-app controls. The Services
tab can restart, stop, or start
managed local services when service lifecycle tasks are present, groups output
by service tree and script name, parses JSON log lines into message and
attribute columns, caps log output at 100 rows, and shows run numbers for
repeated task attempts such as check; no application source migration is
required. The Assets tab lists the app asset manifest and public paths, and
the Terminal tab opens an xterm.js shell through a lazy-owned WebSocket in the
current app root. The Cache tab shows cache size, usage, searchable keys,
selected entry content, and cache controls. Detailed request monitoring also adds
per-region self time and allocation samples to the Requests tab without
requiring application source changes. Panel tab streams hydrate their lists
when they connect and then update only from relevant backend events. The status
bar keeps its status
stream inside the permanent Turbo frame, so tab switches keep one long-lived
status connection; no application source migration is required for that
behavior. The proxy also serves Chrome DevTools Automatic Workspace Folders
metadata for the app's app/js folder; no application route or source
migration is required. The injected host script also exposes
window.disableDevPanel() for the Chrome DevTools extension; no application
source migration is required. The proxy now injects hidden panel host markup
that is controlled by devpanel_controller.js; no application template
migration is required. Read lazydev and
Lazy.
The same public port accepts plain HTTP and HTTPS. Plain HTTP serves the local certificate authority setup page until the browser trusts the GoLazy CA; HTTPS serves the panel and proxied app traffic, with HTTP/2 available for the panel's long-lived streams.