Widget Marketplace
How to participate in the SveltyCMS widget ecosystem.
On this page
The marketplace is the central hub for extending SveltyCMS capabilities.
πͺ Portable Modules
All marketplace widgets are βPortable Modulesβ. They are self-contained and include their own logic, UI, and tests.
π‘οΈ Submission Standards
- Must follow the 3-Pillar Architecture.
- Must include unit tests in a
tests/folder. - Must be accessible (WCAG 2.2 AA).
- Must follow the single naming convention (below). Invalid packages are not registered.
Naming Convention (required)
Custom widgets (src/widgets/custom/) and marketplace packages (src/widgets/marketplace/) share one rule set with core:
| Layer | Format | Example |
|---|---|---|
| Folder | kebab-case | phone-number/ |
Factory Name |
PascalCase (or acronyms) | Name: "PhoneNumber" |
| Schema field | factory Name only | widget: { Name: "PhoneNumber" } |
Invariant: widgetNameToFolder(Name) === folderName
| Folder | Name |
Status |
|---|---|---|
phone-number |
PhoneNumber |
β |
seo |
SEO |
β |
ai-enrichment |
AIEnrichment |
β |
PhoneNumber |
PhoneNumber |
β folder not kebab-case |
phone-number |
phone-number |
β Name not PascalCase |
phone-number |
Phone |
β Name does not map to folder |
Helpers live in src/widgets/widget-naming.ts. Registration (store, proxy, WidgetRegistryService) fails closed for custom/marketplace when naming is invalid, so loaders never look for the wrong path.
Scaffold
src/widgets/marketplace/phone-number/
index.ts # createWidget({ Name: "PhoneNumber", β¦ })
input.svelte
display.svelte
tests/
// index.ts β Name must match folder via widgetNameToFolder
export default createWidget({
Name: "PhoneNumber", // β folder phone-number
Icon: "mdi:phone",
// β¦
});
Schemas always use the factory Name (never the folder string):
fields: [{ db_fieldName: "phone", widget: { Name: "PhoneNumber" } }];
π System Discovery & Autoloading
Marketplace widgets do not require compile-time registration or hardcoded static imports. SveltyCMS automatically registers optional/marketplace modules:
- Vite glob:
src/widgets/marketplace/*/index.tsand*.svelte(same as custom) when packages exist at build time. - Dynamic scan: At boot,
WidgetRegistryServicealso scanssrc/widgets/marketplace/for packages added on disk. - Naming gate: Folder must be kebab-case; factory
Namemust satisfy the invariant above or the package is skipped with an error log. - Drag-and-drop install/delete works with zero config only if naming is correct.
π° Monetization & Licensing Models
Widgets in the marketplace can be published under one of three pricing models:
| Tier | payment | Trial | Description |
|---|---|---|---|
| Free | None | N/A | Available to all installations indefinitely. |
| Hybrid (Freemium) | Paid | 14-Day | Basic fields/inputs are free. Advanced fields require an active license. |
| Fully Paid | Paid | 14-Day | The entire widget is locked behind a license verification gate on install. |
The 14-Day Installation Trial
All paid/freemium widgets support a key-less 14-day trial period calculated from the creation timestamp of the first registered administrator user.
- Key-less Activation: During these first 14 days, license checks return
active: trueeven if no license key is supplied. - Superadmin Demo Keys: Superadmins can provision special license keys starting with
SLM-DEMO-. These keys extend access by exactly 14 days from the registration timestamp.
How to Implement License Checks
To enforce licensing, invoke checkExtensionLicense("widget", "widget-id") within the widgetβs lifecycle.
1. Server-Side Verification (modifyRequest)
In your widget definition (index.ts), intercept save operations (POST/PATCH) to strip or restrict premium data if the user does not have a license:
import { checkExtensionLicense } from "@src/utils/license-manager";
const MyWidget = createWidget({
Name: "PremiumWidget",
// ...
modifyRequest: async ({ data, type }) => {
const value = data.get();
if (!value) return data;
if (type === "POST" || type === "PATCH") {
const status = await checkExtensionLicense("widget", "my-widget-id");
// If trial has expired and no valid license key is configured
if (!status.active) {
// Strip premium attributes
delete value.premiumField;
console.warn("[my-widget] Premium fields stripped due to missing license.");
}
}
data.update(value);
return data;
},
});
2. Client-Side Upgrade Prompts (input.svelte)
Wrap premium configuration features or inputs in Svelte templates using the <UpgradePrompt /> component:
<script lang="ts">
import UpgradePrompt from '@components/ui/upgrade-prompt.svelte';
let { value, status } = $props();
// status is resolved via checkExtensionLicense
</script>
{#if status.active}
<!-- Render Advanced Premium Configuration -->
<input type="text" bind:value={value.premiumField} />
{:else}
<!-- Prompt to Upgrade -->
<UpgradePrompt extensionId="widget:my-widget-id" price="β¬14.99" />
{/if}