Core concepts
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 │ paid / fulfilled / refunded / cancelled
└────────┘
organization_id. 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.
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.
GLOBAL SIGNALS PERSONAL OVERLAY
├ bestseller qty sold, 365d ├ view this customer's view_product events
├ popular view_product, 30d ├ cart this customer's add_to_cart events
├ carted add_to_cart, 30d └ quote products on their prior quotes
├ newness exponential decay
├ featured manual per-product blended by personalisation_strength (0..1)
├ price_target proximity to target ▁▂▃▂▁ │
└ on_sale compare price > price ▼
│ ┌──────────────┐
└─────────────────────────────────▶│ final order │──▶ <sw-recs>, category
└──────────────┘ pages, quote builder
Full detail: Recommendations & ranking.
Data isolation
Three layers, in depth:
-
Every table carries
organization_id, and every model method filters on it explicitly. -
PostgreSQL row-level security is enabled on every tenant table. Each
request sets a session variable identifying the workspace, and the policies reject
rows that don't match. This is defence in depth — it catches a query that forgot its
WHERE, not just a hostile one. - Access resolution goes through one seam. Every route that resolves a workspace does so through the same lookup, and the access gate is called immediately after. A route cannot quietly opt out of the gate while still knowing which tenant it is serving.
That third point is also why the public surfaces are safe: the public quote page, the tracking tag and the external ingest endpoints never resolve a workspace by user at all, so they are carved out by construction rather than by a list someone has to maintain.
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.