The build driver for fylr plugins — the "make" that knows how a fylr plugin is put together. This README is also the reference for how a fylr plugin repository is structured and what a plugin can offer; the living example is fylr-plugin-example, which exercises every feature described here. Start a new plugin by copying its structure.
The tool is distributed as a versioned Go module, so a plugin repo carries no build machinery (no submodule, no vendored Makefile include):
go run github.com/programmfabrik/fylr-build-plugin@latest buildPlugin repos keep a thin Makefile shim for muscle memory (make build,
make zip, …) that just calls the tool.
Versioning: plugins call the tool with @latest, which resolves the
newest tagged release — a v1.0.1 reaches every plugin build automatically,
no plugin repo is touched. main can hold unfinished work; cutting a release
is pushing a vX.Y.Z tag. An incompatible change is released as a new major
version, which per Go's semantic import versioning gets a new module path
(github.com/programmfabrik/fylr-build-plugin/v2) — existing plugins keep
resolving the v1 line untouched and migrate by changing the one go run line
when they choose to.
The name follows the fylr convention: fylr-plugin-* is reserved for plugins,
tooling is named fylr-<verb>-plugin — this one builds plugins. It also
seals them: the former fylr-seal-plugin is merged in here, including the
pluginseal package that fylr imports to open sealed plugins.
One repo per plugin, named fylr-plugin-<name> (custom data types:
custom-data-type-<type>). fylr plugins are fylr-only: no easydb5 hybrid
code, and easydb-library must never be used.
Start a new plugin with fylr-build-plugin init in a fresh repo directory —
it scaffolds everything below (a buildable plugin with one example API
extension, webfrontend and loca) and refuses to run inside an existing plugin.
Existing files (a GitHub-created README.md, LICENSE, …) are kept and skipped.
fylr-plugin-example/
├── manifest.yml the plugin: identity + every feature wiring, read by
│ fylr — ships VERBATIM as build/<name>/manifest.yml
├── build.yml the build: explicit delivery lists for fylr-build-plugin
├── Makefile thin shim: make help/build/zip/seal/loca/check/clean
├── README.md plugin docs; the zip ships a self-contained copy
├── LICENSE
├── .github/workflows/
│ └── release.yaml release build, from templates/ (public or private)
├── .gitignore build/, compiled JS, .DS_Store — generated files are
│ NEVER committed, dev and CI compile from source
├── l10n/
│ └── example-loca.csv localization, mastered in Google Sheets ("fylr
│ localization" sheet, one tab per plugin), pulled
│ with `make loca` — never edited by hand
├── server/ server-side programs (node / python3 / Go), wired in
│ the manifest as extensions and callbacks
│ └── extension/hello/ a Go module: cross-compiled by the build, sources
│ never ship
├── fas_config/ asset-server config: cookbooks + produce config
├── webfrontend/ frontend sources and assets (= base_url_prefix)
│ ├── *.coffee compiled + concatenated into plugin.webfrontend.url
│ ├── *.scss compiled to same-named .css
│ └── *.css/.html/img delivered as-is (build.yml webfrontend.install)
└── build/ generated by `build`, gitignored:
└── fylr_example/ the plugin as fylr loads it — manifest.yml,
build-info.json, compiled JS/CSS/binaries, copies
The folder build/<plugin.name>/ is the plugin in its installed form: fylr
loads it directly from disk during development, and the release zip is exactly
this tree (top-level folder = plugin.name, which fylr enforces on install —
the name comes from the manifest, not the repo).
Every feature is declared in manifest.yml; the
example's manifest
exercises all of them. Server-side programs are wired the same way everywhere:
fylr runs them via its exec services (node, python3, or exec for
compiled binaries), passing the request on stdin and reading the response from
stdout; %_exec.pluginDir% resolves to the installed plugin folder,
%info.json% passes fylr config + call context, and %_exec.GOOS% /
%_exec.GOARCH% select the right cross-compiled binary. A callback can run
with an api token (plugin_user) to call back into the fylr API.
| Manifest section | Feature |
|---|---|
plugin |
identity: name, version, displayname, plus the l10n CSV and the webfrontend entry points (url JS bundle, css, html) |
base_url_prefix |
where the webfrontend files are served from (by convention the webfrontend/ folder) |
extensions |
plugin API endpoints served under fylr's API, each backed by an exec program |
callbacks |
hooks into fylr flows: db_pre_save (an ordered steps list with objecttype filters — validate or modify objects before save), transition_db_pre_save, export, export_transport |
collection_upload |
hooks into collection uploads (api / hotfolder), with per-hook config parameters |
base_config |
plugin settings that appear in fylr's base config UI and /api/config, typed parameters |
system_rights |
plugin-defined system rights with typed parameters (bool, int, text, string-list*, mask-select, objecttype-select, eas-column-select, pool-select, preview-versions) |
custom_types |
custom data types: index mapping (text, number, geo, …, incl. skipTerms), per-schema/mask config, and a batched update callback |
fas_config |
asset production: a produce config and cookbooks merged into the fylr asset server |
custom_events |
plugin-defined event types |
Localization: the plugin.l10n CSV has a key column and one column per
language (de-DE, en-US, …); extra sheet columns (fil, the R TODO
checkboxes) are ignored by fylr's loader, so the Google Sheets export is
committed as-is.
Next to the manifest, the build writes build-info.json (repository,
rev, release, lastchanged, builddate) — fylr's plugin manager shows
it, so an installed plugin is traceable to a commit and release.
| Command | Does |
|---|---|
init |
scaffold a new plugin into the current (fresh) directory |
build |
assemble build/<plugin.name>/ — loadable by fylr from disk |
zip |
build + self-contained README + release zip |
seal |
seal a plugin zip for the marketplace (public key only) |
genkey |
generate a seal recipient keypair |
info |
inspect a (sealed) plugin zip: name, sealed?, recipient |
loca |
pull loca CSV(s) from their Google Sheets master |
readme |
write a self-contained README (relative images inlined) |
check |
validate the build tree against the manifest |
clean |
remove the build folder |
All commands run from the plugin repo root (where manifest.yml lives).
External tools are checked per step with an actionable error: coffee is only
required when the plugin has CoffeeScript, sass only for SCSS, go only for
Go server extensions.
The tool reads no environment variables — every input is a flag. Where a
release workflow has values in env (RELEASE_TAG, ZIP_NAME), the plugin's
Makefile shim translates them into -release/-out flags.
The delivered files are listed explicitly in build.yml — nothing is
derived from the manifest, since a derived list is easily incomplete. The
safety net is check, which runs with every build and validates the assembled
tree against every path the manifest references — a dangling reference is
reported as a warning (it can be deliberate, e.g. the example plugin's
missing-module test callbacks).
manifest.ymlis copied verbatim tobuild/<name>/manifest.yml(no master/generated split;plugin.namein the manifest names the build folder and the zip's top-level dir, which fylr enforces on install).server.installandwebfrontend.installentries (files or dirs) are copied verbatim..DS_Store,.gitignore,.git,node_modulesnever ship.webfrontend.coffee1files compile (CoffeeScript 1.x — the version the plugins are stuck on, hence the key — with-b) and concatenate — in list order — intoplugin.webfrontend.urlunderbase_url_prefix;webfrontend.scssfiles compile to same-named.css.server.go.modulescross-compile for every configured arch (CGO off, trimmed, stripped) into theirexetemplate —%GOOS%/%GOARCH%replaced, matching the manifest's%_exec.GOOS%/%_exec.GOARCH%references. Go sources do not ship.build-info.jsonis written beside the manifest:repository,rev,release(from-release; the release workflow's tag reaches the tool via the Makefile),lastchanged,builddate.checkruns automatically: every manifest reference (%_exec.pluginDir%/...exec programs,plugin.l10n,plugin.webfrontend.*,fas_configfiles) should exist in the build tree; cross-compiled references are checked for every configured arch, commented-out manifest blocks do not count. Missing references are warnings, not errors.
build produces an on-disk plugin. In the fylr server's fylr.yml:
plugin:
paths+:
- ../my-plugin/buildPoint at build/, not the repo root — fylr walks the path for any
manifest.yml, and the root manifest would collide with the built one.
The explicit build description. Unknown keys are rejected. Every feature is exercised by fylr-plugin-example's build.yml.
server:
install: # server-side delivery, copied verbatim
- server
- fas_config
- l10n
go: # Go server extensions, cross-compiled
archs: # each entry sets Go's GOOS/GOARCH build env;
- { GOOS: linux, GOARCH: amd64 } # optional — default: every
- { GOOS: darwin, GOARCH: arm64 } # platform fylr runs on
modules:
- dir: server/extension/hello
exe: server/extension/hello/hello-%GOOS%-%GOARCH%.exe
webfrontend:
coffee1: # CoffeeScript 1.x (the version plugins are stuck
- webfrontend/MyClass.coffee # on), compiled + concatenated in this
- webfrontend/UsesMyClass.coffee # order into plugin.webfrontend.url
scss: # compiled to same-named .css
- webfrontend/MyStyle.scss
l10n_json: # loca CSV -> per-culture JSONs the webfrontend
- csv: l10n/my-loca.csv # loads (easydb-library l10n2json format),
out: l10n # written under base_url_prefix (default l10n)
install: # delivered as-is: plain css, html, images, fonts
- webfrontend/MyPlugin.html
- webfrontend/MyPlugin.css
loca: # Google Sheets masters for the loca command
- csv: l10n/my-loca.csv
sheet: <spreadsheet key>
gid: "<tab gid>"Plugin loca CSVs are mastered in Google Sheets (fylr's "fylr localization"
sheet, one tab per plugin) and pulled with fylr-build-plugin loca — never
edited locally. The raw export (with the fil ticket column and R TODO
checkboxes) is committed as-is; fylr's CSV loader only reads the key column
and xx-XX language columns.
A sealed plugin is still a valid zip: the plugin folder with a plaintext
manifest.yml, the whole plugin encrypted to fylr's public key beside it
(fylr-sealed-plugin.enc, X25519 + XSalsa20-Poly1305 sealed box). Only a fylr
holding the matching private key can open and run it.
fylr-build-plugin seal # builds the zip if needed, dev/CI key
fylr-build-plugin seal -pubkey <base64> # production / marketplace-source key
fylr-build-plugin genkey # fresh keypair
fylr-build-plugin info my-plugin_sealed.zipSealing uses a public key only — no secrets in CI. With no -pubkey the
dev/CI key is used: such a plugin opens only on a fylr built with
-tags licensetest, which is what makes a public example
(fylr-plugin-example-licensed)
safe to publish. fylr opens sealed plugins via the pluginseal
package — sealer and server share one source of truth and cannot drift:
plain, sealed, err := pluginseal.OpenPluginZip(artifact, keysThisBuildHolds...)A release is cut by a manager on the GitHub release page; the workflow builds and publishes the artifact. Two templates live in templates/:
- release-public.yaml — attach the zip to
the release; fylr installs from
releases/latest/download/<repo>.zip. - release-private.yaml — additionally publish to GitHub Pages under an unguessable per-repo UUID URL, since a private repo's release assets are not fetchable by fylr.
Either variant becomes a sealed release by switching make zip to
make seal — no secrets needed.
Writes a self-contained README: every relative image reference is inlined
as a data: URI. This matters because most plugin repositories are private —
once a README leaves the repo (into the plugin zip, then the marketplace) its
relative asset paths are unreachable. zip bundles it automatically as
build/<name>/README.md.
fylr-build-plugin readme [flags]
--in input markdown file (default "README.md")
--out output self-contained markdown file (default "build/README.md")
--max-image-bytes leave images larger than this many bytes as relative links (default 524288)