# Shoebox — how to drive me I am a small system you can break. You describe a system as a Mermaid diagram, you break something in it, you fire one request, and real OpenTelemetry comes out the other side. I am fully synthetic: no database, no cache, no infrastructure. The diagram is the whole state, which is what makes me safe to hand to a stranger. You are reading this because you want to feed me diagrams. Here is everything you need. Nothing here requires an account, a key, or an install. Base URL: https://shoebox.deepcube.ai ## The one rule **Nothing moves until you fire.** I do not run on a timer and I do not run when you paste. You fired one request, you know its path, and you know what you broke. That is the whole lesson, and it is why every endpoint below is explicit. ## Before you paste anything: what leaves your hands My telemetry goes to a **shared, public** backend that other people can read. Service names travel — they are the point of the exercise, they become `service.name` on the spans. **Do not send me anything proprietary.** If the human you are helping described a real internal system, rename the services before you send them to me. "billing-api" and "partner-reconciliation-v2" are equally good at teaching a trace; only one of them tells a stranger what your employer is building. Rename first, then paste. A visitor cannot redirect my telemetry. Where I export is decided by whoever deployed me (`GET /otlp/status` will tell you where), and nothing about a run can change it. ## The endpoints | Verb | Path | Purpose | |---|---|---| | POST | `/shoebox` | mint your shoebox id — do this once | | POST | `/share` | turn a diagram into a link a person can click | | POST | `/topology/parse` | read a diagram back without emitting anything | | POST | `/run?shoeboxId=…` | fire exactly one request through the diagram | | GET | `/otlp/status` | whether telemetry is going anywhere, and where | ### 1. Get a shoebox ``` POST /shoebox → {"shoeboxId":"1676820325a4497796d0293c2078d205"} ``` Do this **once** and keep the id for the whole conversation. It rides OpenTelemetry Baggage onto every span you produce as `shoebox.id`, which is how your runs are told apart from everyone else's in a shared backend. Minting a new one per run throws that away and makes your own traces unfindable. ### 2. Write a diagram Plain Mermaid, read for meaning. The shapes already map onto OpenTelemetry semantic conventions, so there is nothing new to learn. | You write | I read | |---|---| | `api[Orders API]` | a service | | `db[(Postgres)]` | a datastore | | `q[[Job Queue]]` | a queue | | `cache((Redis))` | a cache | | `ext{{Stripe}}` | a third party | | `worker[Worker x5]` | five replicas, load balanced | | `worker[Worker #2]` | one named instance | | `worker[Worker x5 rev2]` | a rollout that replaces every pod: new template hash, new pod names | | `a -->\|broken\| b` | this call always fails | | `a -->\|broken: wrong table\| b` | and this is why | | `a -->\|broken on #3\| b` | only instance 3 fails | | `a -->\|broken on #3,#5: timeout\| b` | those two instances, with a reason | | `q -->\|phantom\| b` | b never runs, so nothing consumes what q published | | `class db broken` | the pod is down: **every** call into it fails, from every caller | Seven things that will bite you if nobody says them: **The entry point is the first pod, in document order, that nothing calls.** Order is meaning, not formatting. Declare the node you want the request to start at first. If every pod is called by something there is no entry point, and the run returns a note saying so instead of spans. **`service.name` comes from the label, not the id, and it is slugified.** `q[[orders.created]]` publishes to a destination called `orders-created`; `api[Orders API]` is `orders-api`. Worth knowing before you go looking for it in a backend. **A queue needs a far side.** Draw `orders --> q[[Job Queue]]` and stop, and the run publishes to a destination nothing receives — the same telemetry a declared phantom produces, span for span. A datastore may be the last thing in a diagram; a queue may not, because the whole reason to draw a queue is what happens on the other side. **The broker comes off the queue label.** `q[[Kafka]]` publishes with `messaging.system = kafka`. A label naming no broker gets `rabbitmq`. **Replicas are load balanced; separate arrows are fan-out.** `q --> worker[Worker x5]` sends one request to *one* worker. Two arrows out of one node call *both*. So "add three more instances of the inventory API" is `inv[Inventory API x3]` — one pod at three replicas — and not three nodes. Three nodes means something different and the trace will show it. **A revision is a redeploy; scaling is not.** Pods are named the way a Kubernetes Deployment names them, `{service}-{templateHash}-{suffix}`, for example `orders-api-9fgkfp96z4-bh4ks`. The template hash is 10 characters and the suffix 5, both from the alphabet Kubernetes uses for generated names, and both derived from SHA-256, so the same diagram and run index name the same pods on every machine. This replaced the old `{service}-{n}` ids, so a backend that saw a diagram before the change sees new pod names for it now. Names are stable within a revision: `x3` to `x5` keeps the first three pods' names and adds two, and going back drops those two again, as a scale-out and scale-in would. A revision is the other case, a rollout that replaces every pod of the service. Add `rev` to the label and the template hash changes, so every pod gets a new name while `service.name` stays the same; other services keep theirs. A node with no marker is revision 0. The marker is lowercase `rev` and a number, after or before the replica count (`x3 rev2` or `rev2 x3`); leading zeros normalize (`rev02` is `rev2`). A label that already ended in ` rev`, such as `a[Payments rev2]`, is read as a marker, so its `service.name` loses that word. Write one marker per node; a second one stays in the name and `notes` says so. The `N` in `xN`, `#N` and `broken on #N` is still a position, not part of any name: `/topology/parse` returns each node's pod names in that order as `instanceIds`. Within the first 256 positions two pods of one service never share a name: a taken suffix is redrawn, as Kubernetes does. That is also how many names `instanceIds` lists. A position above 256 (from `#300` or a large `xN`) is named the same way but not checked against the others, so it can, very rarely, repeat. For the overlap window of a rollout, draw both revisions as two nodes with the same label, the way `[Worker #1]` and `[Worker #2]` are two pods of one service: ```mermaid flowchart LR gw[Gateway] --> old[Orders API x2 rev1] gw --> new[Orders API x2 rev2] ``` Each request reaches both, so pods from two template hashes emit in the same window under one `orders-api`. That is mirrored traffic rather than a load balancer's split, because a run is exactly one request; what it shows honestly is the thing a backend has to get right, two pod sets of one service live at once. To test that a tool keeps one node per service through all of it, fire the same diagram edited in steps: `api[Orders API x2 rev1]`, then `x4 rev1`, then `x2 rev1` (scaling, names kept), then the two-node overlap above, then `api[Orders API x2 rev2]` alone (every old pod gone). **A cycle is fine, and it does not mean what you might fear.** A pub/sub topic drawn with arrows going both ways — `accounting --> topic` *and* `topic --> accounting` — means the request can arrive back where it started. That is an ordinary architecture and I run it. A request never visits the same pod twice on one causal path, the same way a real request does not, so the walk ends because it runs out of paths and not because it hit a ceiling. Nothing is truncated. `notes` names the edges I declined to take a second time, and `/topology/parse` returns `cyclicPods` naming the pods on a loop. Two things follow. A service reached by two *different* callers still runs twice — those are two paths, not a repeat, so a shared database appearing three times in one trace is correct and not a bug. And if the loop is the thing you actually wanted to model, the two directions through a topic are almost always two different events: `OrderPlaced` in, `AccountingCompleted` out. Draw them as two destinations and the cycle disappears on its own, which is both more faithful and more interesting to look at. **Broken and phantom are not the same thing, and the difference is the lesson.** `broken` leaves a span with an error on it, which is evidence. `phantom` leaves nothing at all: no span from that service, no span naming it, nothing downstream of it, and every span in the trace is green. It is a dead consumer — drawn, believed in, never run. Harder to spot, which is why it is worth teaching separately. I am deliberately forgiving. A line I do not understand becomes a note and the run continues. `flowchart`/`graph` headers, `classDef` lines and `%%` comments are skipped. ### Arrows: every form I read All of these are one call, and the label rules above apply to each: ``` a --> b a -->|broken: timeout| b a --- b a -.-> b a -. manages .-> b a ==> b a == sends ==> b a -- calls --> b a --> b --> c (a chain is every edge in it, not one) ``` Two of them involve a judgement, and I tell you when I make one: **`a --- b` has no arrowhead**, so it does not say who calls whom. I read it left to right and put a note in `notes` saying I did. Half the time that is wrong — a service calls its key vault, the key vault does not call it — so draw the arrow if you know the direction, and read the note if you did not. **An edge pointing at a `subgraph`** points at a box rather than at anything that can serve a request, so the box becomes a service of its own and I say so in `notes`. Point it at a node inside the group instead. ### What I still do not model `subgraph` / `end` / `direction` / `style` / `linkStyle` / `click` are for a renderer. They are skipped in silence — not reported as unreadable, because they are perfectly readable, they just have no counterpart in a trace. The nodes declared inside a subgraph are read normally. One note tells you grouping was dropped. Anything else I genuinely cannot read becomes `line not understood, ignored: …` in `notes`, and the run continues. **A line is all or nothing**: if half of it parses I refuse the whole line rather than admit half an edge, because a partly-read line is how a diagram silently becomes a different diagram. That last sentence used to be much longer. A real Azure reference architecture, pasted unedited, came through as 6 of its 11 edges with 18 lines ignored, because chains, dotted arrows and undirected links were all dropped and nothing errored. It now reads 12 of 12. If you are working against an older deployment and see a wall of "line not understood", that is why — flatten the chains and use `-->`. ### Still worth doing: parse before you fire `POST /topology/parse` is free and emits nothing. Check three things in the result: - **`notes` is empty.** Anything in there is something I did not read the way you wrote it. - **`entry` is the node you meant.** It is the first pod nothing calls, in document order — inside a pile of declarations that is rarely your front door. Declare the entry node first. - **`pods` count and `serviceName` values** match what you intended, since the label becomes the service name and gets slugified. - **`cyclicPods` is empty, or you meant it.** Non-empty names the pods a request can return to. The run is still safe and still complete — but a cycle is very often a misread picture rather than a drawn one, so check it is really there before firing. A stock Azure pub/sub diagram read as bidirectional at the topic is the usual way this happens: the topic fans out one way, and only the publisher publishes. And reduce before you send. I am a shoebox, not a datacentre: a 40-node reference architecture parses now, but the lesson is one request's journey. Take the path that matters — five to ten nodes, the hop you want to break — and draw that. If a human hands you an enterprise diagram, say which slice you are modelling rather than silently firing all of it. ### 3. Read the diagram back before you fire (optional, free) ``` POST /topology/parse {"diagram":"flowchart TD\n api[Orders API] --> db[(Postgres)]"} → {"pods":[{"id":"api","label":"Orders API","serviceName":"orders-api", "kind":"service","replicas":1,"pinnedInstance":null, "revision":null,"instanceIds":["orders-api-zd6rv46kn9-kw2xk"]}, …], "calls":[{"from":"api","to":"db","broken":false,"brokenInstances":[],"reason":null}], "entry":"api", "notes":[]} ``` Emits nothing. Use it when you are unsure whether I understood a shape, or to show a human what I read before anything is fired. Check `entry` is the node you meant and `notes` is empty. ### 4. Fire ``` POST /run?shoeboxId=1676820325a4497796d0293c2078d205 {"diagram":"flowchart TD\n gw[Gateway] -->|broken: timeout| orders[Order Service]", "runIndex":1} ``` `runIndex` is the run number, and it is **not decoration**. Replica selection is deterministic round robin on it — run 1 hits instance 1, run 2 hits instance 2, and so on — never random, so a link is a runnable repro rather than a coin flip. That means: to see what `broken on #3` of five actually does, fire five times with `runIndex` 1..5 and watch it fail once. Sending `1` every time shows you instance 1 every time. A missing or non-positive `runIndex` is treated as 1. ### 5. Read what came back ``` {"runIndex":1, "traceId":"dd32ec7f00c7f795e4dedf9483ccc6a4", "servedBy":["gateway-g9v5z56zwn-d92zl","order-service-sgxxw65zhj-k6kb2", …], "spanCount":17, "failedSpanCount":1, "notes":[], "hops":[{"from":"gw","to":"orders","failed":true,"ms":2}, …], "notTaken":[]} ``` - `traceId` — hand this to the human, or to whatever backend you can query. It is how they find this exact run. - `hops` — every edge the request crossed, in the order it crossed it. This is the authoritative path: I chose which replica served and which calls failed, so anything you derived from the diagram yourself could describe a run that did not happen. - `notTaken` — edges that are in the diagram and were crossed **nowhere** in this run, with a reason, as `{from, to, reason}` on pod ids so a renderer can key onto the nodes it already drew. Usually empty, including for most diagrams with a cycle in them: declining is per path, so an arrow refused on one path is normally crossed on another. Non-empty means part of what somebody drew genuinely did not run — say so. - `servedBy` — which instance served each step. - `notes` — things only a run can tell you, including unconsumed queues and phantoms. **Read these out.** They are usually the most useful sentence in the response. - `spanCount` / `failedSpanCount` — a zero `spanCount` with sensible-looking hops means something is wrong on my side, not yours. ## Handing a diagram to a human A link carries the whole diagram **and the shoebox it ran in**, so a link is a runnable repro rather than a picture of one: ``` https://shoebox.deepcube.ai/?shoeboxId=#d= ``` Whoever opens it lands in your shoebox instead of minting their own, so their runs carry the same `shoebox.id` as yours and one filter in the backend finds both. Drop the `?shoeboxId=` and they get a fresh shoebox and a different id, which is the thing that makes a shared repro impossible to follow. Raw deflate, then base64url (`+`→`-`, `/`→`_`, no padding), in the URL **fragment** — never the query string. That is a privacy decision: a fragment never leaves the browser, so real service names stay out of server logs, CDN logs and `Referer` headers. Keep it under about 8000 characters or mail clients will truncate it. **You do not have to build that yourself, and you should not try.** Deflate is not something you can do in your head, and a guessed fragment opens on an empty diagram. Ask for the link instead: ``` POST /share {"diagram":"flowchart TD a[Orders API] --> b[(Store)]","shoeboxId":"..."} -> {"url":"https://shoebox.deepcube.ai/?shoeboxId=...#d=...", "tooLongForSomeClients":false, "hint":null} ``` Free, emits nothing, paced with `/topology/parse` rather than with `/run`. Include the `shoeboxId` and whoever opens the link lands in the same shoebox, so their runs and yours carry the same `shoebox.id`; leave it out and they get a fresh one. If `tooLongForSomeClients` comes back true, say so rather than handing over a link that mail clients may truncate. Give the link to the person. It opens their diagram in the editor, rendered and ready to fire, which is a better answer than a code block they have to paste. ## Where the telemetry goes ``` GET /otlp/status → {"configured":true,"endpoint":"https://otlp.deepcube.ai/", …} ``` `configured: false` means runs still execute but go nowhere — say so rather than letting someone hunt for traces that were never exported. `configured: true` does not mean the backend accepted anything: export is best effort and a run will not fail because the far end refused it. Check for the trace id. ## Rules for you specifically 1. **Hold the diagram as state and mutate it.** Keep the current diagram in a variable, a file, or a structured object — anywhere but your own recollection of the conversation. When someone says "break the call from the gateway to the orders API", change that one edge and re-render. Regenerating the whole diagram from memory each turn is how services quietly go missing and how one requested edit turns into three offered variants. 2. **Say what you did, not what you would have done.** If you have not called `/run`, you have not sent anything. Never describe a diagram as sent, fired, applied or live unless you have a `traceId` in your hand. This is the failure mode that gets noticed, and it is the one that costs trust. 3. **One run per instruction.** I am a teaching tool, not a load generator. Fire because a human asked, or because you are deliberately walking `runIndex` through the replicas. Do not loop. See the pacing below — it is enforced, not advisory. 4. **Report my `notes` and my `hops`, not your expectations.** If the run says nothing failed and you expected a failure, the diagram is the thing to look at. 5. **Rename before you paste.** See the top of this file. You are usually the one turning a human's description of a real system into node labels, which makes this your call more than theirs. ## How fast you may ask Enforced, not advisory. One shared instance serves everybody, so the limits exist to keep me up for the next person rather than to be clever at you. | What | Limit | |---|---| | `POST /run` per shoebox | **20 in a row, then 12 a minute** | | `POST /run` per source address | 40 in a row, then 12 a minute, however many shoeboxes you mint | | `POST /shoebox` per source address | 5, then one every 5 minutes | | `POST /topology/parse` per source address | 60 a minute | The burst is there on purpose: replica selection is round robin on `runIndex`, so seeing what `broken on #3` of five actually does takes five runs, and a burst of twenty buys two of those back to back with spare. Spend it on replica walks, not on a loop. Note the asymmetry: running got looser and minting did not. They guard different things. A run costs emission and is now capped at 500 spans, so it can be priced generously; a mint costs nothing and is limited only because it is the way around the run limit. Minting a fresh shoebox to reset the count buys you one more allowance and then stops working: the per-source limit holds the same twelve a minute and counts every shoebox from your address together, and minting is itself limited. Measured, not asserted — five runs, then five more on a fresh shoebox, then 429 either way. Keep one shoebox; you also lose the ability to find your own traces if you scatter them across ids. Over the line, I answer **429** with a `Retry-After` header and a body that repeats the numbers: ``` {"error":"slow down","retryAfterSeconds":60, "limits":{"run":"20 in a row, then 12 a minute, per shoebox", …}, "hint":"…/llms.txt explains the pacing, and /topology/parse is free"} ``` Wait the stated time. Do not retry immediately, do not fan out across addresses, and do not mint a new shoebox to reset the clock — none of it works, and all of it looks exactly like the thing the limit is for. If you need to iterate quickly on a diagram, `/topology/parse` is generous and emits nothing: get the shape right there, then fire once. ## A worked example, end to end ``` POST /shoebox → {"shoeboxId":"1676…d205"} POST /run?shoeboxId=1676…d205 {"diagram":"flowchart TD\n gw[Gateway]\n auth[Auth Service]\n orders[Order Service]\n inv[Inventory API x3]\n q[[orders.created]]\n mailer[Email Worker]\n gw --> auth\n gw -->|broken: gateway to orders failed| orders\n orders --> inv\n orders --> q\n q -->|phantom| mailer\n", "runIndex":1} → traceId, one failed hop (gw→orders), and a note about the phantom consumer. ``` Then say to the human: which hop failed, what the notes said, and the trace id. Not a summary of the diagram they can already see. ## Mistakes I see most - **Nothing to start from.** Every pod is called by something, so there is no entry point. Declare a caller nobody calls. - **A queue with no far side**, producing a phantom nobody meant to draw. - **Three nodes where three replicas were meant.** "Three more instances" is `x3`. - **Same `runIndex` every time**, then confusion about why `broken on #3` never fires. - **A new shoebox per run**, which scatters your traces across ids nobody can filter on. - **Real service names**, pasted because nobody thought to rename them first. --- Shoebox is open source, Apache-2.0: https://github.com/ImmersiveFusion/shoebox Siblings: Snowglobe (the sealed world, pre-made) and sos-beacon (listens to the real world). A snowglobe is sealed; in a shoebox you can open it up.