Skip to content

Demo

A recorded session, not a live one.

The storefront engine answers inside a signed Shopify request, so what this page can hand you is the recording of 13 exchanges with that engine, taken straight from the repository. All 13 are replayed against the code as it stands today and compared byte for byte, which is what keeps the transcript below tied to the engine it describes.

Replayed on every build Request and payload, side by side Straight from the repository

Four conditions the engine checks before it answers.

A chat box on this page would have to satisfy all four, and each of them is load-bearing. This is the order the code checks them in, and the answer each one gives when its condition is unmet.

  1. The request carries Shopify’s signature

    The store comes out of the signature on a Shopify app proxy request, and that signature is the single source for whose catalogue the engine may open. An unsigned request is answered 401 before a single line of the conversation code runs; the reason goes to our log and the caller gets the code alone, so an attacker learns that the request failed and stops there.

  2. The store resolves to an installed tenant

    A signed request is matched to a store record with a usable sector package; anything else is answered 403. The code holds that line even where the package is missing, because a default rule set would answer an unplaced store with the wrong rules, and wrong rules are as expensive as a wrong catalogue.

  3. The maintenance gate is open

    An open maintenance window — for that store, or for everyone — is answered 503 with a retry header and the notice text, ahead of any model call and any catalogue read. Where the maintenance record itself is unreadable the storefront stays open, and the two data-deletion endpoints pass this gate regardless: a shopper’s right to be deleted outlasts a maintenance window.

  4. The store is on a plan, or inside its trial

    Installed, with a subscription and a trial both absent, is answered 403 — and that answer is the whole of it: the request stops at the gate, ahead of the model, ahead of the catalogue read, ahead of the counter. Every plan is a paid plan, so the trial is the door that opens first.

Interface illustration, drawn from this page’s own four conditions — they are checked in the order shown, and each one carries the answer it gives when its condition is unmet, so a check is reached only once the one before it has passed. The chevrons mark a pass, the downward arrows mark the refusal, and the dashed pair below is the two exceptions this page names on the third check. Every number in the drawing is a status code quoted from the four cards above.

This codebase carries a second, public demo back end, written for the site that is live today, with its own spend ceiling, session cap and per-address rate limit. Wiring it to this page is a decision someone has to take: its bot check is served from another domain, and this directory serves every asset from its own. Until that decision is taken, the recording is what the page shows, and the page says so.

Thirteen exchanges, grouped by what came back.

Both halves are files in the repository. What was sent comes from the scenario list; what came back comes from the recorded copy, captured once against a real worker with a real database and a real signature, before the channel layer under it was rewritten. Every line below comes out of those two files.

It answered 3

  1. 200 · json

    {"mesaj":"which shampoo for dry hair?"}

    For dry hair, a gentle moisturizing shampoo is a good place to start.

    Tool list empty. Product block empty. Handed over: no. The reply arrives with a session id the server minted on its own.

  2. 200 · stream

    accept: text/event-stream, same message

    Three events, in this order: basladi carrying the session id, metin carrying the same sentence, bitti carrying a timestamp.

    Same answer, streamed. The session id lands before the first word, so the widget can start recording the conversation before there is anything to record. Buffering is switched off at the header level.

  3. 200 · json

    Same message, session id omitted

    Same sentence, plus a session id the server generated.

    The id is minted on the server, so a shopper reaches an answer as they are. In the recording it is the one field that had to be masked, because it is random by design.

It handed over 3

  1. 200 · json

    Same message, on a store whose conversation cap was full

    I’ve passed your message to our team, and a team member will follow up with you as soon as they can. If it’s urgent, you can also email us at {eposta}.

    Marked as handed over in the payload, so the widget stops behaving like a chat. This sentence is composed entirely in the worker: a fixed string, written out in every language the assistant answers in. The address in it is the store’s own support address, filled in from that store’s settings — the recording printed a real inbox there, so the page shows the placeholder the code uses and leaves that inbox alone.

  2. 200 · stream

    Same, over the stream

    Four events: basladi, then devir carrying {"hedef":"eposta"}, then metin, then bitti.

    The handoff event arrives before the sentence does. The widget knows it is a handoff and knows which route it is going to, while the text is still being written — the route travels in the event, so the sentence is free to arrive at its own pace.

  3. 200 · json

    {"mesaj":"thanks!"}, after a teammate had taken the conversation over

    Empty. The reply text is an empty string, with the conversation flagged as in human hands.

    The engine skips the model entirely. The shopper’s message is stored and forwarded to the console inbox for the person already in the conversation, and the assistant stays silent over the top of them. This is the one exchange in the recording where the engine is asked a question and answers with an empty string on purpose.

It broke, and said so 2

  1. 200 · json

    Same message, with the model provider returning an error

    Something went wrong on my side just now. Mind trying again in a moment? If it’s urgent, email us at {eposta}.

    Note the status: 200, where a 500 would be the obvious choice. The shopper gets a sentence and the widget stays whole; the payload carries an error flag, so the widget can tell an apology from an answer. Again the address is the store’s own.

  2. 200 · stream

    Same, over the stream

    The metin event carries the same apology and the same error flag, then bitti closes the stream cleanly.

    A stream that has already started is committed to its status code, so the flag travels inside the event. The stream is then closed properly: a dropped stream reads as a network fault to the widget, which would retry a request bound to fail again.

It refused the request 5

  1. 405

    GET instead of POST

    POST bekleniyor

    A conversation is a thing you send, so the route accepts POST alone.

  2. 400

    {bozuk — a body that is not valid JSON

    geçersiz gövde

    Refused at the parse, ahead of any interpretation of what was meant.

  3. 400

    {"mesaj":"   "} — whitespace only

    mesaj boş

    Trimmed first, then judged: three spaces leave an empty message.

  4. 400

    {"mesaj":42} — a number where a message goes

    mesaj boş

    Same refusal as the empty one, and that is the point: the type is checked, so a 42 stays a number and stops here — a coercion would have sent the string 42 to a model.

  5. 413

    A message of 2,001 characters

    mesaj çok uzun

    The cap is 2,000 characters and it is enforced ahead of anything billable. A pasted wall of text is the cheapest way to drive up someone else’s model bill, so it stops at the length check.

All five of those bodies are in Turkish, and they are read by developers rather than shoppers: they are the engine talking to the storefront widget, which carries its own wording in every language it ships with. These strings exist so that a log reader can tell the five refusals apart. The five also share a property worth more than their wording — all of them stop ahead of the model, so a malformed request is free.

What the recording pins down, and where the rest is shown.

A recording proves whatever it happens to contain, and this one was made to pin down an interface. Five things it fixes in place, each with the place in the product where you read the other half of it.

The plumbing, rather than the sentence

In the recording the model provider is replaced by a stub that returns one fixed sentence, on purpose: that is what makes the same request produce the same bytes on every build. What the model itself would write is a thing you read against your own catalogue, in step three of setup.

The interface, rather than the tool calls

All thirteen exchanges carry an empty tool list, because a stub that returns plain text answers on its own. Every tool that runs in a real turn — a stock read, a price, a policy, an order — appears in the console inbox with its result and its timing.

The payload shape, rather than a product card

The product block came back empty in all thirteen, so the transcript shows the field and leaves it at that. The sample conversation on the home page does carry cards, and labels them as examples where they appear.

One store’s settings, rather than its catalogue

Three settings were resolved to make the recording: the store’s language, its support address, its conversation cap. Which store it was stays out of the transcript, and so does everything in its catalogue.

The bytes, rather than the clock

The recording carries exactly one timestamp and the comparison masks it, because a recording that changed on every run could prove only that it had changed. Speed is a thing you measure against your own store and your own traffic, so it belongs to the console.

What the recording does prove is narrow and worth having: every status code, every header and every byte of every body above still comes back from the code as it is written today. The comparison runs with the network cut off inside the process, so it stands on the code alone; a scenario that reaches for the internet fails on the spot.

Where you read the rest: your own console.

The tool calls, the sentences the checks held back and the quality of an answer are all things you judge against your own catalogue. Two places in the product put them in front of you, both of them behind the install.

Every turn, in the inbox

  • The shopper’s question and the answer, as they were shown.
  • Each tool that ran between them, by name, with its result and how long it took.
  • Any sentence the checks held back, listed against the turn it came from.
  • Whether the conversation was handed over, and where it went.

Before you go live, in setup

  • The trial step of setup asks your own store a question with every side effect switched off.
  • It lists what it would have done live — the return it would have opened, the note it would have written.
  • The isolation is structural: everything with a side effect travels through one interface, and the trial hands that interface a stand-in.
  • Where a turn would have left your store untouched, it says that too.