Skip to content

Mental model

SAPA is a monetization decision engine that you run inside your own Effect runtime. You describe what can be sold, send it commands, and query the state derived from accepted history. SAPA does not replace your product database, authentication, HTTP framework, or payment provider.

The easiest way to understand the system is to separate two planes:

  • Definition plane: versioned descriptions of offers and installed capabilities. This answers “what are we allowed to sell and fulfill?”
  • Execution plane: commands, authenticated provider events, accepted facts, projections, and background work. This answers “what happened for this customer, and what are they entitled to now?”

Within those planes, SAPA separates four kinds of information that are often mixed together in billing integrations.

Definitions state what a merchant offers: purchase benefits, billing terms, meter identities, access duration, seat constraints, and fulfillment bindings. They are immutable and identified by a merchant-authored key plus revision.

Changing terms means introducing a new revision. Historical purchases continue to refer to the terms they accepted.

For example, workspace.pro@2 can change price or benefits without changing what a customer who accepted workspace.pro@1 bought. Registration validates the definition and its installed capability references; it does not create a checkout or contact a provider.

A command asks SAPA to perform work, such as creating a checkout or recording usage. Successful processing records authoritative facts. Idempotency keys, provider event identities, and guarded lifecycle transitions prevent a retry or stale writer from inventing a second outcome.

A command is intent, not proof. createCheckout means “attempt to create this checkout at most once.” It does not mean payment succeeded. A raw payment notification becomes settlement evidence only after the selected provider adapter authenticates it and SAPA accepts the resulting lifecycle transition.

Current access, balances, subscription status, seat occupancy, and operational views are derived from accepted facts. These views make reads practical without turning mutable rows into the historical source of truth.

This distinction matters during recovery. SAPA can rebuild or verify a projection from retained history without recreating a provider checkout, resending an email, generating another license key, or calling an external fulfillment system.

Storage, payment providers, clocks, cryptography, downloads, mail, and merchant policies are capabilities supplied by the host runtime. SAPA’s core behavior depends on contracts rather than selecting infrastructure implicitly.

A capability is not another command. It is an implementation SAPA may use while processing a command: for example, a payment adapter creates hosted checkout, while a download adapter turns an accepted asset authorization into a bounded URL. Effect Layers install these implementations.

flowchart TD
  accTitle: SAPA information flow
  accDescr: Immutable definitions, commands, and host capabilities produce authoritative facts, rebuildable projections, and current access decisions.
  Definitions["Immutable definitions"] --> Acceptance["Validate and accept command"]
  Command["Command"] --> Acceptance
  Capabilities["Host-supplied capabilities"] --> Acceptance
  Acceptance --> Facts["Authoritative accepted facts"]
  Facts --> Projections["Rebuildable projections"]
  Facts --> Decisions["Current access decisions"]

An ebook purchase makes the distinction concrete:

StepKindExampleWhat it proves
RegisterDefinitionebook.effect@1 provides ebook.pdf@1These exact terms are valid and installed
Start checkoutCommandcreateCheckout({ idempotencyKey: "order-42", ... })The merchant asked for one hosted checkout
Provider callCapability useMidtrans returns a checkout URLA provider attempt has an observed result, not a payment
Receive webhookExternal evidenceRaw, signed Midtrans payloadNothing until the provider adapter authenticates it
Accept settlementFactpayment identity, amount, currency, accepted offer revisionThe purchase paid under the retained terms
Grant downloadProjectioneligible customer + exact asset revisionA fast read derived from accepted facts
Authorize URLQuery + capability useauthorizeDownload(...)The authenticated customer may receive a bounded URL now
Send emailDurable workretained email obligationDelivery may retry independently of ownership

The same shape scales to subscriptions, seats, credits, licensing, and metered invoices: definitions describe the promise; authenticated commands and events create facts; projections answer current questions; capabilities perform I/O.

Your application ownsSAPA ownsAdapters own
Authentication, authorization, customers in your product, workspaces, members, routes, UI, and when to invoke a commandOffer validation, immutable accepted terms, idempotency, lifecycle decisions, authoritative histories, benefit provenance, and typed failuresDatabase rows and transactions, provider protocols, signatures, cryptography, signed URLs, email transport, and external-system calls

For example, your application proves that the caller administers workspace-42; SAPA proves that the workspace has an available paid seat; the storage adapter atomically records the assignment.

sequenceDiagram
  accTitle: One paid purchase from definition to product access
  accDescr: The merchant registers exact terms, creates checkout, redirects the buyer, accepts an authenticated provider notification, and later queries or fulfills the resulting benefits.
  participant App as Your application
  participant Sapa as SAPA facade
  participant DB as Storage adapter
  participant Pay as Payment adapter
  participant Worker as Durable worker

  App->>Sapa: registerOffers(exact revisions)
  Sapa->>DB: persist immutable definitions
  App->>Sapa: createCheckout(command + idempotency key)
  Sapa->>DB: retain intent and creation claim
  Sapa->>Pay: create hosted checkout
  Pay-->>Sapa: checkout URL or uncertain outcome
  Sapa->>DB: retain observed result
  Sapa-->>App: pending checkout URL
  Note over App,Pay: Browser redirect is not settlement evidence
  Pay-->>App: raw signed webhook
  App->>Sapa: processPaymentNotification(raw payload)
  Sapa->>Pay: authenticate and normalize
  Sapa->>DB: atomically append payment and benefits
  DB-->>Sapa: accepted or replayed result
  Sapa-->>App: settled result
  Worker->>DB: lease durable owed work
  Worker->>Worker: call email, download, or fulfillment adapter
  App->>Sapa: checkAccess / authorize / reserve / assign
  Sapa->>DB: read verified projection and history
  Sapa-->>App: current decision

Checkout provider I/O is deliberately surrounded by durable state: intent is retained before the call, and its observed result is retained afterward. If the network outcome is unknown, reconciliation uses the same checkout identity; it does not blindly create another provider checkout.

  1. What promise is sold? Model entitlements, downloads, seats, credits, licensing, or external fulfillment in a purchase definition.
  2. How is it billed? Select one-time, subscription, per-seat, or metered terms without mixing currency and usage units.
  3. Who receives it? Decide whether the paying customer or a merchant-owned subject such as a workspace is the beneficiary.
  4. What proves activation? Usually authenticated settlement; postpaid enrollment is a separate explicit merchant decision.
  5. What can happen asynchronously? Email and external fulfillment become durable work rather than effects inside the payment transaction.
  6. What must a current read prove? Use access, seat, credit, download, license, or invoice queries instead of trusting cached checkout state.
  • Checkout is not payment. A URL or success redirect grants nothing.
  • An offer is not mutable product state. New terms require a new revision.
  • A projection is not the authority. It is a serving view backed by history.
  • A seat is not a user account. SAPA accounts for paid occupancy; your host owns members and permissions.
  • Credits are not money. They are exact units with their own lifecycle.
  • A capability is not globally available. It must be installed in the request or job runtime that executes the operation.
  • Settlement is not delivery. External work may remain pending or retry after ownership has been accepted.

Decode external input once at the edge. Inside the application, pass validated branded values and checked definitions rather than raw strings, numbers, or provider payloads.

Public operations already decode unknown input and return sanitized typed errors. Keep the raw provider body only long enough to pass it to processPaymentNotification; do not parse, transform, or log it first. See the architecture for the runtime boundaries and the recipes for complete product flows.