@nomideusz/svelte-payments

A payment provider behind one interface, plus the orchestration around it — start a payment, settle it from a webhook, refund it. Mollie is implemented server-side; the Stripe piece is currently the Connect onboarding UI.

pnpm add @nomideusz/svelte-payments

StripeConnectStatus

Onboarding state for a provider's Stripe Connect account. Pure presentation — you supply the connected flag and the onboarding URL.

Connect your Stripe account to receive payments from guests. You'll be redirected to Stripe to complete the setup.

Connect with Stripe
<StripeConnectStatus
  connected={account.chargesEnabled}
  onboardingHref="/api/stripe/connect"
  connectLabel="Connect with Stripe"
/>

MollieCard

An embedded card form built on Mollie Components. It is not rendered here because it loads Mollie's SDK against a real profileId, and a demo profile would take live card input — not something to put on a public page.

<MollieCard
  profileId={PUBLIC_MOLLIE_PROFILE_ID}
  testmode={dev}
  locale="pl_PL"
  labels={{ cardNumber: 'Numer karty', cardHolder: 'Imię i nazwisko' }}
/>

Server: checkout orchestration

createCheckout wraps any PaymentProvider with a store you supply, so persistence stays in your app. Import from the /server subpath — it never reaches the browser.

import { mollie, createCheckout } from '@nomideusz/svelte-payments/server';

const checkout = createCheckout(mollie(env.MOLLIE_API_KEY), {
  savePayment: (p) => db.update(payments).set(p).where(eq(payments.id, p.id)),
  onPaid:      (p) => confirmBooking(p.metadata.bookingId),
  onFailed:    (p) => releaseHold(p.metadata.bookingId),
});

// 1. Start — redirect the customer to payment.checkoutUrl
const payment = await checkout.start({
  amount: 12000, currency: 'PLN',
  description: 'Sunrise Hatha class',
  redirectUrl, webhookUrl,
  billingEmail: guest.email,   // methods that ask (Przelewy24) prefill it
  metadata: { bookingId },
});

// 2. Settle — from your webhook route
export const POST = ({ request }) => checkout.handleWebhook(request);

// 3. The settled Payment carries the payer's real name when the
//    method reports one (P24 consumerName, card holder) — backfill
//    guest records created from an e-mail alone:
onPaid: async (p) => {
  if (p.consumerName) await adoptRealName(p.metadata.bookingId, p.consumerName);
}

SvelteKit caveat: Mollie's webhook POST is form-encoded and carries no Origin header, and SvelteKit's built-in CSRF protection 403s exactly that shape — it runs before any handle hook, so no hook can exempt the route. Disable it in svelte.config.js (csrf: { checkOrigin: false }) and re-implement the origin check yourself in hooks.server.ts, handling the webhook path first — otherwise every delivery fails and Mollie keeps retrying.

The provider interface is small on purpose — createPayment, getPayment, refund, webhookPaymentId — so a second provider is a new adapter rather than a new integration.

Server: idempotency keys

idempotencyKey on CreatePaymentInput, CreateSubscriptionInput and the refund() opts is sent as Mollie's Idempotency-Key header — a retried mutating call (timeout, crash between your DB write and the provider's response) returns the original result instead of acting twice.

// Insert a keyed row in YOUR db first, then call with the key —
// if the process dies mid-call, the retry replays instead of duplicating.
const key = crypto.randomUUID();
await db.insert(paymentAttempts).values({ key, bookingId });

const payment = await checkout.start({
  amount: 12000, currency: 'PLN',
  description: 'Sunrise Hatha class',
  redirectUrl, webhookUrl,
  metadata: { bookingId },
  idempotencyKey: key,
});

// Same shape on the other mutations:
await provider.createSubscription({ ...input, idempotencyKey: key });
await provider.refund(paymentId, {
  amount: 12000, currency: 'PLN', idempotencyKey: key,
});

The sharpest failure this closes: subscription-create timeout → your app releases its DB claim → retry → two live monthly charges on the same mandate.

Server: Connect / OAuth tokens

Plain test_/live_ API keys decide their own mode, but OAuth (Connect) tokens span live+test and need an explicit flag on every call — mollie(token, { testmode: true }) puts testmode: true in POST and DELETE bodies and ?testmode=true on reads. Organization-level tokens must also name which profile a payment belongs to, and can take a platform fee.

import { mollie } from '@nomideusz/svelte-payments/server';

// OAuth access token instead of an API key — testmode must be explicit
const provider = mollie(school.mollieAccessToken, { testmode: dev });

const payment = await provider.createPayment({
  amount: 12000, currency: 'PLN',
  description: 'Sunrise Hatha class',
  redirectUrl, webhookUrl,
  profileId: school.mollieProfileId,  // org-level tokens: required per payment
  applicationFee: {
    amount: 240,                      // integer minor units, payment currency
    description: 'platform fee',
  },
});

All of it is optional — with a plain API key the adapter behaves exactly as before.