Not released yet. PeltyCMS is under development and not released yet; these docs describe the upcoming release and may change.
PeltyCMS Developer docs
User docsDevelopers
v0.1.0

Developer docs/Guides

Gateways, provisioning and registrars

These addon kinds use the same WASM contract as other addons. Each one implements a fixed set of ops, and the SDK provides a macro so you only implement a Rust trait. The traits below are copied from the source.

#Payment gateways

Use pelty_addon_sdk::gateway_addon!. The host-side trait:

crates/pelty-payments/src/lib.rs · pub trait PaymentGatewayRust
/// The stable gateway contract.
pub trait PaymentGateway: Send + Sync {
    fn id(&self) -> &'static str;
    fn display_name(&self) -> &'static str;
    fn capabilities(&self) -> Capabilities;
    fn create_payment<'a>(
        &'a self,
        req: &'a PaymentRequest,
    ) -> BoxFuture<'a, Result<PaymentInit, GatewayError>>;
    /// Verify the signature and parse the event. `now` is unix seconds.
    fn parse_webhook(
        &self,
        headers: &Headers,
        body: &[u8],
        now: i64,
    ) -> Result<WebhookEvent, GatewayError>;
    /// Async webhook entry point used by the HTTP layer. Defaults to
    /// [`parse_webhook`](Self::parse_webhook); gateways whose webhooks carry no
    /// signature (Mollie) override it to fetch the authoritative state.
    fn handle_webhook<'a>(
        &'a self,
        headers: &'a Headers,
        body: &'a [u8],
        now: i64,
    ) -> BoxFuture<'a, Result<WebhookEvent, GatewayError>> {
        Box::pin(async move { self.parse_webhook(headers, body, now) })
    }
    /// Finish a payment after the payer returned from the provider (PayPal
    /// capture, or a status poll). Default: not needed (webhook-driven).
    fn complete_payment<'a>(
        &'a self,
        _provider_ref: &'a str,
        _query: &'a HashMap<String, String>,
    ) -> BoxFuture<'a, Result<CompleteStatus, GatewayError>> {
        Box::pin(async { Err(GatewayError::Unsupported) })
    }
    fn refund<'a>(
        &'a self,
        provider_ref: &'a str,
        amount: Money,
        idempotency_key: &'a str,
    ) -> BoxFuture<'a, Result<RefundResult, GatewayError>>;
    /// Attach a client-side created method token (e.g. a Stripe `pm_…`) for
    /// future off-session use. Gateways without `recurring` keep the default.
    fn save_method<'a>(
        &'a self,
        _token: &'a str,
        _customer_email: &'a str,
        _idempotency_key: &'a str,
    ) -> BoxFuture<'a, Result<SavedMethod, GatewayError>> {
        Box::pin(async { Err(GatewayError::Unsupported) })
    }
    /// Charge a saved method off-session.
    fn charge_saved<'a>(
        &'a self,
        _token: &'a str,
        _req: &'a PaymentRequest,
    ) -> BoxFuture<'a, Result<ChargeOutcome, GatewayError>> {
        Box::pin(async { Err(GatewayError::Unsupported) })
    }
}

#Provisioning modules

Use pelty_addon_sdk::provisioning_addon!:

crates/pelty-provisioning/src/lib.rs · pub trait ProvisioningModuleRust
pub trait ProvisioningModule: Send + Sync {
    fn info(&self) -> ModuleInfo;
    /// Fields asked at checkout for this module's products.
    fn checkout_fields(&self) -> Vec<CheckoutField> {
        Vec::new()
    }
    fn test_connection<'a>(&'a self, server: &'a ServerConn) -> BoxFuture<'a, ProvisionResult<()>>;
    fn create<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<Created>>;
    fn suspend<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
    fn unsuspend<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
    fn terminate<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
    /// Apply the (new) product config in `svc.product_config` to the remote.
    fn change_package<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<()>>;
    /// Client single sign-on: the URL to send the client to, carrying the
    /// signed one-time `token` (see [`sso`]). `None` means unsupported. The
    /// default uses the server's `sso_url` template; modules may override it
    /// (e.g. to exchange the token for a panel-native login link).
    fn sso_url<'a>(&'a self, server: &'a ServerConn, svc: &'a ServiceCtx, token: &'a str) -> BoxFuture<'a, ProvisionResult<Option<String>>> {
        Box::pin(async move { Ok(sso::template_url(server, svc, token)) })
    }
    /// Names of extra actions this module supports (custom buttons).
    fn custom_actions(&self) -> &'static [&'static str] {
        &[]
    }
    /// Run one extra action. `params` carries user input (e.g. a new
    /// password or an image id). Returns module-specific output (a console
    /// URL, a new password…) to show once.
    fn custom<'a>(&'a self, _server: &'a ServerConn, _svc: &'a ServiceCtx, action: &'a str, _params: &'a Value) -> BoxFuture<'a, ProvisionResult<Value>> {
        Box::pin(async move { Err(ProvisionError::Config(format!("action {action} is not supported by this module"))) })
    }
    /// Live resource usage / status for the client area (`None` = unsupported).
    fn usage<'a>(&'a self, _server: &'a ServerConn, _svc: &'a ServiceCtx) -> BoxFuture<'a, ProvisionResult<Option<Value>>> {
        Box::pin(async move { Ok(None) })
    }
    /// Accounts that exist on the server, for import/sync (`None` = unsupported).
    fn list_accounts<'a>(&'a self, _server: &'a ServerConn) -> BoxFuture<'a, ProvisionResult<Option<Vec<RemoteAccount>>>> {
        Box::pin(async move { Ok(None) })
    }
    /// Vendor resale (MarketConnect-style): products this vendor sells,
    /// with the reseller's cost. `None` = not a vendor module.
    fn catalog<'a>(&'a self, _server: &'a ServerConn) -> BoxFuture<'a, ProvisionResult<Option<Vec<VendorProduct>>>> {
        Box::pin(async move { Ok(None) })
    }
}

#Domain registrars

Use pelty_addon_sdk::registrar_addon!:

crates/pelty-provisioning/src/registrar.rs · pub trait RegistrarModuleRust
pub trait RegistrarModule: Send + Sync {
    fn id(&self) -> &'static str;
    fn name(&self) -> &'static str;
    fn available<'a>(&'a self, domain: &'a str) -> BoxFuture<'a, RegistrarResult<bool>>;
    /// Register for `years`; `expires_on` is what the caller computed.
    fn register<'a>(&'a self, domain: &'a str, years: u32, expires_on: &'a str, registrant: &'a Contact, nameservers: &'a [String]) -> BoxFuture<'a, RegistrarResult<()>>;
    /// Register with the TLD's custom domain fields (`{key: value}` as set
    /// up per TLD in Admin → Domain pricing). Default: fields are ignored.
    fn register_with_fields<'a>(&'a self, domain: &'a str, years: u32, expires_on: &'a str, registrant: &'a Contact, nameservers: &'a [String], fields: &'a serde_json::Value) -> BoxFuture<'a, RegistrarResult<()>> {
        let _ = fields;
        self.register(domain, years, expires_on, registrant, nameservers)
    }
    fn renew<'a>(&'a self, domain: &'a str, years: u32, expires_on: &'a str) -> BoxFuture<'a, RegistrarResult<()>>;
    fn transfer<'a>(&'a self, domain: &'a str, auth_code: &'a str, expires_on: &'a str) -> BoxFuture<'a, RegistrarResult<()>>;
    /// Transfer with custom domain fields. Default: fields are ignored.
    fn transfer_with_fields<'a>(&'a self, domain: &'a str, auth_code: &'a str, expires_on: &'a str, fields: &'a serde_json::Value) -> BoxFuture<'a, RegistrarResult<()>> {
        let _ = fields;
        self.transfer(domain, auth_code, expires_on)
    }
    fn set_nameservers<'a>(&'a self, domain: &'a str, nameservers: &'a [String]) -> BoxFuture<'a, RegistrarResult<()>>;
    fn set_contact<'a>(&'a self, domain: &'a str, registrant: &'a Contact) -> BoxFuture<'a, RegistrarResult<()>>;
    fn info<'a>(&'a self, domain: &'a str) -> BoxFuture<'a, RegistrarResult<RemoteDomain>>;
    /// Transfer-out authorization (EPP) code. `None` = not supported.
    fn epp_code<'a>(&'a self, _domain: &'a str) -> BoxFuture<'a, RegistrarResult<Option<String>>> {
        Box::pin(async { Ok(None) })
    }
    /// Cost price list for TLD price sync. `None` = not supported.
    fn tld_prices<'a>(&'a self) -> BoxFuture<'a, RegistrarResult<Option<Vec<TldCost>>>> {
        Box::pin(async { Ok(None) })
    }
    /// Premium price of `domain`; `None` = standard pricing (or unsupported).
    fn premium_price<'a>(&'a self, _domain: &'a str) -> BoxFuture<'a, RegistrarResult<Option<PremiumQuote>>> {
        Box::pin(async { Ok(None) })
    }
    /// A copy configured with the registrar addon's settings (API keys…).
    /// `None` = the module needs no settings and `self` is used as is.
    fn configure(&self, _settings: &serde_json::Value) -> Option<Arc<dyn RegistrarModule>> {
        None
    }
}

#SDK macros

extensions/addons/sdk/src/lib.rs · macro_rules! provisioning_addonRust
/// Export a provisioning addon from an expression building the module.
#[cfg(feature = "provisioning")]
#[macro_export]
macro_rules! provisioning_addon {
    ($module:expr) => {
        struct PeltyAddon;
        impl $crate::Guest for PeltyAddon {
            fn describe() -> String {
                $crate::provisioning::describe(&$module)
            }
            fn call(op: String, input: String) -> Result<String, String> {
                $crate::provisioning::call(&$module, &op, &input)
            }
            fn on_event(_name: String, _payload: String) -> Result<(), String> {
                Ok(())
            }
        }
        $crate::export!(PeltyAddon with_types_in $crate);
    };
}