diff --git a/workshops/build_workshop/README.md b/workshops/build_workshop/README.md
index ff88d9f..7235a29 100644
--- a/workshops/build_workshop/README.md
+++ b/workshops/build_workshop/README.md
@@ -15,8 +15,8 @@ feature branch, open a PR to protected `dev-build-workshop-v1`, verify
## Architecture
The app edge runs on the participant's laptop. ClickHouse, Postgres, ClickPipes, and
-ClickStack/HyperDX live in ClickHouse Cloud; OpenAI, Langfuse, and optional LibreChat are
-separate hosted services. No database or product UI is deployed locally.
+ClickStack/HyperDX live in ClickHouse Cloud; OpenAI and Langfuse are separate hosted
+services. No database or product UI is deployed locally.
```mermaid
flowchart LR
@@ -40,7 +40,6 @@ flowchart LR
subgraph THIRD["Third-party"]
OAI["OpenAI API
chat completions"]
LF["Langfuse Cloud
chat traces"]
- LC["Hosted LibreChat
optional SRE chat"]
TLC["NYC TLC public dataset
download source only"]
end
@@ -58,7 +57,6 @@ flowchart LR
TLC -.->|url seed, module 01| CH
HDX -->|reads otel db| CH
AG -->|RBAC-governed SQL| CH
- LC -.->|remote MCP| MCP
```
The published diagrams (the ClickHouse Cloud platform stack, the workshop architecture, the
@@ -100,7 +98,7 @@ sequenceDiagram
G->>P: INSERT trips (throttled, TLS)
Note over G,P: first run also creates the table
and publication pub_taxi
P->>CP: WAL changes via publication + slot
- CP->>CH: rows land in realtime_trips (_peerdb_* cols), about 60s
+ CP->>CH: rows land in default.realtime_trips, about 60s
CH->>CH: materialized view fans rows into nyc_tlc_data.taxi_trips
UI->>CH: parameterized SQL / guarded NL-to-SQL / RBAC-governed BI
CH-->>UI: live Ops dashboard + 3M-row Historical seed
diff --git a/workshops/build_workshop/app/OBSERVABILITY.md b/workshops/build_workshop/app/OBSERVABILITY.md
index dfcf758..c7d03e0 100644
--- a/workshops/build_workshop/app/OBSERVABILITY.md
+++ b/workshops/build_workshop/app/OBSERVABILITY.md
@@ -107,12 +107,12 @@ cp .env.workshop.example .env.workshop
# 2) Bring up the stack with the overlay:
docker compose --env-file .env.workshop \
- -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d
+ -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --build
# Optional: also scrape raw container stdout (see VERIFY-LIVE):
docker compose --env-file .env.workshop \
-f docker-compose.workshop.yml -f docker-compose.otel.yml \
- --profile container-logs up -d
+ --profile container-logs up -d --build
```
## Environment variables
diff --git a/workshops/build_workshop/app/README.md b/workshops/build_workshop/app/README.md
index 958d53a..d224276 100644
--- a/workshops/build_workshop/app/README.md
+++ b/workshops/build_workshop/app/README.md
@@ -48,7 +48,7 @@ docker compose --env-file .env.workshop -f docker-compose.workshop.yml up -d
# with the ClickStack observability overlay (module 05 onward):
docker compose --env-file .env.workshop \
- -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d
+ -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --build
```
Frontend: http://localhost:8080 - Backend API docs (FastAPI Swagger):
diff --git a/workshops/build_workshop/app/WORKSHOP_CHANGES.md b/workshops/build_workshop/app/WORKSHOP_CHANGES.md
index 6b4e9a9..3029c24 100644
--- a/workshops/build_workshop/app/WORKSHOP_CHANGES.md
+++ b/workshops/build_workshop/app/WORKSHOP_CHANGES.md
@@ -19,10 +19,9 @@ The workshop intentionally separates application code from managed services.
- remote ClickHouse and ClickStack MCP endpoints
- ClickHouse Agents
- Langfuse Cloud and OpenAI
-- the optional instructor-provided HTTPS LibreChat instance
-No PostgreSQL, ClickHouse, MongoDB, LibreChat, HyperDX, Langfuse, or MCP server is
-started on a learner machine.
+No PostgreSQL, ClickHouse, MongoDB, HyperDX, Langfuse, or MCP server is started on a
+learner machine.
## Runtime sequence
@@ -30,8 +29,8 @@ started on a learner machine.
2. Module 01 creates and seeds the Cloud schema.
3. Module 03 creates managed Postgres, validates it with
`./preflight.sh --require-postgres`, then explicitly enables the `cdc` trip writer.
-4. Module 05 enables Managed ClickStack and starts the stateless collector overlay.
-5. Module 06b uses hosted LibreChat; Module 08 sends chat traces to Langfuse Cloud.
+4. Module 05 starts the stateless collector overlay, then enables Managed ClickStack.
+5. Module 08 sends chat traces to Langfuse Cloud.
`.env.workshop.example` leaves managed Postgres credentials blank until Module 03 and
requires TLS. `preflight.sh` rejects loopback/local database hosts. CI policy checks in
diff --git a/workshops/build_workshop/app/docker-compose.otel.yml b/workshops/build_workshop/app/docker-compose.otel.yml
index 6b524f0..922ed7f 100644
--- a/workshops/build_workshop/app/docker-compose.otel.yml
+++ b/workshops/build_workshop/app/docker-compose.otel.yml
@@ -14,7 +14,7 @@
# "ClickStack observability" section of .env.workshop.example):
#
# docker compose --env-file .env.workshop \
-# -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d
+# -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --build
#
# Add raw container-log scraping (optional, see VERIFY-LIVE notes in OBSERVABILITY.md):
# ... -f docker-compose.otel.yml --profile container-logs up -d
diff --git a/workshops/build_workshop/docs/diagrams/gen_diagrams.py b/workshops/build_workshop/docs/diagrams/gen_diagrams.py
index 4947446..d4317e7 100644
--- a/workshops/build_workshop/docs/diagrams/gen_diagrams.py
+++ b/workshops/build_workshop/docs/diagrams/gen_diagrams.py
@@ -20,9 +20,9 @@
# zone tints (muted, harmonious)
ZONES = {
- "laptop": {"fill": "#1B2531", "stroke": "#3E5A78", "label": "PARTICIPANT LAPTOP · the NYC-taxi app runs here"},
- "cloud": {"fill": "#211F14", "stroke": YELLOW, "label": "CLICKHOUSE CLOUD · your trial org (you create all of this)"},
- "third": {"fill": "#1E1E22", "stroke": "#54545C", "label": "THIRD-PARTY"},
+ "laptop": {"fill": "#1B2531", "stroke": "#3E5A78", "label": "PARTICIPANT LAPTOP · local app + tools"},
+ "cloud": {"fill": "#211F14", "stroke": YELLOW, "label": "CLICKHOUSE CLOUD · your trial org"},
+ "third": {"fill": "#1E1E22", "stroke": "#54545C", "label": "EXTERNAL SERVICES"},
}
@@ -46,21 +46,22 @@ def box(x, y, w, h, title, lines=None, accent=False, rounded=12, fill=BOX_FILL,
cx = x + w / 2
# title
ty = y + (22 if lines else h/2 + 5)
- s += (f'{escape(title)}\n')
if lines:
for i, ln in enumerate(lines):
- s += (f'{escape(ln)}\n')
return s
def zone(x, y, w, h, key):
z = ZONES[key]
+ label_color = YELLOW if key == "cloud" else SUBINK
s = (f'\n')
- s += (f'{escape(z["label"])}\n')
+ s += (f'{escape(z["label"])}\n')
return s
@@ -72,9 +73,9 @@ def edge(x1, y1, x2, y2, label=None, mid=None, dashed=False, label_dx=0, label_d
lx = (x1 + x2) / 2 + label_dx if mid is None else mid[0]
ly = (y1 + y2) / 2 + label_dy if mid is None else mid[1]
w = 7.2 * len(label) + 10
- s += (f'\n')
- s += (f'\n')
+ s += (f'{escape(label)}\n')
return s
@@ -87,9 +88,9 @@ def elbow(pts, label=None, mid=None, dashed=False):
f'marker-end="url(#arrow)"/>\n')
if label and mid:
w = 7.2 * len(label) + 10
- s += (f'\n')
- s += (f'\n')
+ s += (f'{escape(label)}\n')
return s
@@ -106,7 +107,6 @@ def module_flow():
("04 ClickHouse Agents", "10 min", "conversational BI", "over your taxi data"),
("05 ClickStack", "15 min", "OTel overlay: traces", "+ logs in HyperDX"),
("06 AI SRE", "15 min", "agent + ClickStack MCP", "builds dashboard + alert"),
- ("06b Hosted LibreChat", "optional · 15 min", "remote ClickStack + GitHub MCP", "chat-native SRE workflow"),
("07 Test, fail, and fix", "20 min", "inject a fault, diagnose", "with the AI SRE, fix it"),
("08 Chat + Langfuse", "15 min", "in-app AI chat,", "every turn traced"),
("09 Wrap-up", "10 min", "running prototype,", "take it home"),
@@ -122,7 +122,7 @@ def module_flow():
s += (f''
f'ClickHouse BUILD Workshop · Module flow\n')
s += (f''
- f'~2h30 core + optional 06b · only module 07 switches to a fault branch\n')
+ f'~2h30 hands-on · only module 07 switches to a fault branch\n')
top = gy + 52
pos = {}
@@ -167,66 +167,38 @@ def module_flow():
# ===========================================================================
-# ARCHITECTURE (3 zones, aligned rows, top bus for third-party API calls)
+# ARCHITECTURE (three simple groups; the data-flow diagram owns sequencing)
# ===========================================================================
def architecture():
- W, H = 1700, 1000
+ W, H = 1200, 650
s = _svg_header(W, H)
s += (f''
- f'ClickHouse BUILD Workshop · Architecture (target end state)\n')
+ f'ClickHouse BUILD Workshop · Where each component runs\n')
s += (f''
- f'App-side components run locally; stateful data services run in ClickHouse Cloud. '
- f'Dashed lines are control/OAuth; solid lines are data.\n')
+ f'Grouped by location; no data direction is implied. Follow the next diagram for the trip path.\n')
# zones
- s += zone(30, 135, 515, 700, "laptop")
- s += zone(590, 135, 650, 760, "cloud")
- s += zone(1290, 135, 380, 560, "third")
-
- # --- laptop boxes ---
- s += box(60, 190, 455, 62, "frontend", ["React SPA · nginx :8080", "Ops + Historical dashboards, chat"])
- s += box(60, 300, 455, 62, "backend", ["FastAPI :8000", "analytics API + /api/chat (NL-to-SQL)"])
- s += box(60, 410, 455, 56, "otel-collector", ["ClickStack overlay (module 05)"])
- s += box(60, 520, 455, 62, "pg-trip-writer", ["synthetic trips; creates the CDC", "table + publication on first run"])
- s += box(60, 660, 455, 62, "coding agent + clickhousectl", ["Claude Code / Cursor / Codex", "+ ClickHouse skills + docs llms.txt"])
-
- # --- cloud boxes ---
- s += box(620, 185, 590, 96, "ClickHouse service :8443 TLS", ["nyc_tlc_data (taxi_trips, taxi_zones,", "views, CDC MV) + otel db (logs, traces)"], accent=True)
- s += box(620, 330, 280, 88, "ClickPipes", ["Postgres CDC pipe", "snapshot + stream (~60s)"])
- s += box(930, 330, 280, 88, "Managed ClickStack / HyperDX", ["traces + logs UI", "module 05"])
- s += box(620, 500, 590, 82, "Postgres managed by ClickHouse :5432 TLS", ["you create it with clickhousectl (module 03) · public.realtime_trips + pub_taxi"])
- s += box(620, 628, 280, 74, "Remote MCP", ["/mcp + /clickstack (OAuth)"])
- s += box(930, 628, 280, 74, "ClickHouse Agents", ["ai.clickhouse.cloud (module 04)"])
- s += box(620, 748, 590, 60, "Instructor cloud fallback only", ["managed Postgres pool when a trial org cannot create one"], dashed=True, fill="#1C1B14")
-
- # --- third-party boxes ---
- s += box(1315, 195, 330, 74, "Langfuse Cloud", ["chat traces, sessions, cost"])
- s += box(1315, 300, 330, 74, "OpenAI API", ["chat completions (gpt-5.4-mini)"])
- s += box(1315, 410, 330, 88, "NYC TLC dataset", ["public parquet; read once by the", "module 01 seed. Data source only."])
- s += box(1315, 530, 330, 74, "Hosted LibreChat", ["optional SRE chat · remote MCP"])
-
- # --- edges (laptop internal) ---
- s += edge(287, 252, 287, 300, "/api proxy", label_dx=42)
- s += edge(180, 362, 180, 410, "OTLP", label_dx=-34)
-
- # laptop -> cloud (near-horizontal, labels in the gap)
- s += elbow([(515, 331), (560, 331), (560, 250), (620, 250)], "SQL · TLS :8443", mid=(567, 300))
- s += elbow([(515, 438), (585, 438), (585, 262), (620, 262)], "traces → otel db", mid=(585, 400), dashed=False)
- s += edge(515, 545, 620, 541, "INSERT trips · TLS", label_dy=-8)
- s += edge(515, 690, 620, 665, "MCP · OAuth", label_dy=-8)
-
- # cloud internal (CDC chain + reads)
- s += edge(760, 500, 760, 418, "logical replication", label_dx=0, label_dy=-4)
- s += edge(760, 330, 760, 281, "CDC rows ~60s", label_dy=-4)
- s += edge(1070, 330, 1070, 281, "reads otel db", label_dy=-4)
- # agents -> service (RBAC), up the cloud's right margin
- s += elbow([(1070, 628), (1215, 628), (1215, 235), (1210, 235)], "RBAC SQL", mid=(1215, 470))
-
- # third-party: backend -> OpenAI / Langfuse along the top bus (above zones)
- s += elbow([(360, 300), (360, 118), (1480, 118), (1480, 300)], "chat completions", mid=(950, 112))
- s += elbow([(330, 300), (330, 96), (1430, 96), (1430, 195)], "chat traces (Langfuse SDK)", mid=(760, 90))
- # TLC seed -> service (in the cloud/third gap)
- s += elbow([(1315, 452), (1262, 452), (1262, 215), (1210, 215)], "url() seed", mid=(1262, 305))
+ s += zone(30, 110, 330, 480, "laptop")
+ s += zone(400, 110, 500, 480, "cloud")
+ s += zone(940, 110, 230, 480, "third")
+
+ # Participant laptop: application edge and stateless tools only.
+ s += box(50, 165, 290, 78, "Frontend + FastAPI", ["dashboards, chat, and API", "runs with Docker Compose"])
+ s += box(50, 270, 290, 68, "Load generator", ["creates synthetic trip rows"])
+ s += box(50, 365, 290, 68, "OTel forwarder", ["sends telemetry to ClickStack"])
+ s += box(50, 460, 290, 78, "Coding agent + CLI", ["workshop commands, MCP,", "and ClickHouse skills"])
+
+ # ClickHouse Cloud: state, ingestion, observability, and AI surfaces.
+ s += box(425, 165, 450, 88, "ClickHouse service", ["default.realtime_trips lands here", "nyc_tlc_data.taxi_trips powers the app"], accent=True)
+ s += box(425, 285, 215, 78, "Managed Postgres", ["public.realtime_trips"])
+ s += box(660, 285, 215, 78, "ClickPipes", ["streams Postgres changes"])
+ s += box(425, 395, 215, 88, "Managed ClickStack", ["HyperDX traces, logs,", "dashboards, and alerts"])
+ s += box(660, 395, 215, 88, "Agents + MCP", ["data questions and", "coding-agent tools"])
+
+ # External services used by the app, kept separate from workshop data storage.
+ s += box(960, 175, 190, 82, "OpenAI API", ["chat completions"])
+ s += box(960, 290, 190, 82, "Langfuse Cloud", ["chat traces and cost"])
+ s += box(960, 405, 190, 92, "NYC TLC dataset", ["one-time historical", "seed source"])
s += "\n"
return s
@@ -320,99 +292,56 @@ def up_arrow(cx, y_from, y_to):
# ===========================================================================
-# DATA FLOW (four swimlanes: seed, live CDC, read, observe)
+# DATA FLOW (one learner-facing left-to-right story)
# ===========================================================================
def data_flow():
- W, H = 1560, 900
+ W, H = 1200, 620
s = _svg_header(W, H)
s += (f''
- f'ClickHouse BUILD Workshop · Data flow\n')
+ f'ClickHouse BUILD Workshop · A trip\'s path\n')
s += (f''
- f'How a row moves: seeded once, streamed continuously, then read and observed — '
- f'all through your ClickHouse service.\n')
-
- LX, LW = 30, W - 60
- TITLE_W = 176 # left title gutter per lane
-
- def lane(y, h, title, subtitle):
- t = (f'\n')
- t += (f'\n')
- t += (f'{escape(title)}\n')
- # wrap subtitle into the gutter
- words, line, ln = subtitle.split(), "", 0
- for wd in words:
- if len(line) + len(wd) > 20:
- t += (f'{escape(line)}\n'); line = wd; ln += 1
- else:
- line = (line + " " + wd).strip()
- if line:
- t += (f'{escape(line)}\n')
- return t
+ f'Follow steps 1–6: across the top, then back across the bottom.\n')
+
+ xcols, yrows, bw, bh = [40, 450, 860], [135, 345], 300, 110
+ steps = [
+ ("Load generator", ["creates synthetic trips", "on your laptop"], False),
+ ("Managed Postgres", ["source table:", "public.realtime_trips"], False),
+ ("ClickPipes", ["streams each change", "with Postgres CDC"], False),
+ ("ClickHouse", ["first landing table:", "default.realtime_trips"], True),
+ ("Materialized view", ["moves new rows into", "nyc_tlc_data.taxi_trips"], False),
+ ("Frontend + FastAPI", ["reads taxi_trips for", "the live dashboards"], False),
+ ]
- def fbox(x, y, w, label, sub, accent=False):
- return box(x, y, w, 62, label, [sub] if sub else None, accent=accent, rounded=10)
-
- def harrow(x1, x2, y, label):
- return edge(x1, y, x2, y, label, label_dy=-9)
-
- x0 = LX + TITLE_W + 10 # where the flow chain starts
- bw = 216 # box width
- gap = 140 # arrow gap (wide enough for the longest label chip)
-
- def col(i):
- return x0 + i*(bw+gap)
-
- # Lane 1 — SEED (module 01)
- y = 96; s += lane(y, 96, "Seed", "one-time, module 01")
- by = y + 17
- s += fbox(col(0), by, bw, "NYC TLC parquet", "~3.2M rows, public")
- s += fbox(col(2), by, bw, "ClickHouse service", "nyc_tlc_data.taxi_trips", accent=True)
- s += harrow(col(0)+bw, col(2)-6, by+31, "url() seed — one statement, module 01")
-
- # Lane 2 — LIVE CDC (module 03)
- y = 212; s += lane(y, 110, "Live CDC", "continuous, module 03")
- by = y + 24
- s += fbox(col(0), by, bw, "pg-trip-writer", "synthetic trips")
- s += fbox(col(1), by, bw, "Managed Postgres", "public.realtime_trips")
- s += fbox(col(2), by, bw, "ClickPipes", "Postgres CDC pipe")
- s += fbox(col(3), by, bw, "ClickHouse service", "MV → taxi_trips", accent=True)
- s += harrow(col(0)+bw, col(1)-6, by+31, "INSERT · TLS")
- s += harrow(col(1)+bw, col(2)-6, by+31, "replication")
- s += harrow(col(2)+bw, col(3)-6, by+31, "CDC rows ~60s")
-
- # Lane 3 — READ (modules 02, 04, 08)
- y = 342; s += lane(y, 176, "Read", "modules 02, 04, 08")
- by = y + 22
- s += fbox(col(0), by, bw, "ClickHouse service", "nyc_tlc_data", accent=True)
- s += fbox(col(1), by, bw, "FastAPI", "safe parameterized SQL")
- s += fbox(col(2), by, bw, "Ops + Historical", "React dashboards")
- s += harrow(col(0)+bw, col(1)-6, by+31, "SELECT")
- s += harrow(col(1)+bw, col(2)-6, by+31, "GET /api/*")
- # two more consumers off the service (stacked below)
- by2 = y + 104
- s += fbox(col(1), by2, bw, "in-app AI chat", "guarded SELECT (module 08)")
- s += fbox(col(2), by2, bw, "ClickHouse Agents", "RBAC SQL (module 04)")
- s += elbow([(col(0)+bw/2, by+62), (col(0)+bw/2, by2+31), (col(1)-6, by2+31)], "NL → SQL", mid=(col(0)+bw/2+70, by2+22))
- s += harrow(col(1)+bw, col(2)-6, by2+31, "RBAC SQL")
-
- # Lane 4 — OBSERVE (modules 05, 06, 07)
- y = 538; s += lane(y, 176, "Observe", "modules 05, 06, 07")
- by = y + 22
- s += fbox(col(0), by, bw, "backend spans", "clickhouse.query + logs")
- s += fbox(col(1), by, bw, "stateless OTel forwarder", "local OTLP :4318")
- s += fbox(col(2), by, bw, "otel db", "in your service")
- s += fbox(col(3), by, bw, "HyperDX UI", "search, traces, dashboards")
- s += harrow(col(0)+bw, col(1)-6, by+31, "OTLP")
- s += harrow(col(1)+bw, col(2)-6, by+31, "write")
- s += harrow(col(2)+bw, col(3)-6, by+31, "read")
- by2 = y + 104
- s += fbox(col(1), by2, bw, "coding agent", "via ClickStack MCP")
- s += elbow([(col(1)+bw/2, by2), (col(1)+bw/2, by+62)], None)
- s += (f''
- f'clickstack_search / save_dashboard / save_alert\n')
+ # Numbered snake: left-to-right on row one, then right-to-left on row two.
+ positions = [
+ (xcols[0], yrows[0]),
+ (xcols[1], yrows[0]),
+ (xcols[2], yrows[0]),
+ (xcols[2], yrows[1]),
+ (xcols[1], yrows[1]),
+ (xcols[0], yrows[1]),
+ ]
+ for i, (title, lines, accent) in enumerate(steps):
+ x, y = positions[i]
+ s += box(x, y, bw, bh, title, lines, accent=accent, rounded=12)
+ s += (f'\n'
+ f'{i+1}\n')
+
+ # Every connector stays in the gutter between boxes. Label chips are painted
+ # after each line, so a line can never cut through its text.
+ s += edge(340, 190, 443, 190, "INSERT", mid=(395, 167))
+ s += edge(750, 190, 853, 190, "CDC", mid=(805, 167))
+ s += elbow([(1010, 245), (1010, 338)], "lands here", mid=(1010, 302))
+ s += edge(860, 400, 757, 400, "feeds", mid=(805, 377))
+ s += edge(450, 400, 347, 400, "reads", mid=(395, 377))
+
+ s += (f'\n')
+ s += (f''
+ f'What the other workshop tools do\n')
+ s += (f''
+ f'ClickStack observes the app · ClickHouse Agents answers data questions · Langfuse traces the chat\n')
s += "\n"
return s
diff --git a/workshops/build_workshop/docs/diagrams/workshop-architecture.svg b/workshops/build_workshop/docs/diagrams/workshop-architecture.svg
index cd365ec..f261096 100644
--- a/workshops/build_workshop/docs/diagrams/workshop-architecture.svg
+++ b/workshops/build_workshop/docs/diagrams/workshop-architecture.svg
@@ -1,107 +1,54 @@
-