Stitchwork Docs

How Stitchwork works

Stitchwork models a retail business as seven things. Learn these and every screen, every endpoint and every webhook payload becomes predictable.

The data model at a glance

                          ┌─────────────────┐
                          │  organisation   │  one workspace, one brand
                          └────────┬────────┘
             ┌─────────────┬───────┴───────┬──────────────┐
             ▼             ▼               ▼              ▼
        ┌─────────┐   ┌──────────┐   ┌───────────┐   ┌──────────┐
        │ channel │   │ product  │   │ customer  │   │ visitor  │
        │ web     │   │  ├option │   │ (email)   │   │ (cookie) │
        │ store   │   │  └variant│   └─────┬─────┘   └────┬─────┘
        │ kiosk   │   └────┬─────┘         │              │
        └────┬────┘        │               │  customer_id │
             │             │               └──────────────┘
             │             │                      │
             │             │                      ▼
             │             │                ┌──────────┐
             │             └───────────────▶│  event   │  page_view, view_product,
             │                              └──────────┘  add_to_cart, qr_scan, …
             │
             ├──────────────▶ ┌────────┐  draft → sent → accepted → paid
             │                │ quote  │──┐
             │                └────────┘  │ convert
             │                            ▼
             └──────────────▶ ┌────────┐
                              │ order  │  draft → pending → paid → fulfilled
                              └────────┘
Everything hangs off the workspace. A quote needs both a channel and a customer; an order needs a channel and may be guest (no customer).

Organisations

An organisation — called a workspace in the UI — is one brand. It owns everything else, and it is the isolation boundary: a workspace can only ever see its own data.

A user creates a workspace by naming it, and becomes its owner. The workspace carries:

slug
A URL-safe identifier derived from the name. It is what appears in your tracking tag URL (/tag/{slug}.js) and it never changes when you rename the workspace, so external references don't break.
access_state, trial_ends_at
Whether the workspace can be used at all. See how access is enforced.
tracking_public_key, tracking_secret_key
The two API credentials for storefront and server-to-server integration.
ranking_config, attribution_config
JSON tuning blobs for the recommendation engine and the attribution window.
quote_complete_url_template
Optional URL template the public quote page's "buy now" button points at.
stripe_connect_*
The merchant's own Stripe Express account, for taking card payment on quotes.
One workspace per user

A signed-in user resolves to exactly one workspace. Being invited to a second workspace joins you to it as a member; there is no workspace switcher, so people who genuinely work for two brands need two accounts.

Channels

A channel is a surface the brand sells through. Creating a workspace auto-creates one called Website; you add the rest.

FieldValuesWhat it's for
type web, store, marketplace, kiosk, pop_up, wholesale What kind of surface it is. Drives filtering and the store-visit KPI.
ownership owned, franchise, authorised, wholesale, marketplace Whose business it is. Useful when you sell through partners.
status active, paused, closed Deleting a channel closes it rather than removing it, so its quotes and orders keep their reference.
url An https:// URL The channel's front door. Channel-entry QR codes point here, and bare page views can be matched to the channel by URL.
address, country, timezone, currency Free text / ISO codes Physical context for stores. country is a 2-letter code; currency a 3-letter one.

Channels also carry a staff roster — who can work in this store, at what level — and a set of QR codes that tie a physical scan to this channel. See channel roles and QR attribution.

Products, options and variants

Three levels, and the middle one only exists for configurable products.

product
The thing a customer thinks they are buying: "Concerto Acoustic". Carries name, description, brand, category, tags, a base price, a compare-at price, a status (draft / active / discontinued), an optional parent SKU, and — importantly — a web_url.
product option
An axis of choice on a configurable product: wood with values mahogany and walnut, size with O, OO, OOO. Option names are lowercase letters, digits and underscores.
product variant
A specific purchasable combination, with its own SKU, price and status. Variants are what quote lines and order lines actually point at.

Product type decides the shape:

TypeVariantsNotes
simpleExactly one, auto-createdThe single variant inherits the parent SKU, price, currency and status. Every product has at least one variant, so downstream code never has to special-case.
configurableMany, generated from optionsDeclare options, then generate the cartesian product. Combinations that disappear from the options are marked discontinued rather than deleted.
variantManualFor catalogues that import each variant as its own row without a parent.
SKUs are the join key

SKU is how everything else finds a product: imported order lines, tracked events, external quote lines, category re-ranking. A SKU is matched against product SKUs first, then variant SKUs. A variant SKU always rolls its activity up to the parent as well, so you can rank at either grain.

Customers

A customer is a named person, unique per workspace by email. That uniqueness is load-bearing: creating a customer with an existing email updates that customer instead of failing, which is what makes order imports and external quote creation idempotent.

Customers carry name, email, phone, company, free-text notes and an arbitrary attributes JSON blob. Everything richer — what they browsed, what they were quoted, what they bought — hangs off them by reference.

Visitors and identity

A visitor is an anonymous browser, keyed by the value of a first-party __sw_v cookie on your domain. Stitchwork never reads that cookie cross-origin; the tag sends its value in the request body.

A visitor becomes a known person when visitors.customer_id is set — and their entire prior anonymous history becomes attributable at that moment. There are four ways it happens:

  1. QR handoff. The customer scans a QR on the agent's screen, bounces through a join endpoint on your domain, and their cookie is linked to the quote's customer.
  2. External quote. Your backend creates a quote and passes the shopper's visitor_token.
  3. External order. Your checkout books the sale and passes the visitor_token.
  4. In-app quote creation. A quote created with a visitor_token in its payload stitches at the same time it stamps attribution.

A visitor can only ever be linked to one customer. An attempt to re-link a visitor that already belongs to someone else is refused rather than silently reassigned.

Full detail: Identity stitching.

Events

The event stream is what everything personalised is built on. Every row carries a visitor, a timestamp, a URL, and — where they can be resolved — a product, a variant and a channel.

TypeFired byUsed for
page_viewThe tag, automatically (including SPA navigation)Timeline. The only type that falls back to URL→product matching.
view_productYou, via sw('track', …)The popular ranking signal and per-customer view affinity.
add_to_cartYouThe carted ranking signal and per-customer cart affinity.
searchYouTimeline — what the customer was looking for, in their words.
purchaseYouTimeline. Note that revenue comes from orders, not from this event.
qr_scanThe tag, when it sees QR markers on the URLStore-visit KPI and physical-channel attribution.
campaign_touchThe tag, when it sees UTM params or a click idAd attribution. Also written to a dedicated touch table.
click, customYouAnything else. A name you invent lands as custom with the name preserved.
Product attribution needs a SKU

Intent events (view_product, add_to_cart, purchase) tie to a product through props.sku, never through the URL. They fire from carts, quick-adds and carousels whose URL isn't the product's page, so URL is deliberately not trusted for them. Send the SKU.

Quotes

A quote is a proposal: a channel, a customer, some line items, totals, and a public token that lets exactly one customer see it without logging in.

   draft ──send──▶ sent ──accept──▶ accepted ──pay──▶ paid
     │              │                   │
     │              └──decline──▶ declined
     │              │
     └──cancel──────┴──▶ cancelled          (and: expired, when valid_until passes)
Only draft quotes can be edited or have lines changed. Once sent, the quote is a snapshot — line descriptions, SKUs and unit prices are frozen against later catalogue edits.

Two things people get wrong at first:

Full detail: Quotes.

Orders

An order is a sale that happened. Orders are a first-class entity, not a quote subtype, because most merchants have far more orders than quotes and most of those orders never went near a quote.

Three ways an order gets created, all landing in the same table:

created_viaSourceIdempotent on
importCSV upload(organization_id, external_source, external_id)
quote_conversion"Mark as ordered" on an accepted quoteexternal_id = "quote:{id}"
externalPOST /api/v1/orders/external from your backend(organization_id, external_source, external_id)

Order lines link to variants softly, by SKU. A line whose SKU matches nothing still records — it just doesn't feed the bestseller signal, and it shows up in the unmatched-SKU list on the Rankings page so you can fix the catalogue.

Full detail: Orders.

Ranking and personalisation

Every active product carries a score. The score is a weighted blend of signals you control, and a per-customer overlay sits on top of it.

  WHAT YOUR CATALOGUE IS DOING        WHAT THIS PERSON IS DOING
  what's selling, what's being        what they've viewed, carted
  looked at, what's new, what          and been quoted
  you've featured, price and                    │
  sale position                                 │
        │                                       │
        └──────────────┬────────────────────────┘
                       ▼   one dial sets how far it leans personal
                ┌──────────────┐
                │ final order  │──▶ <sw-recs>, your category
                └──────────────┘    pages, the quote builder
One score per product, from signals you weight yourself — and the same score drives your website and your showroom, so the two never disagree about what to show someone.

Full detail: Recommendations & ranking.

Data isolation

Your workspace is isolated at the database, not just by a filter in application code. The database itself refuses to return a row belonging to another workspace, underneath every query we write — so isolation does not depend on each individual query being correct, which is the assumption that fails in most systems that leak.

Access is resolved the same way on every route, and the access gate runs immediately after. No route can serve your data while quietly skipping that check. The public surfaces — the quote page, the tracking tag, the external ingest endpoints — never resolve a workspace from a signed-in user at all, so they are safe by construction rather than by an exception list somebody has to keep up to date.

Why this is worth asking us about

"Multi-tenant" covers a wide range, from a shared column that every query has to remember, to genuine separation. If your IT or security reviewer wants the detail behind this section, ask — we will walk them through it directly rather than publish the internals.

What Stitchwork is not