Back to docs Paiements
Live

Paiements

The plumbing that gets you paid. A single boundary between Freelance OS and Stripe, resolved per workspace through Stripe Connect. Everything that takes money in the platform, product checkout, end-user billing, invoice payment, flows through this layer.

Overview

Getting paid is infrastructure. The Payments module has no screen of its own: it is the technical layer the other hubs call when money has to change hands. A product order, an invoice to settle, a subscription to your app, a booking deposit: they all take money through the same path. You connect Stripe once, and the rest works.

Under the hood, a single interface forms the boundary with the PSP: the PaymentGateway port from the @fos/domains-payments package. The app never talks to Stripe directly. It talks to the port. The types are normalized, with no Stripe-specific shape, so a second provider (PayPal, Mollie, Adyen) is a new class to write, not a rewrite of the payment routes. Today, only one provider is actually implemented behind that port: Stripe.

The gateway is resolved per workspace, not globally. Each workspace connects its own Stripe account through Connect Standard. The platform keeps one Stripe key and acts on behalf of the connected account by passing its identifier on every call. Until a workspace connects Stripe, this layer stays inert: it cannot take payments. That is the one action needed to switch it on.

What the module does

A port, not an SDK

The heart of the module is an interface, PaymentGateway. Every operation the app needs to take money is declared on it: create a customer, capture a card without charging, create a payment intent, open a hosted checkout, charge off-session, verify a webhook. Routes, crons and public pages depend on this interface, never on hard-coded stripe.* calls. Adding a provider means writing a class that implements the port and registering it, without touching anything else.

Stripe Connect per workspace

Each workspace connects its own Stripe account. The platform holds one secret key and acts on behalf of the connected account by passing its identifier (acct_...) on every Stripe call. The connected account secret is never stored: Connect Standard relies on the platform auth plus the account header. A workspace with no connected account falls back to the platform account (legacy mono-tenant behavior), but a new workspace must connect Stripe before it can take payments.

  • Account resolved from workspace_stripe_accounts, cached for 60 seconds
  • The connected flag says whether the workspace wired its own account or falls back to the platform
  • The connected account publishable key is what public pages use to mount Stripe Elements

What the gateway can do

The port covers the full lifecycle of a payment. Each operation returns a normalized, PSP-agnostic type, so the caller never writes Stripe-specific logic.

  • PSP customer: fetched or recreated if the stored id is dead
  • Card capture (SetupIntent): saves a payment method without a charge
  • On-session payment (PaymentIntent): the client pays on the page, with 3DS handled
  • Hosted checkout: the PSP hosts the page for a one-off payment, redirects to success or cancel
  • Off-session charge: debits a saved card without the client present, with an idempotency key against double charges
  • Webhook verification: checks the signature and returns a normalized event

Where the money comes in

The Payments layer has no screen of its own. It is consumed by the surfaces that sell. An invoice payment opens a branded checkout page that mounts Stripe Elements and keeps the client on your domain. A product checkout creates an order, applies shipping and coupons, charges through Connect. A subscription to a Collection bills the end-user on top. A booking deposit runs through the same path. On platform flows, a commission (application fee) can be taken on each session.

One PSP written today

The design is provider-agnostic, but to be honest: only Stripe is actually implemented behind the port. PayPal, Mollie and Adyen are planned identifiers, not written gateways. A provider that is chosen but not implemented fails loudly, it never silently falls back to Stripe: taking money through the wrong PSP would be worse than not taking it. And until Stripe is connected at the workspace level, this whole layer stays inert.

How to use it

  1. 01

    Connect Stripe

    From Settings then Integrations, you click Connect Stripe. You authorize the platform on stripe.com (log in or sign up), you come back, and the connected account id is stored on your workspace.

  2. 02

    The workspace becomes payable

    Once Stripe is wired, the gateway resolves with connected set to true. The surfaces that sell see a ready account and can create payments.

  3. 03

    A client pays

    On an invoice payment page or a product checkout, the app creates an intent server-side and mounts Stripe Elements, or opens a hosted checkout for a one-off payment. The client pays without leaving your domain.

  4. 04

    The webhook confirms

    Stripe calls the platform back. The signature is verified by the port, the event is normalized, and the caller flips the invoice or order to paid.

  5. 05

    Charge automatically

    For a recurring or off-session debit, a cron calls chargeOffSession on the saved card, with an idempotency key. The operation never throws: it returns an outcome (success, decline, action required, error) that the caller handles in a switch.

Connections