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:
| Moment | Your code does | SAPA does |
|---|---|---|
| Build or configuration | Defines and parses versioned offers | Validates schemas and cross-benefit invariants without I/O |
| Startup or administrative change | Installs capabilities and registers offer revisions | Checks exact references and persists immutable definitions |
| Request or webhook | Authenticates the caller, constructs a command, and provides a runtime | Decodes input, coordinates policy, and returns a typed result or failure |
| Background work | Runs bounded workers with concrete adapters | Supplies 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.
Public facade
Section titled “Public facade”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, andrateMeteredLine. These return Effects but need noSapaservice. - Durable operations, such as
registerOffers,createCheckout,processPaymentNotification,reserveCredits, andcheckAccess. These require a composed request-scopedSapaLayer.
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.
Functional core
Section titled “Functional core”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.
Application workflows
Section titled “Application workflows”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 boundary
Section titled “Storage boundary”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.
Adapter boundary
Section titled “Adapter boundary”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:
| Adapter | Owns | Must not decide |
|---|---|---|
| Storage | SQL/driver calls, transactions, row decoding, conflict classification | Whether a customer deserves access |
| Payment | Provider authentication, payload parsing, checkout protocol, provider references | Whether a redirect counts as settlement |
| Crypto | Random key material, hashing, constant-time verification | Whether license issuance is eligible |
| Download | Exact asset mapping and bounded signed URL creation | Which customer owns the asset |
| Template transport and provider delivery result | Whether failed email revokes ownership | |
| External fulfillment | Remote protocol and idempotent delivery call | Whether payment was accepted |
Composition root
Section titled “Composition root”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"]
What the runtime must supply
Section titled “What the runtime must supply”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.
Request-scoped execution
Section titled “Request-scoped execution”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.
Three execution paths
Section titled “Three execution paths”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.
Authoritative state
Section titled “Authoritative state”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.
What is authoritative?
Section titled “What is authoritative?”| Data | Role | Recovery rule |
|---|---|---|
| Offer and purchase snapshots | Exact commercial promise accepted at that time | Immutable; never reinterpret with the latest definition |
| Checkout, payment, subscription, benefit, usage, and lifecycle histories | Source of business truth | Append guarded facts; do not overwrite history |
| Command receipts and provider identities | Idempotency and replay evidence | Exact retries return the retained winner |
| Projections and indexes | Fast current reads | Rebuild or verify from bounded authoritative history |
| Durable obligations | Owed external work | Lease, 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.
Supported deployment boundary
Section titled “Supported deployment boundary”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.