Skip to content

Architecture

SAPA separates portable business rules from persistence, providers, and runtime infrastructure. Consumers use one public facade: @byfungsi/sapa. Arrows in the diagram point toward dependencies; concrete adapters satisfy capability contracts without becoming dependencies of the core.

In practical terms, your code interacts with SAPA at four moments:

MomentYour code doesSAPA does
Build or configurationDefines and parses versioned offersValidates schemas and cross-benefit invariants without I/O
Startup or administrative changeInstalls capabilities and registers offer revisionsChecks exact references and persists immutable definitions
Request or webhookAuthenticates the caller, constructs a command, and provides a runtimeDecodes input, coordinates policy, and returns a typed result or failure
Background workRuns bounded workers with concrete adaptersSupplies durable obligations, leases, retry identity, and fenced outcomes
flowchart TB
  accTitle: Portable SAPA package architecture
  accDescr: The public facade depends inward on application workflows, storage contracts, and domain rules while host-selected adapters implement capabilities from the outside.
  Host["Your application or composition root"] --> Facade["@byfungsi/sapa public facade"]
  Facade --> Application["Application workflows and capability contracts"]
  Application --> StorageContract["Storage contracts and atomic operations"]
  Application --> Domain["Domain Schemas and pure transitions"]
  StorageContract --> Domain
  Adapters["Host-selected adapters<br/>database · payments · crypto · email · downloads"] -. "implement" .-> Application
  Adapters -. "implement" .-> StorageContract

Read this diagram from the outside inward: your application chooses adapters and calls the facade; the facade coordinates workflows; workflows depend on domain rules and cohesive storage capabilities. The core never imports your database driver or provider SDK.

The facade owns consumer-facing Schemas, operations, Layers, and sanitized error unions. Import only from @byfungsi/sapa. Repository packages named @sapa/* are private implementation boundaries, even when their declaration types are used internally to preserve type information.

There are two kinds of public facade API:

  • Resource-free definitions and calculations, such as Offer.parse, selectOfferSeats, and rateMeteredLine. These return Effects but need no Sapa service.
  • Durable operations, such as registerOffers, createCheckout, processPaymentNotification, reserveCredits, and checkAccess. These require a composed request-scoped Sapa Layer.

That is why defining an offer works immediately after installing the package, while creating checkout cannot work until storage, provider, clock, identity, and snapshot-protection capabilities exist.

Definition parsing, compatibility checks, seat pricing, fixed-duration calculation, metered-line rating, and subscription quote validation are pure or resource-free Effect programs. They can run before a database or payment provider exists.

Durable operations coordinate domain rules with narrow capabilities. Examples include checkout creation, payment settlement, usage recording, invoice finalization, seat assignment, credit reservation, and license issuance. Expected failures remain typed values.

The workflow owns ordering. For checkout creation, it validates the command, resolves frozen terms, claims one idempotent creation attempt in storage, calls the payment adapter, then persists the observed result. Callers do not receive a generic transaction callback and adapters do not decide commercial policy.

Storage adapters own rows, transactions, conflict detection, and driver errors. They expose cohesive atomic operations rather than open-ended transaction callbacks. Accepted history and immutable snapshots are authoritative; serving projections can be rebuilt from retained facts.

An atomic storage operation can update several business records when they are one decision. Settlement, for example, may append payment history and create access contributions, seat capacity, credit lots, subscription history, and fulfillment obligations together. Either that decision is retained consistently or none of it is. Provider network calls and email delivery are never part of that database transaction.

Concrete payment, storage, cryptography, download, email, and deployment code lives outside the core. Adapters authenticate and normalize provider payloads, classify failures, and return domain values without leaking transport-specific details into the facade.

An adapter owns technology mechanics, not business truth:

AdapterOwnsMust not decide
StorageSQL/driver calls, transactions, row decoding, conflict classificationWhether a customer deserves access
PaymentProvider authentication, payload parsing, checkout protocol, provider referencesWhether a redirect counts as settlement
CryptoRandom key material, hashing, constant-time verificationWhether license issuance is eligible
DownloadExact asset mapping and bounded signed URL creationWhich customer owns the asset
EmailTemplate transport and provider delivery resultWhether failed email revokes ownership
External fulfillmentRemote protocol and idempotent delivery callWhether payment was accepted

Your server, Worker, job, or test composes concrete Layers with Sapa.layer or a configured Sapa constructor. Compose once per runtime boundary. Do not hold database transactions across network calls, and do not share request-specific handles across requests.

flowchart LR
  accTitle: Runtime composition boundary
  accDescr: A merchant server or Worker creates one SAPA runtime from storage, provider, policy, clock, cryptography, email, and download Layers.
  Merchant["Merchant server or Worker"] --> Runtime["One composed SAPA runtime"]
  Runtime --> Sapa["Sapa service"]
  Runtime --> Storage["Storage Layer"]
  Runtime --> Providers["Provider Layers"]
  Runtime --> Policies["Merchant policy Layers"]
  Runtime --> Clock["Clock, crypto, mail, and download Layers"]

The durable facade requires these capability groups:

  • catalog, offer, checkout, customer, payment, access, subscription, seat, metering, invoice, credit, fulfillment, and license storage;
  • checkout provider registry;
  • installation identity, ID generation, clock, and snapshot protection.

Optional offer features add exact installed capabilities:

  • downloads need an asset registry and bounded URL policy;
  • licensing needs cryptography and a credential policy;
  • metered postpaid billing needs meter and merchant access-policy revisions;
  • external fulfillment needs exact binding revisions in the capability manifest.

Sapa.layer uses an empty exact-capability manifest. Use Sapa.makeConfigured when registered offers reference downloads, meters, policies, fulfillment bindings, or other installation-specific behavior. Missing exact revisions fail registration or checkout rather than silently degrading.

The Layer belongs at the transport boundary, not inside domain logic:

import { createCheckout } from "@byfungsi/sapa";
import { Effect } from "effect";
import { makeRequestSapaLayer } from "./runtime.js";
export const handleCheckout = (request: Request, env: Env) =>
Effect.gen(function* () {
const authenticated = yield* authenticateMerchantRequest(request);
const input = yield* decodeCheckoutRequest(request, authenticated);
return yield* createCheckout(input);
}).pipe(Effect.provide(makeRequestSapaLayer(env)));

authenticateMerchantRequest, decodeCheckoutRequest, and makeRequestSapaLayer belong to the host application. On Cloudflare, the Layer must close over the current request’s bindings and D1 handle. A long-lived module-global runtime can accidentally reuse request-bound resources and bypass the intended isolation boundary.

The foreground command, authenticated event, and background worker paths have different responsibilities.

flowchart TB
  accTitle: Foreground, event-driven, and background SAPA paths
  accDescr: Merchant commands return synchronously, provider events establish payment facts, and durable workers perform retryable external delivery outside transactions.

  subgraph Foreground["1. Foreground merchant command"]
    Request["Authenticated request"] --> Decode["Decode public Schema"]
    Decode --> Decide["Validate policy and current history"]
    Decide --> CommitIntent["Atomic intent or lifecycle commit"]
    CommitIntent --> Response["Typed success or expected failure"]
  end

  subgraph ProviderEvent["2. Provider event"]
    Webhook["Bounded raw webhook body"] --> Authenticate["Provider adapter authenticates"]
    Authenticate --> Settlement["Atomic settlement or refund decision"]
    Settlement --> Benefits["History + projections + durable work"]
  end

  subgraph Background["3. Background delivery"]
    Lease["Lease retained obligation"] --> External["Call external adapter with stable identity"]
    External --> Outcome["Append succeeded, retry, failed, or uncertain outcome"]
  end

For checkout creation, provider I/O occurs during the foreground workflow but outside the database transaction and behind an at-most-once creation claim. For email and external fulfillment, I/O occurs later in workers because settlement must not be rolled back merely because a remote delivery system is unavailable.

Accepted history and immutable commercial snapshots are authoritative. Read models and indexes are projections: they make serving practical, but must be verifiable against retained facts and rebuildable without replaying external side effects.

flowchart LR
  accTitle: Authoritative state and projections
  accDescr: Validated commands atomically produce accepted history, immutable snapshots, and durable work while bounded projections remain rebuildable.
  Commands["Validated commands"] --> Commit["Cohesive atomic commit"]
  Commit --> History["Accepted history"]
  Commit --> Snapshots["Immutable terms snapshots"]
  Commit --> Work["Durable owed work"]
  History --> Projection["Serving projections"]
  Snapshots --> Projection
  Projection --> Reads["Bounded reads and access decisions"]
  History -. "verify or rebuild" .-> Projection
  Work --> Adapters["External adapters"]

External network work does not run inside database transactions. Durable workers lease owed work, call an adapter with a stable idempotency identity, and then append the fenced outcome.

DataRoleRecovery rule
Offer and purchase snapshotsExact commercial promise accepted at that timeImmutable; never reinterpret with the latest definition
Checkout, payment, subscription, benefit, usage, and lifecycle historiesSource of business truthAppend guarded facts; do not overwrite history
Command receipts and provider identitiesIdempotency and replay evidenceExact retries return the retained winner
Projections and indexesFast current readsRebuild or verify from bounded authoritative history
Durable obligationsOwed external workLease, execute with stable identity, then append a fenced outcome

This is not event sourcing for its own sake. It protects concrete monetization questions: which terms were accepted, which payment supports this entitlement, whether a refund should remove this particular contribution, and whether a retry may safely perform external work again.

Follow one checkout through the architecture

Section titled “Follow one checkout through the architecture”
flowchart LR
  accTitle: Checkout call path and responsibility handoffs
  accDescr: Host authentication and transport feed the public operation, which validates input, coordinates terms and persistence, performs provider I/O behind a durable claim, and returns a sanitized result.
  HostAuth["Host auth + HTTP boundary"] --> Public["createCheckout(input)"]
  Public --> Schema["Strict public Schema decode"]
  Schema --> Terms["Resolve catalog, inline, offer, renewal, or invoice terms"]
  Terms --> Claim["Storage: retain intent + at-most-once provider claim"]
  Claim --> Provider["Payment adapter: create hosted checkout"]
  Provider --> Observe["Storage: retain URL, failure, or unknown outcome"]
  Observe --> Result["CheckoutCreateResult or typed CheckoutCreateError"]
  Result --> HostResponse["Host maps result to HTTP"]

Expected provider, validation, lookup, idempotency, and lifecycle failures stay in the Effect error channel as public code unions. Defects and transport mapping remain the host’s responsibility. If provider creation is uncertain, recoverCheckout reconciles the retained identity and is forbidden from initiating a previously unattempted checkout.

The framework is portable, but the repository currently contains one complete reference composition: the source-hosted Cloudflare deployment. Version 0.1.0 does not publish the concrete adapters as npm packages and does not expose a complete merchant-facing HTTP commerce API. Installing @byfungsi/sapa therefore provides the portable Effect API, not a turnkey hosted service.

See deployment overview for the support boundary and Cloudflare reference deployment for its concrete resource topology.

See runtime composition for the Layer shape and payment providers for provider invariants. For the same ideas applied to one customer journey, read the mental model, then choose an end-to-end recipe.