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
└────────┘
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.
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.
| Field | Values | What 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 — aweb_url. - product option
-
An axis of choice on a configurable product:
woodwith values mahogany and walnut,sizewith 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:
| Type | Variants | Notes |
|---|---|---|
simple | Exactly one, auto-created | The 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. |
configurable | Many, generated from options | Declare options, then generate the cartesian product. Combinations that disappear from the options are marked discontinued rather than deleted. |
variant | Manual | For catalogues that import each variant as its own row without a parent. |
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:
- 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.
- External quote. Your backend creates a quote and passes the shopper's
visitor_token. - External order. Your checkout books the sale and passes the
visitor_token. - In-app quote creation. A quote created with a
visitor_tokenin 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.
| Type | Fired by | Used for |
|---|---|---|
page_view | The tag, automatically (including SPA navigation) | Timeline. The only type that falls back to URL→product matching. |
view_product | You, via sw('track', …) | The popular ranking signal and per-customer view affinity. |
add_to_cart | You | The carted ranking signal and per-customer cart affinity. |
search | You | Timeline — what the customer was looking for, in their words. |
purchase | You | Timeline. Note that revenue comes from orders, not from this event. |
qr_scan | The tag, when it sees QR markers on the URL | Store-visit KPI and physical-channel attribution. |
campaign_touch | The tag, when it sees UTM params or a click id | Ad attribution. Also written to a dedicated touch table. |
click, custom | You | Anything else. A name you invent lands as custom with the name preserved. |
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)
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:
-
Cancel and delete are different actions.
POST /quotes/{id}/cancelis the soft path — the quote stops being actionable but stays on the customer's history and in the totals.DELETE /quotes/{id}removes it for good. - A quote is not an order. An accepted quote is still a proposal; converting it creates a separate order record that links back.
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_via | Source | Idempotent on |
|---|---|---|
import | CSV upload | (organization_id, external_source, external_id) |
quote_conversion | "Mark as ordered" on an accepted quote | external_id = "quote:{id}" |
external | POST /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
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.
"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
- Not a payment processor. Stripe Connect is optional, and when it is on, your own Stripe account is on the receiving end.
- Not a consent management platform. The tag integrates with whatever CMP you already run.
- Not a storefront or CMS. It sits alongside Shopify, Magento, WooCommerce or a bespoke site and connects with a script tag and CSV imports.
- Not a BI warehouse. The event store exists for personalisation and attribution. The dashboard is glance-and-go, not a reporting suite.