Development
Add-ons
Install versioned GoLazy capabilities with reproducible source edits and typed runtime hooks.
Install an add-on
Run lazy add from the application root. PostgreSQL is the first bundled
add-on:
lazy add postgres
The command resolves an exact version, shows conflicts before writing, and
applies one baseline-bound changeset. It adds the Go module and generated
runtime bridge, creates the application migration tree, installs PostgreSQL
mise tasks, and registers the local service. The application records both its
direct selection and resolved lock data in addons.toml.
Add-on configuration is non-secret committed state. For example, this tells the PostgreSQL runtime which environment variable contains the connection URL:
lazy add --config database_url_variable=APP_DATABASE_URL postgres
Repeat --config key=value to merge more keys. Unmentioned keys remain in
place; use --unset-config key to remove one. The generated runtime selection
receives these values from addons.toml. Never put a password, token, or URL
containing credentials in add-on configuration—put it in the referenced
environment variable instead.
Add the durable jobs integration separately:
lazy add postgres/jobs
postgres/jobs requires postgres, so installing it into a new app resolves
the base add-on first. Removing it leaves PostgreSQL installed when PostgreSQL
was also selected directly.
Preview any change without writing files:
lazy add --dry-run postgres
lazy remove --dry-run postgres
lazy update --dry-run
Inspect and maintain the resolved selection with:
lazy addons
lazy update postgres
lazy remove postgres/jobs
Application state
addons.toml lives at the application root beside lazy.toml and js.toml.
It combines desired state and lock data: direct and transitive selections,
exact package and module versions, the manifest source and digest, and hashes
for generated or rewritten files. It also records non-secret per-add-on
configuration. Commit it with the source changes.
Generated files carry a GoLazy header. If a generated file no longer matches the recorded hash, update and removal stop instead of overwriting local work. Syntax-aware edits remember the property they own, so removing the final owner does not discard unrelated application configuration.
Local add-on development
An add-on package declares one lazyaddon.toml. Test it against an app without
publishing a registry entry:
lazy add --manifest ../lazyseo/lazyaddon.toml [email protected]
The manifest pins its package and add-on version, Go import and module, required
and conflicting add-ons, and declarative contributions. A bare same-package
requirement inherits the package's exact version; cross-package requirements
use [email protected]. Contributions may add generated or copied application files,
JavaScript libraries and entrypoints, source edits, mise tasks, and named build
phases. The installer reads those declarations and builds a lazycode
changeset; it does not run an unrestricted package install hook.
Here is the portable contribution surface in one add-on entry:
[[addons]]
id = "seo"
description = "SEO controller helpers and browser metadata"
go_import = "example.com/lazyseo/addon/seo"
go_module = "example.com/lazyseo"
go_version = "v1.2.3"
requires = ["[email protected]"]
files = [
"app/controllers/seo.go=>app/controllers/seo_addon.go",
"app/views/seo/meta.html.tpl=>app/views/seo/meta.html.tpl",
"tasks/generate=>.mise/tasks/seo/generate",
]
source_edits = ["edits/seo.json"]
js_libraries = ["@hotwired/[email protected]"]
js_entrypoints = ["seo=example.com/lazyseo/browser"]
tasks = ["seo:generate"]
build_hooks = ["before-build=seo:generate"]
File mappings use package-source=>application-target. Sources stay confined
to the package, targets stay inside the application even through symlinked
parents, and contributed Go files receive a generated header. Identical files
can be shared by multiple add-ons; different content is a planning conflict.
source_edits points to package-local JSON. Schema 1 supports only explicit,
versioned TOML string/string-array sets and Go imports in an existing file:
{
"schema": 1,
"operations": [
{
"kind": "toml.set_string.v1",
"path": "settings.toml",
"table": "seo",
"key": "title",
"value": "My site"
},
{
"kind": "toml.set_strings.v1",
"path": "settings.toml",
"table": "seo",
"key": "features",
"values": ["canonical", "robots"]
},
{
"kind": "go.ensure_import.v1",
"path": "app/services/services.go",
"import": "example.com/lazyseo/runtime",
"name": "_"
}
]
}
The Go import name must be an explicit Go identifier or _; a named import is
not removed while application code still uses its selector. Descriptor bytes
are part of the immutable release digest. Each property records its owners and
prior value, so identical edits can be shared and final removal restores the
app value. Drift or incompatible edits stop the plan. New JSON, JavaScript, or
richer Go operations require new schema operation versions rather than opaque
installer code.
Controller methods that should feel native are contributed as generated files
inside the application's controller package, for example
app/controllers/seo_addon.go. Go cannot add a method to an application type
from the add-on package itself. Do not use _seo.go: Go ignores source files
whose names begin with _ or ..
Build hooks are mise task references, using
before-build=<task> or after-build=<task>. The task must be listed by the
same add-on and mapped from a packaged source to an installer-owned,
inspectable .mise/tasks file. A pre-existing application task cannot be
adopted as a hook. Development and native builds use one lock snapshot and run
dependency hooks before dependent add-ons; a failing task fails the build.
tasks = ["seo:generate"]
build_hooks = ["before-build=seo:generate"]
files = ["tasks/generate=>.mise/tasks/seo/generate"]
The generated runtime selection carries every exact locked version, so startup
rejects a different package version even if Go MVS selected a newer module.
The installed Go package registers runtime behavior with typed
golazy.dev/lazyaddon hooks. lazyapp currently exposes phases for layered
views and public files, dependencies, migrations, jobs, routes, template
helpers, and the development control plane. Dependencies run before the add-on
that requires them. Values shared between add-ons use versioned typed
capabilities rather than process globals.
Files and migrations
Add-on views, public files, and migrations use golazy.dev/lazyfs. Layers are
deterministic: framework defaults are lowest, add-ons are in resolved order,
and application files win. Development keeps the application layer on disk so
live edits are not hidden by embedded files.
Migration add-ons mount filesystems into a lazymigrate.Catalog under stable
namespaces. The base postgres add-on provides the pool and migration backend;
postgres/jobs mounts only the jobs migrations and supplies the PostgreSQL jobs
backend. App-owned PostgreSQL migrations remain under
db/postgres/migrations.
Development panels
An add-on may register a development-only control-plane endpoint and a panel descriptor that points to it. The development panel discovers descriptors from the running application and exposes them under its Add-ons tab. Only exact, same-owner, GET-capable paths are accepted; the CLI proxy connects only to the current loopback app, caps response time and size, strips upstream headers, and renders add-on HTML in a sandboxed frame.
Panels can expose functionality with named PanelAction entries. Every action
must reference a separate exact, same-owner, POST-only endpoint. The trusted
outer panel renders its button, checks a process-random CSRF token and browser
origin, re-resolves the panel/action IDs from discovery, and sends an empty
loopback POST. It never forwards a browser URL, query, cookies, headers, or
body, while add-on HTML remains script- and form-disabled. The bundled
PostgreSQL panel uses this contract for Ping database.
Production builds do not include _lazydev.go registrations. A future paid
registry can add entitlement checks, signed manifests, and verified artifact
delivery at the source boundary without changing the application runtime or
allowing registry code to execute in the installer. The installer binds every
delivered artifact to its declared path and mode; the registry source is
responsible for publisher authentication, entitlement, signature checks, and
artifact-digest verification before resolution.