Module development
Create maintainable product modules with a manifest, versioned SDK contracts, migrations, endpoints and optional Vue extensions.
FormPlatform Module Development Step-by-Step
中文:MODULE_DEVELOPMENT_STEP_BY_STEP.zh-CN.md
This tutorial creates an independent `Inventory` module from an empty directory. It includes configuration, three-provider migrations, an ORM entity, domain service, API, Server Action, client i18n, and deployment. The module is source-separated from the Host and references only the published SDK.
1. Choose a module when appropriate
Layout, field, and event changes need only a form. Use a module for dedicated tables, server APIs, actions/triggers, reusable controls, routes, or customer-specific logic. A module is trusted in-process code with full server privileges; isolate untrusted code in a separate service.
2. Prerequisites and layout
Build/publish FormPlatform so the SDK directory contains `FormPlatform.Sdk.dll/.xml` and `FormPlatform.Extension.Abstractions.dll/.xml`.
workspace/
FormPlatform/
Inventory/
InventoryModule.csproj
module.json
appsettings.inventory.json
InventoryModuleEntry.cs
InventoryMigrations.cs
InventoryService.cs
InventoryActions.cs
client/index.js
client/inventory.css
deploy.ps1
deploy.bat
uninstall.ps1
uninstall.bat
3. Create the project
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>InventoryModule</AssemblyName>
<RootNamespace>InventoryModule</RootNamespace>
<GenerateDependencyFile>true</GenerateDependencyFile>
<FormPlatformSdkDirectory Condition="'$(FormPlatformSdkDirectory)' == ''">..\FormPlatform\src\FormPlatform.Host\bin\Debug\net10.0</FormPlatformSdkDirectory>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<Reference Include="FormPlatform.Sdk" HintPath="$(FormPlatformSdkDirectory)\FormPlatform.Sdk.dll" Private="false" />
<Reference Include="FormPlatform.Extension.Abstractions" HintPath="$(FormPlatformSdkDirectory)\FormPlatform.Extension.Abstractions.dll" Private="false" />
</ItemGroup>
<ItemGroup>
<None Update="module.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="appsettings.inventory.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="client\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
`Private=false` ensures the Host supplies the single shared SDK and Abstractions assemblies.
4. Create the manifest
{
"manifestVersion": 1,
"name": "Inventory",
"version": "1.0.0",
"entryAssembly": "InventoryModule.dll",
"type": "InventoryModule.InventoryModuleEntry",
"sdk": { "minimum": "1.0.0", "maximumExclusive": "2.0.0" },
"platform": { "minimum": "1.0.0", "maximumExclusive": "2.0.0" },
"navigation": [
{ "token": "inventory-management", "titleKey": "menu.inventory.management", "icon": "📦", "isGroup": true },
{ "token": "inventory-items", "titleKey": "menu.inventory.items", "target": "/inventory", "icon": "📋", "parentToken": "inventory-management" }
],
"ownedSystemForms": []
}
Names use only alphanumerics, `.`, `_`, and `-`, with a 100-character maximum. Manifest version must match assembly Major/Minor/Build. Ranges are minimum-inclusive and maximum-exclusive.
`navigation` is the module's complete declarative menu ownership list. Every entry has a stable `token`, `titleKey`, and `icon`; a normal link also has an application-relative `target`. A menu group uses `isGroup: true`; its child links use `parentToken`. The host inserts missing entries and moves a module-owned child below its declared parent. `ownedSystemForms` lists only fixed IDs of system forms that this module can regenerate; retain explicit empty arrays when neither resource is owned.
5. Add configuration and an ORM entity
{
"Inventory": { "DefaultPageSize": 25, "MaximumPageSize": 200 },
"DataAccess": {
"Entities": {
"InventoryItem": {
"Schema": "public",
"TableName": "inventory_item",
"Attributes": [
{ "PropertyName": "id", "ColumnName": "id", "ValueKind": "Guid", "IsNullable": false, "IsKey": true, "IsGenerated": false },
{ "PropertyName": "name", "ColumnName": "name", "ValueKind": "String", "IsNullable": false, "MaxLength": 200 },
{ "PropertyName": "quantity", "ColumnName": "quantity", "ValueKind": "Int32", "IsNullable": false },
{ "PropertyName": "updatedBy", "ColumnName": "updated_by", "ValueKind": "Guid", "IsNullable": false },
{ "PropertyName": "updatedAt", "ColumnName": "updated_at", "ValueKind": "DateTimeOffset", "IsNullable": false, "CanWrite": false }
]
}
}
}
}
Module configuration is added before Core binds DataAccess options. Entity and property names are stable logical contracts, not UI labels.
6. Add three-provider migrations
Implement `IDatabaseMigrationModule` with module name `Inventory`, data source `Management`, and immutable IDs such as `001_create_inventory_item`. Use PostgreSQL `uuid`, MySQL `binary(16)`, and SQL Server `uniqueidentifier` for owned identifiers and references. Do not add a database UUID default: create UUID v7 values in the application or let the ORM populate an empty writable Guid key. See Native UUID v7 identifiers.
Never edit a released `001`; add `002_...`. Review MySQL implicit commits and conditional-index syntax instead of assuming every provider supports `IF NOT EXISTS`.
7. Implement the domain service
Consume SDK ORM interfaces, not Host implementation. Methods accept current user ID and `CancellationToken`; apply owner/authorization filters on the server. Use one unit of work for related reads, validation, and writes, then commit explicitly.
public sealed class InventoryService(IUnitOfWorkFactory workFactory, IDynamicRepository repository)
{
public async Task<object> CreateAsync(string userId, InventorySaveRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Name))
throw new PlatformApiException(400, "inventory.nameRequired", "inventory.nameRequired", "Name is required.");
await using var work = await workFactory.BeginAsync("Management", ct);
var row = new DynamicEntity("InventoryItem");
row["name"] = request.Name.Trim();
row["quantity"] = request.Quantity;
row["updatedBy"] = userId;
var saved = await repository.InsertAsync(work, row, ct);
await work.CommitAsync(ct);
return saved;
}
}
Use the SDK XML documentation and Todo/ResourceBooking source for exact repository/query APIs. Never convert request sort/filter text directly into SQL.
8. Actions and triggers
Implement `IServerActionsProvider` when form events/triggers need server behavior. Register stable names and return structured results. Re-read identity on the server; never trust a client `userId`. Substituted values still require typed validation. Use `PlatformApiException` or SDK trigger results for failure.
9. Implement the entry point
public sealed class InventoryModuleEntry : IFormPlatformSdkModule
{
public void ConfigureConfiguration(ConfigurationManager configuration, IHostEnvironment environment)
{
var directory = Path.GetDirectoryName(typeof(InventoryModuleEntry).Assembly.Location)!;
configuration.AddJsonFile(Path.Combine(directory, "appsettings.inventory.json"), false, true);
}
public void ConfigureServices(IServiceCollection services, IConfiguration configuration, IHostEnvironment environment)
{
services.Configure<InventoryOptions>(configuration.GetSection("Inventory"));
services.AddSingleton<InventoryService>();
services.AddSingleton<IServerActionsProvider, InventoryActions>();
services.AddSingleton<IDatabaseMigrationModule, InventoryMigrations>();
}
public void MapEndpoints(IEndpointRouteBuilder endpoints)
{
var api = endpoints.MapGroup("/api/inventory/items").RequireAuthorization();
api.MapGet("/", InventoryEndpoints.QueryAsync);
api.MapPost("/", InventoryEndpoints.CreateAsync);
api.MapPut("/{id}", InventoryEndpoints.UpdateAsync);
api.MapDelete("/{id}", InventoryEndpoints.DeleteAsync);
}
}
Hook order is Configuration, Services, Application, Endpoints. Implement only what is needed. Middleware affects the entire Host and requires special care.
10. API rules
Namespace APIs under `/api/{module}` and pages under `/{module}`. Apply authorization explicitly. Use 200/201/204 and return resource IDs/Location for creation. Throw `PlatformApiException` for reusable failures; allow Host mapping of `KeyNotFoundException` where appropriate. Forward cancellation throughout.
11. Forms versus a client route
Prefer Data Models, system forms, and DataGrid for CRUD. Use a standalone client route for calendars, boards, or graphics, while editing can still open ordinary forms. ResourceBooking demonstrates this hybrid.
System-form initialization is idempotent. Do not overwrite user-owned forms on every startup. Store system text as `@inventory.key`.
12. Client extension
`client/index.js` exports an installer:
export default {
install(api) {
api.registerMessages('Inventory', {
en: { inventory: { title: 'Inventory' } },
'zh-CN': { inventory: { title: '库存' } }
})
api.registerRoute({
path: '/inventory',
component: () => import('/extension-assets/Inventory/inventory-page.js')
})
api.extendActionApi('refreshInventory', () =>
window.FormPlatform.API.fetch('/api/inventory/items'))
}
}
The extension API registers controls, routes, respondent-dashboard tools, Action APIs, Runtime APIs, messages, and app installers. It exposes `components.FormReader` for trusted module pages and `preloadFormComponents(schema)` for resolving the lazy control chunks that an offline package must cache. A trusted host can pass runtime-only `fileHandler` (`stage`, `preview`, `remove`) and `optionHandler.query` callbacks; controls retain their normal online APIs when a handler is absent, and handlers must never be persisted in form JSON. An option handler must serve the normal `{ items, total, offset, limit, hasMore, nextCursor }` page shape and enforce its snapshot's version and expiry. A route that must bootstrap while disconnected uses `meta.public`; this affects only the browser guard, so its package/data endpoints must still require the appropriate server policy. A custom control uses mock data in `designerMode`, calls APIs through `window.FormPlatform.API.fetch`, and keeps business state in Vue reactivity rather than direct DOM mutations.
13. Enable the client module
{
"FormPlatformExtensions": {
"ModulesDirectory": "Modules",
"ClientModules": ["/extension-assets/Inventory/index.js"]
}
}
Extensions load before Vue and initial Router navigation, preserving direct module-route navigation.
14. Build and deploy
dotnet build InventoryModule.csproj -c Debug `
-p:FormPlatformSdkDirectory="C:\path\FormPlatform\bin\Debug\net10.0"
Deploy DLL, deps.json, manifest, module configuration, and client assets under `FormPlatform/Modules/Inventory`. Do not copy SDK/Host DLLs into the package. Restart the Host for a server DLL change; a pure client asset change normally needs only refresh/hard refresh.
Windows deployment launcher standard
Every Windows-targeting module must include both `deploy.ps1` and `deploy.bat`. `deploy.ps1` is the canonical, automation-friendly implementation; `deploy.bat` is a small Command Prompt/Explorer launcher that keeps the working directory stable, forwards all arguments, and returns the PowerShell exit code:
@echo off
setlocal
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0deploy.ps1" %*
exit /b %ERRORLEVEL%
For example, an administrator can run `deploy.bat -Configuration Release -FormPlatformDirectory C:\inetpub\FormPlatform` without having to compose a PowerShell command. Keep deployment logic in `deploy.ps1`; do not duplicate it in the batch file.
Uninstallation standard
Every module must also provide `uninstall.ps1` and `uninstall.bat`, following the same wrapper rule. A normal uninstall removes only `FormPlatform/Modules/<module-name>` after the Host has stopped. `-RemoveMenuEntries` removes declared leaf tokens and then removes a declared group only when it is empty; it never removes administrator-added links. `-RemoveSystemForms` removes only `ownedSystemForms`. Both options queue a provider-neutral host-side cleanup request that runs through `IFormStore` on the next startup. It must preserve database tables and migration history by default: deleting application data silently makes a later reinstall inconsistent or destructive. A data-purge parameter is allowed only when it is explicit (for example `-RemoveDatabase`), requires an explicit provider and connection string, drops the module's schema, and deletes the corresponding migration history as one deliberate purge operation. PostgreSQL/SQL Server may use a transaction; MySQL DDL implicitly commits, so it must stop and report the first failure. Never infer destructive credentials from a secrets file.
@echo off
setlocal
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0uninstall.ps1" %*
exit /b %ERRORLEVEL%
15. Debugging order
Build Host and verify current SDK files; build module against that exact path; compare assembly and manifest versions; deploy the complete package; restart and inspect module/migration logs; call APIs directly; inspect `/api/platform/client-extensions`; verify JS/CSS network responses and route registration; test Designer mock data and Viewer runtime data.
Common failures include old Host/SDK references (`TypeLoadException`), manifest mismatch, unloaded Entity configuration, edited migration checksums, authentication/policy failures, and routes registered after initial navigation.
16. Versioning
Use patch for compatible fixes, minor for compatible additions, and major for breaking contracts. Synchronize project version, manifest, compatibility ranges, changelog, and all-module CI. Increase SDK minimum when needed; do not widen maximum without validation.
17. Completion checklist
- Only SDK/Abstractions references, both `Private=false`.
- Manifest, assembly, and ranges match.
- Immutable migrations cover three providers.
- Entity metadata matches schema and generated/write flags.
- APIs authorize, filter ownership, forward cancellation, and use common errors.
- Actions/triggers never trust client identity.
- Designer avoids runtime APIs; Preview/Viewer value semantics agree.
- English and Chinese messages/fallbacks exist.
- Package contains no private Host/SDK copy.
- PostgreSQL integration and critical Playwright flows are covered.
For a source-complete Chinese Todo walkthrough, see the dedicated tutorial. ResourceBooking is the hybrid reference module beside the FormPlatform repository.
---
Source layout and assembly boundaries
Purpose
The repository is organised by assembly ownership. A source file is compiled by exactly one project; the host consumes public code through project references rather than linked source files. Assembly names and public namespaces remain unchanged, so deployed modules continue to reference `FormPlatform.Sdk.dll` and `FormPlatform.Extension.Abstractions.dll`.
Layout
| Path | Builds | Responsibility |
|---|---|---|
| `src/FormPlatform.Host/` | `FormPlatform.dll` | ASP.NET Core host, official domain services, endpoints, configuration, Vue application and development module packages |
| `src/FormPlatform.Sdk/` | `FormPlatform.Sdk.dll` | Public ORM, dynamic entity/query/transaction APIs, trigger/action contracts, migrations, form-store contracts and shared media/error contracts |
| `src/FormPlatform.Extension.Abstractions/` | `FormPlatform.Extension.Abstractions.dll` | Stable module manifest and lifecycle contracts only |
| `src/FormPlatform.Licensing/` | `FormPlatform.Licensing.dll` | License document encryption/signature primitives |
| `samples/FormPlatform.Extension.Sample/` | sample module | Minimal reference module using the SDK and Abstractions |
| `tests/` | test assemblies | PostgreSQL and SQL Server integration tests |
| `src/FormPlatform.Host/Modules/` | runtime packages | Trusted modules discovered by the local development host |
`FormPlatform.slnx` is the root solution entry point for IDEs and CI.
Common commands
# Run the host from repository root
dotnet run --project src/FormPlatform.Host/FormPlatform.csproj
# Build the host and all official projects
dotnet build FormPlatform.slnx
# Build the browser client manually
cd src/FormPlatform.Host/ClientApp
npm ci
npm run build
Standalone and DNN packaging scripts remain at `deploy/` and use the new host project automatically. Third-party module deployment scripts should pass the host directory, for example `-FormPlatformDirectory C:\src\FormPlatform\src\FormPlatform.Host`.
Rules
1. Put code a third party must compile against in `src/FormPlatform.Sdk` or `src/FormPlatform.Extension.Abstractions`, never in the host.
2. A module references only SDK and Abstractions; it must not reference `FormPlatform.dll`.
3. Keep `Program.cs` a composition root. Host-specific endpoints belong under `src/FormPlatform.Host/Hosting/Endpoints`.
4. Do not add linked `Compile Include=... Link=...` entries to borrow SDK source into the host.
5. `src/FormPlatform.Host/Modules` is a runtime package location, not a place to author module source.
---
Private deployment and trusted secondary development
中文版本:
`PRIVATE_DEPLOYMENT_EXTENSION_ARCHITECTURE.zh-CN.md`
Goal
Customers receive compiled FormPlatform server binaries and compiled Vue assets,
not FormPlatform source. They are trusted secondary developers and may run their
own code inside the application process. They can:
- edit the deployed `appsettings.json` and add their own configuration sections;
- use the public ORM and metadata APIs;
- register server actions, triggers, DI services and hosted services;
- map arbitrary Minimal APIs for controls and integrations;
- add form controls to the Designer palette;
- extend client Action API and per-form Runtime API.
This model protects source distribution; it is deliberately not a plugin
sandbox. A server module has the same process permissions as FormPlatform and
must be treated as trusted application code.
Distribution model
The vendor distributes:
1. the Release output of `FormPlatform`;
2. the compiled Vue production assets under `wwwroot`;
3. `FormPlatform.Sdk.dll` as the server development reference;
4. `FormPlatform.Extension.Abstractions.dll` or its NuGet package;
5. API XML documentation, this guide and the sample extension.
Do not distribute FormPlatform `.cs`/`.vue` files, source maps, Git data, build
caches, private PDBs, signing keys or secrets. .NET IL and browser JavaScript can
still be reverse engineered; optional obfuscation and licensing only increase
the cost. Browser code must never contain secrets.
Server module
Implement `IFormPlatformSdkModule` from the compiled `FormPlatform.Sdk.dll`.
The four hooks run in this order:
For a customer project outside this source tree, use file references to the
vendor SDK directory and do not copy the platform assemblies into the module
package:
<ItemGroup>
<Reference Include="FormPlatform.Sdk" HintPath="..\sdk\FormPlatform.Sdk.dll" Private="false" />
<Reference Include="FormPlatform.Extension.Abstractions"
HintPath="..\sdk\FormPlatform.Extension.Abstractions.dll"
Private="false" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
The in-repository sample uses a project reference only so platform developers
can evolve the example with the source tree.
public sealed class CustomerModule : IFormPlatformSdkModule
{
public void ConfigureConfiguration(
ConfigurationManager configuration,
IHostEnvironment environment)
{
configuration.AddJsonFile(
Path.Combine(environment.ContentRootPath,
"Modules", "Customer", "appsettings.customer.json"),
optional: true,
reloadOnChange: true);
}
public void ConfigureServices(
IServiceCollection services,
IConfiguration configuration,
IHostEnvironment environment)
{
services.Configure<CustomerOptions>(configuration.GetSection("Customer"));
services.AddSingleton<IServerActionsProvider, CustomerActions>();
services.AddSingleton<CustomerService>();
}
public void ConfigureApplication(WebApplication application)
{
// Optional trusted middleware. FormPlatform exception handling and
// authentication already run before this middleware.
}
public void MapEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/api/customer/items", async (
CustomerService service, CancellationToken ct) =>
Results.Ok(await service.LoadAsync(ct)))
.RequireAuthorization();
}
}
`ConfigureServices` receives the normal ASP.NET Core service collection. An
extension may inject/use `IDynamicRepository`, `IUnitOfWorkFactory`,
`IEntityModelResolver`, `IJoinQueryService`, `IFormStore` and other public
FormPlatform services. Register `IServerActionsProvider` to add both form Server
Actions and entity Triggers. Use normal authorization on every extension API.
Package and load a server module
Each package is a directory below `Modules`:
Modules/
Customer/
module.json
Customer.Module.dll
Customer.Module.deps.json
appsettings.customer.json
client/
index.js
customer.css
`module.json`:
{
"entryAssembly": "Customer.Module.dll",
"type": "Customer.Module.CustomerModule"
}
Set the directory and client entry modules in the deployed application
`appsettings.json`:
{
"FormPlatformExtensions": {
"ModulesDirectory": "Modules",
"ClientModules": [
"/extension-assets/Customer/index.js"
]
},
"Customer": {
"ApiBaseUrl": "https://customer.example/api"
}
}
Restart the application after installing or replacing a server DLL. Module
configuration JSON can use `reloadOnChange`, but service registrations and
endpoint mappings require restart. Packages load deterministically by full
`module.json` path; prefix directory names (`010-Customer`, `020-Reports`) when
one customer module intentionally overrides a registration from another.
ORM from an extension
The ORM is an ordinary injectable public API. A typical query is:
var model = await models.ResolveAsync("Customer", ct);
await using var work = await workFactory.BeginAsync("Business", ct);
var rows = await repository.QueryAsync(
model,
new QuerySpec(
Filter: new FilterCondition("IsActive", FilterOperator.Equal, true),
OrderBy: [new("Name")],
Limit: 50),
work,
ct);
await work.CommitAsync(ct);
Models and data sources may be declared in the customer's deployed
`appsettings.json` under `DataAccess`, or managed with the existing Data Model
features. The extension uses the same providers, parameterized SQL, transactions
and trigger chain as the built-in application.
Module JSON is loaded before options are bound. Development User Secrets are
then re-applied, and the protected production secret JSON is applied last, so a
module configuration file cannot accidentally override secret values.
Client extension
The client loads each configured ES module before mounting Vue. Its default
export must contain `install(api)`.
export default {
install(api) {
const { h } = api.Vue
api.registerControl({
type: 'customerPicker',
component: {
props: ['value', 'designerMode'],
emits: ['update:value'],
setup(props, { emit }) {
return () => h('button', {
onClick: () => emit('update:value', 'customer-1')
}, props.value || 'Choose customer')
}
},
group: 'controls',
label: 'Customer Picker',
icon: '◆',
description: 'Selects a customer',
dataComponent: true,
create: () => ({ props: { value: '' } }),
events: [{ name: 'onChange', label: 'Value changed' }]
})
api.extendActionApi('openCustomer', ({ context }, id) =>
context.api.navigate(`/customers/${id}`))
api.extendRuntimeApi(runtime => {
runtime.customerValue = propertyName => runtime.data[propertyName]
})
}
}
`registerControl` automatically adds the component to runtime rendering and its
palette group. Optional `propertyEditor` is a Vue component used for the General
tab. The normal Style, Events, Tooltip and Other tabs remain available.
Client extension API:
- `api.Vue`: the exact Vue runtime instance used by FormPlatform;
- `api.registerControl(definition)`: control, palette metadata, factory,
property editor, events and data-control status;
- `api.extendActionApi(name, function)`: adds a method to
`window.FormPlatform.API` and form Action Code context;
- `api.extendRuntimeApi(function)`: decorates every newly created form runtime;
- extended runtime methods are available to Action Code through
`context.api.runtime` after the active `FormReader` is ready;
- `api.registerMessages(owner, messages)`: contributes i18n messages.
- `api.registerRoute(route)`: adds a Vue Router route supplied by the module;
- `api.configureApp(callback)`: performs advanced setup against the created Vue
app, router and Pinia instance before mount.
Client assets are served only from `Modules/<name>/client` and only for the
allowed web extensions. A client module can call its own server APIs with
`context.api.fetch` or normal `fetch`.
Building `.vue` controls
For ordinary Vue SFC development, build the customer control as an IIFE and
externalize `vue` to `FormPlatform.Extensions.Vue`; this prevents a second Vue
runtime from entering the page. The IIFE entry calls
`window.FormPlatform.Extensions.registerControl(...)` directly. Such a bundle
does not need an ES-module default export—the loader also accepts a module that
self-registers while executing. A minimal Rollup/Vite output configuration is:
build: {
lib: { entry: 'src/index.js', name: 'CustomerControls', formats: ['iife'] },
rollupOptions: {
external: ['vue'],
output: { globals: { vue: 'window.FormPlatform.Extensions.Vue' } }
}
}
The readable source and build project stay with the customer; only the generated
JavaScript and CSS are copied into `Modules/Customer/client`.
Trust and versioning
- Only administrators should install module DLLs; never provide anonymous DLL
upload.
- A module can do anything the FormPlatform OS account can do. Review customer
code and use a dedicated application identity with least-privilege database
credentials.
- Publish a versioned SDK/runtime bundle. Breaking public API changes require a
major version and a compatibility note.
- Use API/contract tests in customer modules. A runtime update should be tested
against all installed modules before production rollout.
- Keep secrets in User Secrets, environment variables, systemd credentials or
the existing external production secret JSON—not in module JavaScript.
The complete working examples are under
`samples/FormPlatform.Extension.Sample`.