Vendune
extensions/README.mdView on GitHub ↗

Tenant extension examples

The working ABI is approve(total_minor: i64, limit_minor: i64) -> i32. Amounts are integer EUR cents; zero rejects and any nonzero result allows. The hook runs in the actual B2B order transaction, after authoritative item + shipping totals and inventory checks, before any stock/order mutation. B2C orders do not invoke this hook. No guest can rewrite prices or access data.

Example Business policy Boundary example
company-limit.wat Supplied company purchase budget 100,000 cents allowed; 100,001 rejected
budget-reserve.wat Preserve EUR 100 of that budget At a EUR 1,000 budget, EUR 900 allowed; EUR 900.01 rejected
minimum-order.wat Minimum EUR 50, respecting the budget EUR 49.99 rejected; EUR 50 allowed
single-order-cap.wat At most EUR 250 per purchase, respecting the budget EUR 250 allowed; EUR 250.01 rejected

The current demo supplies a EUR 1,000 per-purchase budget. It is not a monthly/remaining-credit ledger; purchases do not consume a company budget. Policies are alternatives: activating one replaces the previous hook for the selected tenant. They are not automatically composed.

Activate a policy

An owner/administrator can call POST /api/extensions/activate with {"wat":"...contents of the selected example..."}, personal bearer session, and x-tenant. The server compiles and probes it before persisting source, SHA-256 digest and revision. Checkout reuses compiled modules while creating an isolated Store for each invocation. It reads the persisted policy inside the transaction: changes from another application instance refresh a stale local compiled module before purchase. Restarts restore the persisted source.

No host imports, WASI, filesystem, network or clocks are supplied. Guests have 10,000 fuel units, a 1 MiB linear-memory limit, a 256 KiB stack limit and a 32 KiB source limit. Traps fail closed and roll back the order transaction. This is Wasmtime compilation of Wasm/WAT, not PHP-to-Rust transpilation. Arbitrary existing Shopware plugins are not supported.

Verify actual behavior

set -a; source .env; set +a
cargo test --locked sandbox
REPORT_PATH=.run/extensions.json python3 scripts/extensions.py

The HTTP check temporarily replaces the workshop demo policy, submits blocked and allowed synthetic B2B purchases through the real cart/checkout API, checks host-import/fuel/memory failures, and restores the exact previous policy even on failure. It creates three simulated orders and changes synthetic inventory. Use it against the local demo only. Set TEST_TENANT to another synthetic shop containing the demo B2B buyer and template catalog if needed.

Adding hooks for discount rules, content or fulfillment requires a new typed host contract, role authorization and transaction tests. A guest does not gain those capabilities merely by exporting a function with that name.

Versioned apps · API 1

The WAT examples above remain supported. The broader app API adds independently installed packages with their own data, actions and UI. See full implementation and limits.

Package Executable features Example files
Native care studio Visual-builder-compatible admin form/table, public product-detail cards, own translated records, HTTP/MCP and flow actions manifest, guide
Product personalization Own price-rule entity; HTTP/MCP read/write; product form; server-taxed per-unit cart surcharge; order configuration view; agent preview/approval manifest
Workshop notes Own notes/tickets tables and tenant foreign reference; admin forms; native MCP actions; external service action, opaque iframe UI and durable SQLite event inbox manifest, service, browser SDK
PayPal Sandbox/Live Native provider adapter, checkout handoff, paid/refund ledger and app administration manifest, src/payments
Shopware Payments readiness Explicit connector status in admin; cannot process payment until its official standalone contract is verified manifest

Install through Studio's package review, or request POST /api/apps/review with {"builtIn":"engraving"} / {"manifest":...}. Submit the exact reviewed digest, complete permissions and approve:true with the installation request. An owner/admin personal session and owning x-tenant are required. Immutable versions, schema changes and outbox commit atomically; activation changes retain data/history and check dependent apps. This is not destructive uninstall.

Managed fields include translated strings, JSON, integer/boolean, date/datetime, scaled decimal, explicit-currency money, rich text, files/images and scalar/multiple relations. Every tenant/app owns its physical tables with forced RLS, composite foreign keys, recursive validation, unique constraints and storage quotas. Explicit migration steps can rename/remove/convert/fill fields with bounded recovery snapshots. No app-supplied SQL is executed.

Capabilities separately declare core reads/writes, PII, precise event names, assets, native/iframe surfaces, AI/MCP exposure and flow access. Callback keys are short-lived, revocable and package-bound; surface grants also bind context IDs and action allowlists on the server. External service credentials need exact operator-approved manifest digests, and HTML bundles need byte-hash pins. Private shop/app secrets use authenticated encryption and optimistic rotation. See the canonical app contract and security/operating limits before deploying a service.

List/save your data through /api/apps/{id}/entities/{entity}. Saves use {"id":"one","revision":0,"fields":{"title":"My note"}}; existing records require their actual revision. The same operation is a declared action at /api/apps/{id}/actions/{name} and MCP app.{id}.{name}. The planner exposes registered managed save actions as reviewable proposals, with revisions bound by the server. Existing-record revisions omitted/guessed by the model cannot bypass the comparison.

Run the external app

Start the provided service in a separate terminal:

APP_TOKEN=choose-a-private-development-token \
APP_DB=.run/workshop-app.sqlite \
python3 extensions/apps/service-example/server.py

Build the self-contained UI with python3 scripts/build_app_ui.py service-example --output .run/service-example.html. Hash the built bytes for uiDigests; obtain the package digest from target/debug/vendune --app-digest extensions/apps/service-example/manifest.json. Configure the core and independently deployed app worker with these exact server-only pins, then restart them:

export APP_SERVICES='{"workshop_notes":{"url":"http://127.0.0.1:8795","uiUrl":"http://127.0.0.1:8795/","token":"choose-a-private-development-token","approvedDigests":["RUST_CANONICAL_PACKAGE_SHA256"],"uiDigests":{"v1/index.html":"BUILT_HTML_SHA256"}}}'
# Main HTTP process; projects core events without delivering external events:
PROCESS_ROLE=http target/debug/vendune
# Separate terminal, with the same DB/APP_SERVICES configuration:
PROCESS_ROLE=app-worker target/debug/vendune

Install apps/service-example/manifest.json in your synthetic shop with POST /api/apps. Apps shows managed data editors and the iframe. The UI uses connectCommerce() from the SDK and can call sdk.action('notes') or sdk.action('availability', {sku:'mug'}). Core session tokens never enter this iframe. The availability result is explicitly synthetic, not a real ERP. The app UI owns its content localization; the host supplies the selected locale in the SDK context. This example provides English, German, French and Spanish copy and shows ordinary notes instead of raw JSON.

Events contain tenant, event ID, kind, data and stable idempotencyKey. The service must deduplicate (tenant, idempotencyKey) in its own storage. Delivery is at least once; timeouts are retried, never exactly-once remote execution. The SQLite example persists and deduplicates its inbox. Eight failed deliveries move to failed; an operator retry/dead-letter dashboard remains future work.

External-service execution is a separate process, not a microVM sandbox. Deployment needs resource/network limits; the supplied service is a development example. The browser boundary is an opaque iframe and constrained action bridge. No app may choose an arbitrary backend URL through a merchant manifest.

App and payment verification

set -a; source .env; set +a
python3 scripts/apps.py
python3 scripts/services.py
python3 scripts/payments.py
# Real local inference, using the synthetic workspace created by apps.py:
TEST_MODEL=1 python3 scripts/app_inference.py

These use real core/DB operations and a real standalone SQLite service. Payment wire responses/signature verification are simulated by a local contract server; no actual PayPal Sandbox account is contacted. Real local inference is separate.

App-owned product configuration

Product configuration is a host capability, not engraving logic in the core. The two runnable examples are engraving and gift message. Their configuration.wat files own their rules: engraving accepts 1–40 characters and fees of 0–100,000 cents; gift messages accept 1–12 characters and fees of 0–500 cents. Each manifest embeds the matching Wasm source, localized labels/hints, its own input field, typed entity and default data. Submit the gift-message manifest through the normal app installation API; no Rust branch or recompilation is needed to add it.

The generic host invokes two exports:

validate_fee(fee_minor: i64) -> i32          # 1 accepts app price data
configuration_fee(fee_minor: i64, input_length: i64) -> i64
                                          # negative rejects; otherwise gross EUR cents per item

The platform enforces printable bounded input, tenant/authentication boundaries, resource and money ceilings, immutable package versions, cart/data revisions and authoritative quantity/tax calculation. The package owns its business predicates. POST /store-api/apps/{id}/configure accepts {"productId":"mug","revision":3,"fields":{"message":"For Ada"}} with the cart context token and owning tenant. Input field names are declared by the package. Multiple apps compose on a SKU; order appConfigurations records each app separately. The generic storefront renders registered product-configuration slots.

This initial pure-Wasm contract passes price and character count, not full text or arbitrary product data into Wasm. Richer rules need a versioned typed ABI or an external service contract; the example does not claim a universal configurator.

Upgrade engraving 1.0 to 1.1 explicitly from Apps. Existing rule data remains; open carts must be reconfigured against the new version. Completed orders retain their original snapshot and idempotent checkout replay. The small core compatibility.rs adapter reads the older cart format; it contains no current engraving acceptance or price rules.

Storyfront connector app

apps/storyfront declares an independent service app with a multilingual merchant iframe. generate imports this tenant's catalog into Storyfront; status reports the durable job and configured shop URL. The companion Ambient-C connector owns manifest mapping and publication. The core exposes only generic app actions and a single-use checkout transfer. See the complete setup and limits.

Packing workflow app

packing-helper supplies translated packing checklists, an app-owned typed entity and an admin form. Its workflow definition adds a packed order state and a single-click “Confirm packed” action. It belongs to Apps → Operations.

Install the manifest through POST /api/apps, then save the reviewed definition through PUT /api/merchant/order-state-machine with {"revision":0,"data":<workflow.json>} (use the current revision returned by GET). The equivalent MCP tools are merchant.workflow and merchant.workflow.save. Saving app provenance requires the installed app, apps.manage and settings.write; orders retain their ordinary payment/delivery business guards. The workflow can be staged and selectively released as order-workflow.

Committed actions emit order.state_changed for native flows and external app inboxes. A note flow can attach a visible activity to that event exactly once. The example does not run arbitrary code on the transition, automatically install its workflow, or implement the complete Shopware graphical Flow Builder. python3 scripts/merchant_operations.py exercises installation, validation, concurrent exact-once transition, the actual flow note and selective publication.

Connected provider apps and order-alert example

apps/google-analytics, apps/gmail and apps/slack are service apps. Their provider implementation lives outside the Rust kernel in services/connectors/; their manifest capabilities work through the ordinary HTTP/MCP action gateway. See the OAuth, private knowledge and deployment guide.

apps/order-alerts/manifest.json demonstrates an events.publish action, publishing app.order_alerts.support_received after schema/merchant permission checks. Install it through POST /api/apps with {manifest: ...}. slack-flow.json is a real graphical flow definition: orders above EUR 100 in the consumer group invoke slack.post_order. Install/connect Slack first, choose a bot-accessible channel, then save the definition through PUT /api/automation/flows/order_alerts with {revision:0,data: ...}. The four-language instruction is the Slack template. The core supplies a stable delivery key, event kind and sanitized order facts; the app handles external delivery receipts.

GET /api/automation/catalog lists actual app actions and emitted/import events for the visual builder. A mail import can trigger app.gmail.source_imported and test eventField facts such as sourceKind, title or metadata.orderNumber. AI-proposal actions remain reviewable; private support text never becomes a system instruction.

The reusable sdk/analytics.js adapter also supports headless storefronts. Read only public google_analytics.tracking configuration for the correct tenant/sales channel, collect customer consent, then call event() for real ecommerce actions. Never embed merchant/provider credentials or duplicate a tag managed by another storefront plugin.

Full UI/API/AI apps

Product Lab owns a Studio module, product-detail panel, storefront page, API aliases, selected AI context, managed JSONB guides and an independent SQLite/event inbox. Run PRODUCT_LAB=1 ./scripts/dev.sh from the repository root; install the manifest explicitly. No core recompilation is needed. Guest UIs can use any framework through the scoped SDK, including live context updates and size requests. Read the complete extension contract and resource/performance boundaries.

Email Delivery app

apps/email connects standard SMTP (STARTTLS or implicit TLS), Resend and SendGrid through the same private API/MCP/flow capability. Its transport, encrypted settings and durable PostgreSQL jobs live in the focused Rust modules under src/connectors/. The service image contains no Python interpreter; external apps remain language-independent. Use order-confirmation-flow.json after installing/configuring the app; never enable the automatic all-order path and an equivalent flow unintentionally. See the complete configuration, template and test guide.

Guided app examples

Twelve assistant contracts cover frontend/admin/combined, payment/shipping/ERP services, event subscriptions, signed webhook ingress and persistent UTC schedules, with product/customer/order bindings and independent team/MCP access. Provider contracts require provider service implementations.

Payment providers

apps/payment-provider/manifest.json is a provider-neutral service contract with translated methods, account onboarding and protected payment commands for HTTP, MCP and Flow Builder. App Studio edits the same contract. The public core validates exact receipts and owns stock/order/refund jobs; proprietary PSP calls remain in a separate private service. See Payment provider API 1 for deployment, embedded checkout, immutable upgrades, callbacks and limits. The example is synthetic; it does not call Stripe or collect money.

App translations

Use sdk.uiText for interface controls and sdk.text for merchant-owned content. The host supplies interface/content/main languages and enabled shop locales; both native and external apps retain per-field inheritance. See the complete contract and examples. All bundled manifests and guest catalogues are checked by npm --prefix frontend run localization.

Platform-v2 examples

  • Catalog export: leased background job, scoped product callback, private CSV upload and downloadable result.
  • Typed commerce hooks: read-only WIT snapshots drive native price, discount, shipping and validation decisions.
  • Signed package tooling: canonical Rust digest/signature, publisher keys, Semver dependencies and stable/beta metadata.
  • Visual App Studio: raster, code-behind, model wizard, autosaved drafts, F5 private records and immutable publication share one schema.

Independent Python examples demonstrate a language-neutral contract; they are not bundled production runtimes. Outgoing event signatures are verified by the shared receiver SDK, with tenant/app-bound batches and durable consumer deduplication. Do not replace that path with unvalidated webhook JSON.

  • Care knowledge: graph-native app model/edge mapping over current native records, shared App Studio/agent manifest and authorized API/MCP/planner views.