@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.