Skip to content

Documentation

Widget Marketplace

How to participate in the SveltyCMS widget ecosystem.

5/6/2026
4 min read Edit on GitHub

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:

  1. Vite glob: src/widgets/marketplace/*/index.ts and *.svelte (same as custom) when packages exist at build time.
  2. Dynamic scan: At boot, WidgetRegistryService also scans src/widgets/marketplace/ for packages added on disk.
  3. Naming gate: Folder must be kebab-case; factory Name must satisfy the invariant above or the package is skipped with an error log.
  4. 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: true even 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}

Related

widgetsmarketplaceguide
Was this page helpful?