S
supero.docs
Documentation/Application Architectures/Online Storefront
Solution Architecture

Online Storefront

One seller, one catalog, many buyers. Product types that extend product:base_product, a cart that converts into an order, and a payment record that tracks the money — assembled from platform base types rather than written from scratch.

A storefront assembled from the product, cart, order and payment base types
Publish this elsewhere:

The Project

A storefront sells goods you own, from a catalog you control, to buyers who mostly arrive without an account. One seller, many buyers. The distinguishing feature is not the catalog — plenty of applications list things — it is that a browsing session has to turn into a durable, auditable record of money owed and goods promised, and that two different audiences work against the same data: buyers on a public surface, and staff in an admin panel afterwards.
Reach for this shape when your requirements include most of the following. If more than one party lists inventory and takes the proceeds, you want the multi-vendor marketplace shape instead, which splits the customer type in two.
  • •A catalog with prices, a currency, and a visible/hidden distinction.
  • •A cart that survives navigation and belongs to a specific buyer.
  • •A checkout step that produces an order record you can still read a year later.
  • •A payment record whose lifecycle is separate from the order lifecycle, because refunds outlive fulfilment.
  • •Customer-written reviews that a staff member moderates before they appear.
  • •Staff who work orders after the fact: confirm, fulfil, cancel, refund.

ℹ️ The exemplar behind this page

Everything described here is taken from a generated application in the test corpus: bundle_m70-bookhaven, an online bookstore in the namespace chapter_verse. It carries 29 schemas and imports ten services. Where this page says the platform does something, it is because that application does it; where it says you have to write something, it is because that application had to.

Architecture & Design

The whole design turns on one line. A type earns its behaviour by extending a platform base type — there is no registration step and no service configuration that names your types. The extends line is the mechanism; everything below follows from it.
The bookstore defines eight object types of its own, and each one earns its behaviour by extending a platform base type. Nothing else opts a schema into a service — there is no registration step and no service configuration that names your types. The extends line is the whole mechanism.
Here is the catalog type as it goes over the wire. Note that extends sits inside schema_content, not at the wrapper level, and that the field list is called attributes with entries of the form name, type, mandatory — not JSON Schema properties and required.
json
{
  "name": "book",
  "namespace": "chapter_verse",
  "schema_type": "object",
  "schema_content": {
    "name": "book",
    "parent_type": "tenant",
    "extends": "product:base_product",
    "attributes": [
      {"name": "title",          "type": "string",       "mandatory": true},
      {"name": "author",         "type": "string",       "mandatory": true},
      {"name": "isbn",           "type": "string",       "mandatory": true},
      {"name": "category",       "type": "BookCategory", "mandatory": true},
      {"name": "cover_image",    "type": "Image"},
      {"name": "publisher",      "type": "string"},
      {"name": "page_count",     "type": "integer"},
      {"name": "stock_quantity", "type": "integer"},
      {"name": "average_rating", "type": "float"},
      {"name": "is_visible",     "type": "boolean"}
    ]
  }
}
Ten declared fields. What the book type actually has is those ten plus everything base_product contributes: status, price, currency, sku, metadata, primary_image, thumbnail, image_list, and the display_name, description and summary that the schema-driven UI renders. The full table of types follows.
Type in chapter_verseExtendsWhat that inheritance buys
bookproduct:base_productprice, currency, sku, and a status of active, inactive or discontinued
store_cartcart:base_cartsubtotal, item_count, expires_at, and a status of open, checked_out, abandoned or expired
store_cart_itemcart:base_cart_itemline-item shape; parented to store_cart rather than to the tenant
store_orderorder:base_ordersubtotal, tax_amount, shipping_amount, discount_amount, total_amount, source_cart_uuid, and a status of pending, confirmed, fulfilled, cancelled or refunded
store_order_itemorder:base_order_itemline-item shape; parented to store_order
store_paymentpayment:base_paymentcaptured_amount, refunded_amount, external_charge_id, and a status of pending, authorized, captured, refunded, partially_refunded, voided or failed
book_reviewfeedback:base_feedbackrating, verified_purchase, helpful_count, and a status of draft, published, hidden, flagged or deleted
customercustomer:base_customerthe buyer profile, parented to a user account rather than to the tenant
Two more schemas are plain value lists with no base type at all — book_category and delivery_method — and they are what the BookCategory and DeliveryMethod field types above resolve to. Records point at each other through named references carrying a role: book_review references Customer as actor and Book as primary_item, store_order references Customer as actor. Those roles are how the services know which record is the buyer and which is the thing bought.

💡 The rest of the 29

Beyond the eight types above, the bundle carries schemas it did not write: the email service brings campaign, message, sender, template and suppression; Stripe Checkout brings its own session and payment_link records; and the workflow engine brings workflow_definition, workflow_instance and workflow_step_log. Importing a service brings its schemas with it. See Schema Design for the field types and reference syntax.

The End-to-End Flow

Every step below except the first and last is a call to one endpoint. There is no cart API and no order API — POST /api/v1/services/execute takes a service_id, an operation, and an input that names the extending schema. The service reads the state machine attached to its base type and either performs the transition or refuses it.
  1. 1

    Browse

    The public catalog reads book records through the ordinary CRUD surface. Visibility is a field on the record — is_visible on the book, plus the inherited status — so hiding a title is a data change, not a deployment.

  2. 2

    Add to cart

    cart / add_to_cart. The first call materialises a store_cart for the buyer and creates their customer profile if it does not exist yet. That customer-profile create is subject to your access policy, so a self-signup buyer needs to be permitted to have one made for them — this is the single most common reason a checkout fails before Stripe is ever reached.

  3. 3

    Recompute

    cart / recompute_totals rolls the line items back up into subtotal and item_count on the cart header. Quantity edits and removals go through the same service rather than a direct write.

  4. 4

    Checkout

    cart / checkout_cart converts the cart into a store_order and copies the store_cart_item rows into store_order_item rows. The cart moves to checked_out; the order arrives at pending with source_cart_uuid pointing back at where it came from.

  5. 5

    Take payment

    stripe_checkout / create_checkout_session hands the buyer to Stripe and records a checkout session. The payment service then tracks the money in its own vocabulary — authorize_payment, then capture_payment, and refund_payment later if it comes to that.

  6. 6

    Confirm and fulfil

    order / place_order and order / fulfill_order move the order along its own state machine, independently of the payment. cancel_order exists for the other outcome. Each transition stamps its own timestamp — confirmed_at, fulfilled_at, cancelled_at — so the history is on the record, not in a log you have to join against.

  7. 7

    Review

    The buyer writes a book_review. It starts as draft and a staff member publishes it, because base_feedback has a real status rather than a boolean.

ℹ️ Operations are guarded, not advisory

Sixteen services ship state machines, and an illegal transition is refused with HTTP 409 and INVALID_STATE_TRANSITION rather than silently applied. Capturing a payment that was never authorized, or fulfilling an order that was already cancelled, fails loudly. Repeating an operation that already succeeded is idempotent — the original timestamp is preserved rather than restamped. Transactional Services covers the wire format.

What You Get Without Building It

The bookstore bundle imports ten services: cart, order, payment, product, customer, feedback, email, amazon_s3, stripe_checkout and workflows. Between them they account for most of what a storefront would otherwise be.
  • •The cart-to-order conversion, including line-item copying and the totals rollup.
  • •Five state machines — product, cart, order, payment, feedback — with idempotent operations and 409 on an illegal transition.
  • •Stripe Checkout as a configured integration rather than an SDK you embed: two keys, stripe_api_key and stripe_webhook_secret, held in the keystore and masked on read.
  • •Transactional and marketing email through SendGrid, with campaign, template, sender, message and suppression records already modelled.
  • •File storage on your own S3 bucket for cover art and review images.
  • •Two audiences on one dataset: a public landing page with hero, catalog, product-detail, cart and order-detail blocks, and an admin panel generated per type from the base UI schemas.
  • •Metric cards and charts declared as records — the bookstore ships total revenue, active books, pending orders, a revenue trend and orders by status.
  • •Multi-tenancy, the platform roles, and record-level ownership, without a tenant_id column anywhere in your model.
None of that is a template you copied and now maintain. The services are the platform's; upgrades to them are not merges into your code.

How to Implement in Supero

Two kinds of work. First you model the shape above and point each type at its base type; then you write the commercial logic the base types deliberately leave to you.

Model it

  1. 1

    Build the schema

    Describe the app in App Studio — it generates the schemas and a reviewable UI — or author it over MCP from your editor. Aim at the object model above: a type opts into a service by extending its base type. Schemas are live on the CRUD API the moment they upload — there is no separate build step to run.

  2. 2

    Configure the services

    Imported services arrive uncredentialed. Put each provider’s keys in the keystore — here that is Stripe for checkout, plus email and SMS for order and shipping notifications — see Email & Notifications. Until a service is credentialed, its calls fail with a clear error rather than silently doing nothing.

What you write yourself

This is the part worth reading twice, because it is where a storefront demo diverges from a storefront business. The base types give you fields and transitions. They do not give you commercial policy, and several fields that look like enforcement are just fields.

⚠️ Stock is a number, not a lock

stock_quantity on the book type is a plain integer. Nothing decrements it when a cart is checked out, and nothing refuses a checkout when it reaches zero. The platform does not take a lock on inventory. If overselling matters to you, the check and the decrement are yours to write — as a workflow bound to the checkout event, or in the surface that calls add_to_cart. The same is true of every capacity-shaped field on every base type.
The other decisions, in rough order of how soon they will bite:
DecisionWhy the platform does not make it for you
Tax and shippingbase_order carries tax_amount and shipping_amount as fields. The rates, the jurisdiction rules and the carrier quote are yours.
Discounts and promotionsdiscount_amount is a field. There is no coupon type, no eligibility rule and no stacking policy in the base types.
average_rating on a bookIt is a float that nothing computes. Aggregate it on review publication with a workflow, or compute it at read time.
Review moderationbase_feedback gives you the draft/published/hidden/flagged states. Who may move a review between them, and on what evidence, is an access policy you write.
Buyer permissions at checkoutadd_to_cart creates a customer profile on the buyer's behalf. A self-signup tenant_user whose policy default is no access will be refused, and the buyer sees a permission error rather than a Stripe page. Decide the policy before you demo it.
Refund policyrefund_payment is an operation, not a rule. Windows, partial refunds and restocking are yours.
Order-to-payment couplingThe two state machines are deliberately independent. Whether a fulfilled order requires a captured payment is your invariant to enforce, most naturally as a workflow.
A workflow is a good home for most of these. It is a record of type wf:workflow_definition with steps of type service_call, crud_operation, condition, parallel, transform or loop, and it can be bound to a CRUD event so that creating an order runs it. See the Workflow Engine Reference for the full step and error contract. Errors are handled per step — stop, continue or retry, with a retry count and a timeout — and every run leaves a workflow_instance plus a workflow_step_log entry per step. See Workflows & Automation.

Deployment

The path from the modelled shape to a running storefront at a public URL, end to end:
  1. 1

    Preview while you iterate

    A preview deploy answers at https://preview-{domain}-{project}.supero.live for a short lease while you build and test. Publish Your App covers how preview and live differ.

  2. 2

    Go Live

    Go Live publishes to a permanent, Supero-hosted URL — https://live-{domain}-{project}.supero.live — with no cloud account to connect and nothing to provision. It is asynchronous: you get a 202 and a poll URL, then the live address. The full contract is in Publish Your App.

  3. 3

    Watch the logs

    When a deploy fails or the live app misbehaves, the **Cloud Deploy Logs** page in the dashboard shows the published service’s container logs — pick the Live lane to read the published app rather than the preview build (up to ~24 hours back, ~500 lines per request). For platform activity — requests, auth, errors across your domain — GET /api/v1/logs is a filtered read, and a single request traces across services at /api/v1/logs/trace/{correlation_id} when you set your own X-Correlation-ID. See Operations & Observability.

Final Thoughts

This shape fits a single seller with a catalog and a checkout. Two boundaries tell you when to reach elsewhere: if more than one party lists inventory and takes the proceeds, the multi-vendor marketplace shape splits the customer type in two; and if the revenue is recurring, that is a different object model with a renewal loop you define — this shape does not sell subscriptions.
If you take one thing into the build, take this: decide the buyer's access policy before you demo. add_to_cart creates a customer profile on the buyer's behalf, and a self-signup buyer whose policy default is no access is refused before Stripe is ever reached — the most common first failure of a storefront that otherwise works.
And what you walk away with is yours. The five state machines, the cart-to-order conversion, Stripe and SendGrid as configured integrations — those stay the platform's, and upgrades to them are never merges into your code. What you own is the schema, the access policy, and the commercial logic above it — in a repository and a database you keep.