JSON Configuration
forge init scaffolds by default — is FalkForge's primary, fully supported authoring model. JSON configuration is an experimental, incomplete subset intended for simple, no-compiler installers. JSON now authors the Firewall, IIS, SQL, and .NET-detection extensions (sections 5 and 6 below): each extensions block is translated into the same real extension the C# API attaches and emitted into the compiled MSI. Use the C# API for anything beyond the JSON subset.
Not every team writes C#. For simple, data-driven installers FalkForge can build from JSON configuration files instead — no code required. Define your product, files, and features in JSON; run forge build config.json to produce an MSI. (JSON also authors the Firewall, IIS, and SQL extensions — see sections 5 and 6.)
1. Minimal JSON Installer
Open demo/json/01-minimal.json. This is the simplest possible JSON installer — 17 lines.
{
"product": {
"name": "MinimalApp",
"manufacturer": "Contoso",
"version": "1.0.0"
},
"ui": "Minimal",
"features": [
{
"id": "Complete",
"title": "Complete",
"files": [
{ "source": "payload/MinimalApp.exe" }
]
}
]
}
Three things make up every JSON installer:
product— name, manufacturer, and version. These appear in Programs & Features.ui— the dialog template."Minimal"shows a single Install button.features— at least one feature containing the files to install.
Build it:
forge build demo/json/01-minimal.json
FalkForge reads the JSON, validates the structure, and produces an MSI. No C# project, no compilation step.
Deep dive: JSON schema and validation
FalkForge validates JSON configs with the same rigor as C# builds. The JsonConfigLoader maps your JSON to internal PackageBuilder calls and runs the full validation pipeline. Errors use the JSN prefix:
- JSN001 — Invalid JSON syntax.
- JSN002 — Missing required
product.name. - JSN003 — Missing required
product.manufacturer. - JSN004 — Invalid version format.
- JSN005 — Invalid upgrade code GUID.
- JSN006 — Invalid UI dialog set value.
- JSN007 — Invalid platform value.
- JSN009 — Feature must have an id.
- JSN010 — Configuration error during mapping.
- JSN011–JSN014 — Firewall, IIS, SQL, and .NET extension field validation (runs first, before the block is translated into an extension).
The full list, including the JSN015–JSN019 signing-section codes (JSN019 also covers unresolvable signing key/auth material), is in the Reference Docs error code appendix. After JSON-specific checks pass, the standard PKG, FEA, and SVC validators run too. You get the same safety net as a C# build.
2. Install Directory Selection
Open demo/json/02-installdir.json. This adds directory selection and upgrade handling.
{
"product": {
"name": "NotepadClone",
"manufacturer": "Contoso",
"version": "2.1.0",
"description": "A simple text editor",
"upgradeCode": "12345678-1234-1234-1234-123456789012"
},
"ui": "InstallDir",
"installDirectory": "Contoso/NotepadClone",
"majorUpgrade": {},
"features": [ ... ],
"downgrade": {
"message": "A newer version is already installed."
}
}
Key additions over the minimal example:
ui: "InstallDir"— shows a directory chooser dialog so users can pick where files go.installDirectory— the default install path under Program Files."Contoso/NotepadClone"becomesC:\Program Files\Contoso\NotepadClone.majorUpgrade— enables automatic upgrade detection. Windows removes the old version before installing the new one. Takes an optionalschedulefield naming the MSIRemoveExistingProductsschedule; an empty object uses the builder's default.downgrade— a separate top-level object (not nested insidemajorUpgrade) withallow(bool) andmessage(block message shown when a newer version is already installed andallowis false, the default).upgradeCode— a fixed GUID that ties all versions together. Set this once and never change it.
Features can also contain registry entries and shortcut definitions on files:
"files": [
{
"source": "payload/NotepadClone.exe",
"shortcut": {
"name": "NotepadClone",
"location": "Desktop",
"description": "Launch NotepadClone"
}
}
],
"registry": [
{
"root": "HKCU",
"key": "Software\\Contoso\\NotepadClone",
"name": "Version",
"value": "2.1.0"
}
]
Deep dive: How JSON maps to PackageBuilder
Internally, forge build delegates to JsonConfigLoader, which maps each JSON property to the equivalent PackageBuilder call:
"ui": "InstallDir"→package.UseDialogSet(MsiDialogSet.InstallDir)"installDirectory": "Contoso/NotepadClone"→KnownFolder.ProgramFiles / "Contoso" / "NotepadClone""majorUpgrade"→package.MajorUpgrade(mu => ...)"downgrade"(separate top-level object) →package.Downgrade(d => ...)"shortcut"on a file →files.Add(...).WithShortcut(s => ...)"registry"→package.Registry(r => ...)
The JSON format covers the most common scenarios. For anything it can't express — custom actions, sequence manipulation, conditional logic — use the C# API directly.
3. Feature Trees
Open demo/json/03-featuretree.json. This installer defines a tree of selectable features.
"features": [
{
"id": "CoreTools",
"title": "Core Tools",
"description": "Essential developer tools",
"required": true,
"files": [ ... ]
},
{
"id": "Plugins",
"title": "Plugins",
"features": [
{ "id": "GitPlugin", "title": "Git Integration", ... },
{ "id": "DockerPlugin", "title": "Docker Support", "default": false, ... }
]
},
{
"id": "BackgroundService",
"title": "Background Service",
"default": false,
"services": [ ... ]
}
]
Feature properties:
required: true— always installed; the user cannot deselect it.default: true(the default) — selected by default but the user can turn it off.default: false— not selected by default; the user opts in.- Nested
features— child features appear indented in the tree. Deselecting the parent deselects all children.
The "services" array on the BackgroundService feature installs a Windows service:
"services": [
{
"name": "DevToolkitSvc",
"displayName": "DevToolkit File Watcher",
"executable": "[INSTALLDIR]devtoolkit-svc.exe",
"startType": "Manual",
"account": "LocalService"
}
]
Use ui: "FeatureTree" to show the selection dialog.
Deep dive: Feature nesting — JSON vs C#
In C#, features are nested by calling feature.AddChild(child => ...) inside a parent feature builder. In JSON, you nest them by placing a features array inside a feature object. The result is identical — same MSI feature tree.
JSON supports arbitrary nesting depth. Each level appears as a child row in the FeatureTree dialog. Keep hierarchies shallow (2–3 levels) for usability.
The launchConditions array at the top level blocks installation when conditions are not met:
"launchConditions": [
{
"condition": "VersionNT64",
"message": "This application requires a 64-bit operating system."
}
]
Conditions use standard MSI property expressions.
4. Advanced Configuration
Open demo/json/05-advanced.json. This demo exercises most JSON features: registry, shortcuts, services, environment variables, license files, and multiple launch conditions.
Registry entries
"registry": [
{
"root": "HKLM",
"key": "Software\\Contoso\\AdvancedDemo",
"name": "InstallDir",
"value": "[INSTALLDIR]"
}
]
The root accepts HKLM, HKCU, HKCR, HKU, and HKMU. Values can reference MSI properties like [INSTALLDIR].
Environment variables
"environmentVariables": [
{
"name": "ADVANCED_HOME",
"value": "[INSTALLDIR]",
"action": "Set",
"system": true
},
{
"name": "PATH",
"value": "[INSTALLDIR]bin",
"action": "Append",
"system": true
}
]
Actions: Set (overwrite), Append (add to existing), Remove (delete on install). Set system: false for per-user variables.
Services and nested features
The advanced config defines two services under a parent "Background Services" feature, each with its own startType: "Automatic", "DelayedAutomatic", or "Manual". Services are cleanly removed on uninstall.
License and UI template
"ui": "Advanced",
"license": "payload/eula.rtf"
The "Advanced" template adds typical/custom/complete install types. The license file is shown in a scrollable panel before the user can proceed.
Deep dive: When to use JSON vs C#
Use JSON when:
- Your installer is simple and data-driven — files, registry, services, shortcuts, feature trees.
- Your CI/CD pipeline generates or transforms the config.
- Your team doesn't use C# and you want a low-barrier format.
- You need fast iteration without a compile step.
Use C# — the primary, fully supported path, and what forge init scaffolds by default — when:
- You need custom actions (DLL, EXE, or script execution during install).
- You have conditional features based on detected system state.
- You need custom sequence table manipulation.
- You are building merge modules, patches, or transforms.
- You want compile-time type safety and IDE refactoring support.
JSON is a work-in-progress subset. You can start with JSON for the fields it actually supports (product, features, files, shortcuts, registry, services, environment variables, major upgrade/downgrade, launch conditions, license, signing, and the Firewall/IIS/SQL/.NET-detection extensions) and move to C# once you need anything else.
5. Web Server Installer
Open demo/json/06-web-server.json. It installs a web app and authors a firewall rule plus an IIS application pool and web site — all from JSON, emitted into the compiled MSI.
extensions block below is translated into the same FirewallExtension and IisExtension the C# API attaches via new MsiCompiler().Use(...), so forge build emits the identical WixFirewallException, IIsAppPool, and IIsWebSite tables into the MSI. Field-level validation (JSN011/JSN012) still fires first for malformed rules. For the full C# equivalents see Demos 29 (Firewall) and 30 (IIS).
Firewall rules
"extensions": {
"firewall": [
{
"id": "HttpRule",
"name": "WebServerApp HTTP",
"protocol": "Tcp",
"port": "80",
"direction": "Inbound",
"action": "Allow",
"profile": "All"
}
]
}
Each rule gets a unique id and must specify port or program. Supported profiles: All, Domain, Private, Public. The rule is authored into the MSI's WixFirewallException table, which creates the rule on install and removes it on uninstall — the same output the C# Firewall extension produces.
IIS app pools and web sites
"iis": {
"appPools": [
{
"id": "WebAppPool",
"name": "WebServerAppPool",
"runtimeVersion": "v4.0",
"pipelineMode": "Integrated",
"identity": "ApplicationPoolIdentity"
}
],
"webSites": [
{
"id": "WebSite",
"description": "WebServerApp Site",
"directory": "[INSTALLDIR]",
"appPool": "WebAppPool",
"bindings": [
{ "protocol": "http", "port": 80, "host": "localhost" },
{ "protocol": "https", "port": 443, "host": "localhost" }
]
}
]
}
The appPool field on a web site references an app pool by its id in the JSON document. Bindings support http and https protocols with configurable port and host header. The app pool and web site are authored into the MSI's IIsAppPool and IIsWebSite tables — the same output the C# IIS extension produces.
Deep dive: Extension-specific JSON schemas
Each extension has its own section under "extensions":
firewall— array of rule objects (id,name,protocol,port,direction,action,profile).iis— object withappPoolsandwebSitesarrays. Sites reference pools by id.sql— array of database objects with connection info and scripts.dotnet— array of runtime detection checks.
Extension validation uses the same error code families as C#: JSN011 for firewall, JSN012 for IIS, JSN013 for SQL, JSN014 for .NET detection (see JsonConfigLoader.ValidateExtensions). Invalid JSON extension configs fail fast with clear messages. Once a config passes that structural check, the firewall, iis, sql, and dotnet blocks are all translated into real extensions and emitted into the MSI (see JsonConfigLoader.LoadExtensionsFromFile).
6. Database Application
Open demo/json/07-database-app.json. It installs a database app and provisions a SQL database with an install script — authored from JSON and emitted into the compiled MSI.
sql and dotnet blocks below — each is translated into the same extension the C# API attaches: SqlExtension emits the SqlDatabase and SqlScript tables (see Demo 31 for the C# equivalent), and DotNetExtension emits MSI-native Signature/DrLocator/AppSearch runtime detection plus a LaunchCondition gate (see Demo 32).
SQL database creation
"extensions": {
"sql": [
{
"id": "AppDatabase",
"server": "[SQLSERVER]",
"database": "ContosoDb",
"createOnInstall": true,
"dropOnUninstall": false,
"scripts": [
{
"id": "CreateSchema",
"sourceFile": "payload/sql/create-schema.sql",
"executeOnInstall": true,
"sequence": 1
},
{
"id": "SeedData",
"sourceFile": "payload/sql/seed-data.sql",
"executeOnInstall": true,
"sequence": 2
}
]
}
]
}
Key fields:
server— the SQL Server instance.[SQLSERVER]is an MSI property the user sets at install time (via the UI or command line:msiexec /i app.msi SQLSERVER=.\SQLEXPRESS).createOnInstall— creates the database if it doesn't exist.dropOnUninstall— set tofalseto preserve data after uninstall. Usetrueonly for development or disposable databases.scripts— SQL files executed insequenceorder. Schema first, then seed data.
.NET runtime detection (MSI-native)
"dotnet": [
{
"runtimeType": "Runtime",
"platform": "X64",
"minimumVersion": "8.0.0",
"variableName": "DOTNET8_INSTALLED",
"message": ".NET 8 Runtime is required to run this application."
}
]
The JSON dotnet block reaches the compiled installer via the same DotNetExtension the C# API attaches, emitting real MSI-native detection — a Signature (sentinel-file version) plus a DrLocator search over the platform-appropriate shared-framework directory, bound to variableName via AppSearch (see Reference Docs §10.5 .NET Extension). runtimeType, platform, minimumVersion, and variableName are required (JSN014 if any is missing); message is optional. variableName must be an all-uppercase public MSI property identifier (NET005 otherwise); runtimeType: "Sdk" is not supported for MSI-native detection because the SDK has no shared-framework sentinel to search for (NET004). Because the JSON path has no separate call like the C# fluent API's package.Require(...), DotNetExtension always emits its own LaunchCondition gate using message — or, if message is omitted, a generated default naming the exact requirement.
Deep dive: Connection string handling (C# SQL extension)
The JSON sql block reaches the compiled installer via the same SQL extension the C# API uses, so this behavior applies to both. The server field accepts MSI property references like [SQLSERVER]. At install time, Windows Installer resolves these to the actual values. This keeps connection strings out of the config and lets the user (or admin deploying silently) provide the server name.
For silent deployments, pass the property on the command line:
msiexec /i DatabaseApp.msi /qn SQLSERVER=dbserver\SQLEXPRESS
For interactive installs, use the "InstallDir" or "Advanced" UI template. The SQL extension can display a server selection dialog if you use the C# API with the custom UI system.
Authentication uses Windows Integrated Security by default. For SQL authentication, combine with a custom UI page that collects credentials via SetSecureProperty — that requires the C# API.
7. Signed Bundle Output (signing section)
An optional top-level "signing" section makes forge build wrap the compiled MSI in a single-package EXE bundle whose integrity manifest is ECDSA-signed through the ISignatureProvider seam (optionally hybrid post-quantum — see below). Without a signing section the build keeps its normal MSI-only output.
No secrets ever go in the JSON file. Keys and credentials are referenced by file path or by environment variable name (the *Env fields); the values are read from the environment at build time. The loader rejects inline secret material (JSN016), and any referenced environment variable that is unset at build time fails the build (JSN019) — it never falls back to unsigned output or an unauthenticated signing request.
Local PEM key
"signing": {
"provider": "pem",
"keyPath": "keys/release.pem"
}
Exactly one key source is required: keyPath (an ECDSA private-key PEM file, relative paths resolve against the config file) or keyEnv (the name of an environment variable whose value is the PEM, so the key never touches the config or the repo).
Hybrid post-quantum signing (pem provider only)
"signing": {
"provider": "pem",
"keyPath": "keys/release.pem",
"pqKeyPath": "keys/release-mldsa.pem"
}
Adding pqKeyPath or pqKeyEnv to the pem provider turns the build hybrid: the manifest is signed by both the classical ECDSA key and an ML-DSA (FIPS 204, post-quantum) companion key — the field's presence is the switch, there is no separate flag. The PQ key follows the exact same secret rules as the classical key: pqKeyPath is a PEM file path and pqKeyEnv is an environment variable name holding the PEM — never paste key material into the config (JSN016), use at most one of the two (JSN017), and an unset environment variable fails the build (JSN019); the build never silently falls back to a classical-only bundle.
pqKeyPath/pqKeyEnv are rejected on the signserver provider (JSN018): SignServer cannot yet produce the context-bound ML-DSA signature FalkForge requires, and failing loud beats emitting a bundle the author believes is hybrid-signed but is not. See the manual's “Post-Quantum Hybrid Signing” section and Demo 63 (hybrid-pq-signing) for the trust-pinning half (the engine-side companion pin that makes stripping the PQ signature a hard failure).
Keyfactor SignServer (remote signing)
"signing": {
"provider": "signserver",
"baseUrl": "https://signserver.example.com:8443",
"worker": "PlainSigner",
"authMode": "bearer",
"bearerTokenEnv": "SIGNSERVER_TOKEN"
}
authMode is one of none, basic (usernameEnv + passwordEnv), bearer (bearerTokenEnv), or clientcert (clientCertPathEnv naming an env var that holds a PFX path, plus optional clientCertPasswordEnv). An optional keyId labels the signature in the envelope. An http:// base URL or authMode: "none" builds with a warning — acceptable for local SignServer CE test containers only.
The bundle produced this way embeds the published NativeAOT engine as its self-extracting front, so it is a runnable installer. The compiler resolves the engine automatically: from the FALKFORGE_ENGINE_STUB environment variable, from an engine directory next to the forge executable, or from the repository’s artifacts/publish/engine output (run scripts/publish.ps1 once). If no engine can be found the build fails with an actionable message. To build a non-runnable design-time bundle anyway (signing/verification tooling, CI without an engine publish), pass --no-engine — the build then prints a “NOT a runnable installer” warning.
Recap: JSON vs C# decision guide
| Scenario | JSON | C# |
|---|---|---|
| Files, shortcuts, registry | Yes | Yes |
| Feature trees | Yes | Yes |
| Services, environment variables | Yes | Yes |
| Firewall, IIS, SQL extensions | Yes | Yes |
| .NET-detection extension | Yes | Yes |
| CI/CD pipeline generation | Ideal | Possible |
| Signed bundle (integrity manifest, PEM or SignServer) | Yes | Yes |
| Custom actions | No | Yes |
| Sequence table manipulation | No | Yes |
| Merge modules, patches, transforms | No | Yes |
| Custom UI (WPF pages) | No | Yes |
| Conditional logic / dynamic features | No | Yes |
Start with JSON for straightforward installers and CI/CD pipelines — including those needing the Firewall, IIS, SQL, or .NET-detection extensions. Move to C# — the primary path — when you need custom actions, conditional features, or advanced MSI capabilities. For the fields JSON does support, both produce identical MSI output.