<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[tycoworks]]></title><description><![CDATA[Exploring real-time infrastructure, AI with guarantees, and AI-native development.]]></description><link>https://www.tycoworks.com</link><image><url>https://substackcdn.com/image/fetch/$s_!szLU!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7e68468a-6113-4d6f-a039-39e74145408c_1024x1024.png</url><title>tycoworks</title><link>https://www.tycoworks.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 10 Nov 2026 11:19:24 GMT</lastBuildDate><atom:link href="https://www.tycoworks.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Chris]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[tycoworks@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[tycoworks@substack.com]]></itunes:email><itunes:name><![CDATA[Chris]]></itunes:name></itunes:owner><itunes:author><![CDATA[Chris]]></itunes:author><googleplay:owner><![CDATA[tycoworks@substack.com]]></googleplay:owner><googleplay:email><![CDATA[tycoworks@substack.com]]></googleplay:email><googleplay:author><![CDATA[Chris]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Can Slides Be Built From Source Files?]]></title><description><![CDATA[Creating editable PowerPoint slides from markdown, using existing .pptx templates]]></description><link>https://www.tycoworks.com/p/marketing-collateral-from-source-files</link><guid isPermaLink="false">https://www.tycoworks.com/p/marketing-collateral-from-source-files</guid><dc:creator><![CDATA[Chris]]></dc:creator><pubDate>Sun, 27 Sep 2026 20:14:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!oRC_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This post is the story of how I built tycoslide, which turns markdown into editable PowerPoint slides using your existing template. To skip straight to trying it, see <a href="https://github.com/tycoworks/tycoslide">tycoslide on GitHub</a>.</em></p><p>I do product strategy and product marketing for early-stage startups, mostly with technical products like databases and developer tools. A lot of the work is helping dial in their roadmap, messaging, and positioning. But I also help create finished collateral from those foundations, such as one-pagers, pitch decks, and battlecards.</p><p>One of the biggest challenges is keeping everything in sync. Make a change to the messaging, for example, and I may need to change every one-pager, every pitch deck, and every battlecard. AI agents like Claude Code can quickly make such changes to plain text, such as markdown files or wiki pages. But editing PowerPoint files or Canva designs takes longer, and there&#8217;s no easy way to verify the result without opening and checking each file.</p><p>This got me thinking about static site generators such as <a href="https://gohugo.io/">Hugo</a>, which build finished web pages from plain text content. Could presentations and collateral work in a similar way? Could I instead build these assets from declarative source files? I decided to find out.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.tycoworks.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">The follow-up post will cover building themes &amp; decks with AI agents. Subscribe so you don&#8217;t miss out!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Exploring the Options</h2><p>We&#8217;ve seen a wave of new AI slide tools over the past few years, which have changed the way slides and collateral are made. <a href="https://gamma.app/">Gamma</a>, for example, lets you build a deck through prompts rather than clicking around in a UI. <a href="https://notebook.google.com/">NotebookLM</a> goes further, using image generation models to produce high-fidelity slides and infographics. And <a href="https://claude.com/product/design">Claude Design</a> can build a complete design system, helping you create slides, one-pagers, and mockups with a common look and feel.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vxwD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vxwD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 424w, https://substackcdn.com/image/fetch/$s_!vxwD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 848w, https://substackcdn.com/image/fetch/$s_!vxwD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 1272w, https://substackcdn.com/image/fetch/$s_!vxwD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vxwD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png" width="1456" height="797" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:797,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:875029,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.tycoworks.com/i/217720778?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vxwD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 424w, https://substackcdn.com/image/fetch/$s_!vxwD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 848w, https://substackcdn.com/image/fetch/$s_!vxwD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 1272w, https://substackcdn.com/image/fetch/$s_!vxwD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff7dcef44-2d4d-485c-a3a6-dfeb04cf07b4_3024x1656.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Claude Design&#8217;s design-system view for the tycoworks template</figcaption></figure></div><p>However, none of these tools suited my use case, as they work from prompts that can be interpreted differently each time, rather than from source files like Hugo does. I needed a tool that could:</p><ol><li><p>Generate slides from source files that I could track in Git.</p></li><li><p>Produce editable output that clients could open in their existing tools.</p></li><li><p>Match a client&#8217;s existing design system exactly.</p></li></ol><p>That narrowed the search to markdown presentation tools and template engines.</p><h3>Markdown Presentation Tools</h3><p><a href="https://marp.app/">Marp</a> and <a href="https://sli.dev/">Slidev</a> are examples of markdown presentation tools, which work in a similar way to Hugo. You write slides as markdown files, which are then rendered into HTML and exported to PDF or PowerPoint. You can customize the design system of each with CSS, and both have open-source themes to start from, such as <a href="https://github.com/gureckis/slidev-theme-neversink">Neversink</a> for Slidev. Both are popular with engineers and engineering use cases, with support for code syntax highlighting and <a href="https://mermaid.js.org/">Mermaid</a> diagrams.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oRC_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oRC_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 424w, https://substackcdn.com/image/fetch/$s_!oRC_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 848w, https://substackcdn.com/image/fetch/$s_!oRC_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 1272w, https://substackcdn.com/image/fetch/$s_!oRC_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!oRC_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png" width="1456" height="916" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:916,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2319925,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.tycoworks.com/i/217720778?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oRC_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 424w, https://substackcdn.com/image/fetch/$s_!oRC_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 848w, https://substackcdn.com/image/fetch/$s_!oRC_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 1272w, https://substackcdn.com/image/fetch/$s_!oRC_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feace4f48-6d04-4792-8503-360d053ee1a0_3600x2264.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The tycoworks title slide rebuilt as a Slidev theme</figcaption></figure></div><p>Slidev in particular looked promising, given its extensible component model based on <a href="https://vuejs.org/">Vue</a>. I implemented my <code>tycoworks</code> color scheme and slide layouts in CSS, and built some Vue components for things I commonly use, such as timeline views and comparison matrices.</p><p>The result looked great, but there was a big problem: neither Slidev nor Marp export editable PowerPoint files. Both render each slide to an image using a headless browser, rather than implementing the design as native PowerPoint objects, and so I had to keep searching.</p><h3>Template Engines</h3><p>Template engines work by filling in placeholders within PowerPoint files, such as <code>{title}</code> typed into the title slide. It&#8217;s a tried and tested approach so there are plenty of options, from open-source libraries like <a href="https://docxtemplater.com/">docxtemplater</a> through to commercial products like <a href="https://conga.com/products/conga-composer">Conga Composer</a>. The big advantage is that you reuse your existing slides, so these tools are quick to set up and the output is editable and on brand by design.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hw5a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hw5a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 424w, https://substackcdn.com/image/fetch/$s_!hw5a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 848w, https://substackcdn.com/image/fetch/$s_!hw5a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 1272w, https://substackcdn.com/image/fetch/$s_!hw5a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!hw5a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png" width="1456" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:600,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:208163,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.tycoworks.com/i/217720778?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hw5a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 424w, https://substackcdn.com/image/fetch/$s_!hw5a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 848w, https://substackcdn.com/image/fetch/$s_!hw5a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 1272w, https://substackcdn.com/image/fetch/$s_!hw5a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3c018116-3f09-46b1-b310-df9d86aa5045_2400x989.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The tycoworks title slide filled in by docxtemplater</figcaption></figure></div><p>I tried docxtemplater, since it&#8217;s open core, has been around for over a decade, and supports Word and Excel as well as PowerPoint. I first added placeholders to my slides, such as <code>{title}</code>, <code>{name}</code>, and <code>{jobtitle}</code> in the case of the opening slide. I could then write new presentations as JSON files of key-value pairs, which docxtemplater merges into the slides.</p><p>It was simple and my slides looked perfect, but this simplicity was also a limitation. While template engines work well for plain text values like <code>{title}</code>, swapping in whole <em>blocks</em> of text, with any mix of bullets, paragraphs, and formatting, is harder. With docxtemplater, you have to slice content into separate text runs to apply different formatting, which can make the JSON unwieldy and hard to diff versus markdown.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VXtW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VXtW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 424w, https://substackcdn.com/image/fetch/$s_!VXtW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 848w, https://substackcdn.com/image/fetch/$s_!VXtW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 1272w, https://substackcdn.com/image/fetch/$s_!VXtW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!VXtW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png" width="1456" height="373" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:373,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:103082,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.tycoworks.com/i/217720778?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VXtW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 424w, https://substackcdn.com/image/fetch/$s_!VXtW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 848w, https://substackcdn.com/image/fetch/$s_!VXtW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 1272w, https://substackcdn.com/image/fetch/$s_!VXtW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3534c0f-546c-47c2-8ac4-05bf7d07b46e_2396x613.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The same slide in Slidev markdown and docxtemplater JSON</figcaption></figure></div><h2>Filling in the Gap</h2><p>Both approaches had their strengths. Slidev let me write slides in markdown, whereas docxtemplater gave me native slides I could edit. But I needed a tool that could do both and more, so I set out to build one.</p><p>My <a href="https://github.com/tycoworks/tycoslide/tree/166aeb6">first attempt</a> was similar to Slidev, but with its own flexbox-based DSL instead of raw CSS and Vue. You defined design tokens, layouts, and components with TypeScript, then wrote the slides themselves in markdown. Under the hood, it rendered each slide as HTML, measured the position and size of every object, then wrote an editable PowerPoint file with <a href="https://gitbrent.github.io/PptxGenJS/">pptxgenjs</a>.</p><p>However, I eventually abandoned this idea due to differences between HTML and PowerPoint rendering. Slight variations in glyph widths, line heights, and text wrapping meant a line could break differently in PowerPoint, throwing off the position of everything around it. I&#8217;d also need to support a huge amount of HTML <em>and</em> PowerPoint for it to be useful, further confirming it was the wrong approach.</p><p>So I kept the markdown layer but replaced the renderer underneath with a template engine. The result was <a href="https://github.com/tycoworks/tycoslide">tycoslide</a>.</p><h2>How It Works</h2><p>tycoslide is a template engine with a markdown front end. You start by wrapping your existing <code>.pptx</code> file as a &#8216;theme&#8217; &#8212; an npm package that describes layouts, colors, and fonts &#8212; which you can version and manage like code. You then write slides in <a href="https://github.github.com/gfm/">GitHub-flavored markdown</a>, including code blocks and Mermaid diagrams, which tycoslide turns into editable PowerPoint files.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;ba2ed5c4-0650-43a2-9417-05649dcf370d&quot;,&quot;duration&quot;:null}"></div><p>Rather than putting placeholders like <code>{title}</code> directly into the template, tycoslide uses a separate manifest to describe the available text, tables, and images per layout. Under the hood, it uses <a href="https://github.com/singerla/pptx-automizer">pptx-automizer</a> to fill the template, <a href="https://github.com/remarkjs/remark">remark</a> to parse the markdown, and <a href="https://shiki.style/">Shiki</a> to highlight code. It also packages each theme as an agent skill.</p><p>Want to try it out? The <a href="https://github.com/tycoworks/tycoslide/blob/main/docs/guide.md">guide</a> walks you through setting tycoslide up on your template, including packaging it as a skill for your team. Or if you just want to see it in action, the <a href="https://github.com/tycoworks/tycoworks-theme">tycoworks theme</a> and its two example decks are a good place to start.</p><h2>Conclusion</h2><p>So, can slides be built from source files? For me, yes. Most of my decks are now themes and markdown files in Git, which Claude Code can update in bulk when things change. And because the theme enforces the design, I don&#8217;t need to visually check each change, so I can work entirely through pull requests like code.</p><p>Where could it go from here? There are some near-term features I need, like supporting charts and multiple PowerPoint files per theme, so I can build one-pagers, pitch decks, and battlecards from the same theme. I could also add a template system, so that common content like taglines could be injected instead of duplicated (a template engine inside a template engine? <em>Meta</em>).</p><p>But my bet is to keep tycoslide simple and build more into the agent layer instead. <a href="https://agentskills.io/">Skills</a> have become a common standard for giving agents new capabilities, replacing a lot of custom code. Frontier LLMs are becoming cheaper and more capable every week. And new decision models like <a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev">Jev</a> and <a href="https://github.com/NandhaKishorM/laya">Laya</a> could detect outdated messaging or banned terms in milliseconds, making them fast and cheap enough to run on every change.</p><p>If you&#8217;ve also been thinking about this, I&#8217;d love to hear from you. What have you tried? Did tycoslide work for you? Drop me a line here or at <a href="mailto:chris@tycoworks.com">chris@tycoworks.com</a>.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.tycoworks.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share tycoworks&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.tycoworks.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share tycoworks</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[tycostream: Turn Materialize Views into Real-Time GraphQL APIs]]></title><description><![CDATA[Building an API layer for streaming databases]]></description><link>https://www.tycoworks.com/p/tycostream-turn-materialize-views</link><guid isPermaLink="false">https://www.tycoworks.com/p/tycostream-turn-materialize-views</guid><dc:creator><![CDATA[Chris]]></dc:creator><pubDate>Tue, 06 Jan 2026 01:13:06 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b38b747d-4522-4ba2-be37-b71edf4397ef_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my last two posts (<a href="https://tycoworks.substack.com/p/can-a-streaming-database-power-a">Part 1</a> and <a href="https://tycoworks.substack.com/p/can-a-streaming-database-power-a-f49">Part 2</a>), I explored whether streaming databases like <a href="https://materialize.com/">Materialize</a> could power a trading desk UI. The answer was a cautious yes: creating the backend logic was straightforward, but getting data into the frontend was surprisingly hard. I ended up building a custom WebSocket relay for the last mile which worked, but felt too brittle and difficult to maintain in the long term.</p><p>I wanted a better way, and so I&#8217;ve started working on a new project: <a href="https://github.com/tycoworks/tycostream">tycostream</a>. It turns Materialize views into real-time GraphQL APIs, so you can quickly build live, type-safe apps, agents, and dashboards without needing custom infrastructure or glue code.</p><p>It would need a lot more work for production use, but the core functionality is ready for feedback. Let&#8217;s take a look.</p><h2><strong>How It Works</strong></h2><p>The design is inspired by view servers such as <a href="https://vuu.finos.org/">Finos Vuu</a> or the <a href="https://docs.genesis.global/docs/develop/server-capabilities/real-time-queries-data-server/">Genesis Data Server</a>, which are common components of financial markets applications. These components excel at streaming ticking prices, positions, and orders to thousands of connected UIs with fine-grained permissions and filtering. I wanted to build something similar but using open standards: GraphQL for the API and Postgres wire protocol for the database connection.</p><p>With that in mind, tycostream works a bit like <a href="https://hasura.io/">Hasura</a>, but for streaming databases (currently Materialize). There are two key features:</p><ul><li><p><strong>Subscriptions</strong> stream typed, real-time updates to clients, with optional <code>where</code> clauses for filtering.</p></li><li><p><strong>Triggers</strong> register webhooks that fire when data meets specific conditions, for alerting, workflow automation, or integrating with external systems.</p></li></ul><p>You define the views to expose, and tycostream generates GraphQL subscriptions and mutations from their schema. For example, given a <code>trades</code> table like this:</p><div class="github-gist" data-attrs="{&quot;innerHTML&quot;:&quot;<div id=\&quot;gist144200514\&quot; class=\&quot;gist\&quot;>\n    <div class=\&quot;gist-file\&quot; translate=\&quot;no\&quot; data-color-mode=\&quot;light\&quot; data-light-theme=\&quot;light\&quot;>\n      <div class=\&quot;gist-data\&quot;>\n        <div class=\&quot;js-gist-file-update-container js-task-list-container\&quot;>\n  <div id=\&quot;file-trades_schema-md\&quot; class=\&quot;file my-2\&quot;>\n      <div id=\&quot;file-trades_schema-md-readme\&quot; class=\&quot;Box-body readme blob p-5 p-xl-6 \&quot;\n    style=\&quot;overflow: auto\&quot; tabindex=\&quot;0\&quot; role=\&quot;region\&quot;\n    aria-label=\&quot;trades_schema.md content, created by chrismichaelanderson on 01:05PM yesterday.\&quot;\n  >\n    <article class=\&quot;markdown-body entry-content container-lg\&quot; itemprop=\&quot;text\&quot;><div class=\&quot;highlight highlight-source-sql\&quot; dir=\&quot;auto\&quot;><pre><span class=\&quot;pl-k\&quot;>CREATE</span> <span class=\&quot;pl-k\&quot;>TABLE</span> <span class=\&quot;pl-en\&quot;>trades</span> (\n  id <span class=\&quot;pl-k\&quot;>INT</span>,\n  instrument_id <span class=\&quot;pl-k\&quot;>INT</span>,\n  side <span class=\&quot;pl-k\&quot;>TEXT</span>,\n  quantity <span class=\&quot;pl-k\&quot;>INT</span>,\n  price <span class=\&quot;pl-k\&quot;>NUMERIC</span>,\n  executed_at <span class=\&quot;pl-k\&quot;>TIMESTAMP</span>\n);</pre></div>\n</article>\n  </div>\n\n  </div>\n</div>\n\n      </div>\n      <div class=\&quot;gist-meta\&quot;>\n        <a href=\&quot;https://gist.github.com/chrismichaelanderson/84a8f58a6345f8adbb70d618b59313cc/raw/daaa02943b7534866ca34154c803877e52c74bdc/trades_schema.md\&quot; style=\&quot;float:right\&quot; class=\&quot;Link--inTextBlock\&quot;>view raw</a>\n        <a href=\&quot;https://gist.github.com/chrismichaelanderson/84a8f58a6345f8adbb70d618b59313cc#file-trades_schema-md\&quot; class=\&quot;Link--inTextBlock\&quot;>\n          trades_schema.md\n        </a>\n        hosted with &amp;#10084; by <a class=\&quot;Link--inTextBlock\&quot; href=\&quot;https://github.com\&quot;>GitHub</a>\n      </div>\n    </div>\n</div>\n&quot;,&quot;stylesheet&quot;:&quot;https://github.githubassets.com/assets/gist-embed-68783a026c0c.css&quot;}" data-component-name="GitgistToDOM"><link rel="stylesheet" href="https://github.githubassets.com/assets/gist-embed-68783a026c0c.css"><div id="gist144200514" class="gist">
    <div class="gist-file" data-color-mode="light" data-light-theme="light">
      <div class="gist-data">
        <div class="js-gist-file-update-container js-task-list-container">
  <div id="file-trades_schema-md" class="file my-2">
      <div id="file-trades_schema-md-readme" class="Box-body readme blob p-5 p-xl-6 " style="overflow:auto">
    <article class="markdown-body entry-content container-lg" itemprop="text"><div class="highlight highlight-source-sql"><pre><span class="pl-k">CREATE</span> <span class="pl-k">TABLE</span> <span class="pl-en">trades</span> (
  id <span class="pl-k">INT</span>,
  instrument_id <span class="pl-k">INT</span>,
  side <span class="pl-k">TEXT</span>,
  quantity <span class="pl-k">INT</span>,
  price <span class="pl-k">NUMERIC</span>,
  executed_at <span class="pl-k">TIMESTAMP</span>
);</pre></div>
</article>
  </div>

  </div>
</div>

      </div>
      <div class="gist-meta">
        <a href="https://gist.github.com/chrismichaelanderson/84a8f58a6345f8adbb70d618b59313cc/raw/daaa02943b7534866ca34154c803877e52c74bdc/trades_schema.md" style="float:right" class="Link--inTextBlock">view raw</a>
        <a href="https://gist.github.com/chrismichaelanderson/84a8f58a6345f8adbb70d618b59313cc#file-trades_schema-md" class="Link--inTextBlock">
          trades_schema.md
        </a>
        hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com">GitHub</a>
      </div>
    </div>
</div>
</div><p>tycostream generates a GraphQL API like this:</p><div class="github-gist" data-attrs="{&quot;innerHTML&quot;:&quot;<div id=\&quot;gist144199974\&quot; class=\&quot;gist\&quot;>\n    <div class=\&quot;gist-file\&quot; translate=\&quot;no\&quot; data-color-mode=\&quot;light\&quot; data-light-theme=\&quot;light\&quot;>\n      <div class=\&quot;gist-data\&quot;>\n        <div class=\&quot;js-gist-file-update-container js-task-list-container\&quot;>\n  <div id=\&quot;file-trades_api-md\&quot; class=\&quot;file my-2\&quot;>\n      <div id=\&quot;file-trades_api-md-readme\&quot; class=\&quot;Box-body readme blob p-5 p-xl-6 \&quot;\n    style=\&quot;overflow: auto\&quot; tabindex=\&quot;0\&quot; role=\&quot;region\&quot;\n    aria-label=\&quot;trades_api.md content, created by chrismichaelanderson on 12:47PM yesterday.\&quot;\n  >\n    <article class=\&quot;markdown-body entry-content container-lg\&quot; itemprop=\&quot;text\&quot;><div class=\&quot;highlight highlight-source-graphql\&quot; dir=\&quot;auto\&quot;><pre><span class=\&quot;pl-k\&quot;>type</span> <span class=\&quot;pl-c1\&quot;>Subscription</span> {\n  <span class=\&quot;pl-v\&quot;>trades</span>(<span class=\&quot;pl-v\&quot;>where</span>: <span class=\&quot;pl-c1\&quot;>tradesExpression</span>): <span class=\&quot;pl-c1\&quot;>tradesUpdate</span><span class=\&quot;pl-k\&quot;>!</span>\n}\n\n<span class=\&quot;pl-k\&quot;>type</span> <span class=\&quot;pl-c1\&quot;>Mutation</span> {\n  <span class=\&quot;pl-v\&quot;>create_trades_trigger</span>(<span class=\&quot;pl-v\&quot;>input</span>: <span class=\&quot;pl-c1\&quot;>tradesTriggerInput</span><span class=\&quot;pl-k\&quot;>!</span>): <span class=\&quot;pl-c1\&quot;>Trigger</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>delete_trades_trigger</span>(<span class=\&quot;pl-v\&quot;>name</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>): <span class=\&quot;pl-c1\&quot;>Trigger</span><span class=\&quot;pl-k\&quot;>!</span>\n}\n\n<span class=\&quot;pl-k\&quot;>type</span> <span class=\&quot;pl-c1\&quot;>Query</span> {\n  <span class=\&quot;pl-v\&quot;>trades_triggers</span>: [<span class=\&quot;pl-c1\&quot;>Trigger</span><span class=\&quot;pl-k\&quot;>!</span>]<span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>trades_trigger</span>(<span class=\&quot;pl-v\&quot;>name</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>): <span class=\&quot;pl-c1\&quot;>Trigger</span>\n}\n\n<span class=\&quot;pl-k\&quot;>type</span> <span class=\&quot;pl-c1\&quot;>tradesUpdate</span> {\n  <span class=\&quot;pl-v\&quot;>operation</span>: <span class=\&quot;pl-c1\&quot;>RowOperation</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>data</span>: <span class=\&quot;pl-c1\&quot;>trades</span>\n  <span class=\&quot;pl-v\&quot;>fields</span>: [<span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>]<span class=\&quot;pl-k\&quot;>!</span>\n}\n\n<span class=\&quot;pl-k\&quot;>type</span> <span class=\&quot;pl-c1\&quot;>trades</span> {\n  <span class=\&quot;pl-v\&quot;>id</span>: <span class=\&quot;pl-c1\&quot;>Int</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>instrument_id</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>side</span>: <span class=\&quot;pl-c1\&quot;>side</span>\n  <span class=\&quot;pl-v\&quot;>quantity</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>price</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>executed_at</span>: <span class=\&quot;pl-c1\&quot;>String</span>\n}\n\n<span class=\&quot;pl-k\&quot;>type</span> <span class=\&quot;pl-c1\&quot;>Trigger</span> {\n  <span class=\&quot;pl-v\&quot;>name</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>webhook</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>fire</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>clear</span>: <span class=\&quot;pl-c1\&quot;>String</span>\n}\n\n<span class=\&quot;pl-k\&quot;>enum</span> <span class=\&quot;pl-c1\&quot;>RowOperation</span> {\n<span class=\&quot;pl-c1\&quot;>  INSERT</span>\n<span class=\&quot;pl-c1\&quot;>  UPDATE</span>\n<span class=\&quot;pl-c1\&quot;>  DELETE</span>\n}\n\n<span class=\&quot;pl-k\&quot;>enum</span> <span class=\&quot;pl-c1\&quot;>side</span> {\n<span class=\&quot;pl-c1\&quot;>  buy</span>\n<span class=\&quot;pl-c1\&quot;>  sell</span>\n}\n\n<span class=\&quot;pl-k\&quot;>input</span> <span class=\&quot;pl-c1\&quot;>tradesTriggerInput</span> {\n  <span class=\&quot;pl-v\&quot;>name</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>webhook</span>: <span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>fire</span>: <span class=\&quot;pl-c1\&quot;>tradesExpression</span><span class=\&quot;pl-k\&quot;>!</span>\n  <span class=\&quot;pl-v\&quot;>clear</span>: <span class=\&quot;pl-c1\&quot;>tradesExpression</span>\n}\n\n<span class=\&quot;pl-k\&quot;>input</span> <span class=\&quot;pl-c1\&quot;>tradesExpression</span> {\n  <span class=\&quot;pl-v\&quot;>id</span>: <span class=\&quot;pl-c1\&quot;>IntComparison</span>\n  <span class=\&quot;pl-v\&quot;>instrument_id</span>: <span class=\&quot;pl-c1\&quot;>IntComparison</span>\n  <span class=\&quot;pl-v\&quot;>side</span>: <span class=\&quot;pl-c1\&quot;>sideComparison</span>\n  <span class=\&quot;pl-v\&quot;>quantity</span>: <span class=\&quot;pl-c1\&quot;>IntComparison</span>\n  <span class=\&quot;pl-v\&quot;>price</span>: <span class=\&quot;pl-c1\&quot;>FloatComparison</span>\n  <span class=\&quot;pl-v\&quot;>executed_at</span>: <span class=\&quot;pl-c1\&quot;>StringComparison</span>\n  <span class=\&quot;pl-v\&quot;>_and</span>: [<span class=\&quot;pl-c1\&quot;>tradesExpression</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_or</span>: [<span class=\&quot;pl-c1\&quot;>tradesExpression</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_not</span>: <span class=\&quot;pl-c1\&quot;>tradesExpression</span>\n}\n\n<span class=\&quot;pl-k\&quot;>input</span> <span class=\&quot;pl-c1\&quot;>IntComparison</span> {\n  <span class=\&quot;pl-v\&quot;>_eq</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>_neq</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>_gt</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>_lt</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>_gte</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>_lte</span>: <span class=\&quot;pl-c1\&quot;>Int</span>\n  <span class=\&quot;pl-v\&quot;>_in</span>: [<span class=\&quot;pl-c1\&quot;>Int</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_nin</span>: [<span class=\&quot;pl-c1\&quot;>Int</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_is_null</span>: <span class=\&quot;pl-c1\&quot;>Boolean</span>\n}\n\n<span class=\&quot;pl-k\&quot;>input</span> <span class=\&quot;pl-c1\&quot;>FloatComparison</span> {\n  <span class=\&quot;pl-v\&quot;>_eq</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>_neq</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>_gt</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>_lt</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>_gte</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>_lte</span>: <span class=\&quot;pl-c1\&quot;>Float</span>\n  <span class=\&quot;pl-v\&quot;>_in</span>: [<span class=\&quot;pl-c1\&quot;>Float</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_nin</span>: [<span class=\&quot;pl-c1\&quot;>Float</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_is_null</span>: <span class=\&quot;pl-c1\&quot;>Boolean</span>\n}\n\n<span class=\&quot;pl-k\&quot;>input</span> <span class=\&quot;pl-c1\&quot;>StringComparison</span> {\n  <span class=\&quot;pl-v\&quot;>_eq</span>: <span class=\&quot;pl-c1\&quot;>String</span>\n  <span class=\&quot;pl-v\&quot;>_neq</span>: <span class=\&quot;pl-c1\&quot;>String</span>\n  <span class=\&quot;pl-v\&quot;>_in</span>: [<span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_nin</span>: [<span class=\&quot;pl-c1\&quot;>String</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_is_null</span>: <span class=\&quot;pl-c1\&quot;>Boolean</span>\n}\n\n<span class=\&quot;pl-k\&quot;>input</span> <span class=\&quot;pl-c1\&quot;>sideComparison</span> {\n  <span class=\&quot;pl-v\&quot;>_eq</span>: <span class=\&quot;pl-c1\&quot;>side</span>\n  <span class=\&quot;pl-v\&quot;>_neq</span>: <span class=\&quot;pl-c1\&quot;>side</span>\n  <span class=\&quot;pl-v\&quot;>_in</span>: [<span class=\&quot;pl-c1\&quot;>side</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_nin</span>: [<span class=\&quot;pl-c1\&quot;>side</span><span class=\&quot;pl-k\&quot;>!</span>]\n  <span class=\&quot;pl-v\&quot;>_is_null</span>: <span class=\&quot;pl-c1\&quot;>Boolean</span>\n}</pre></div>\n</article>\n  </div>\n\n  </div>\n</div>\n\n      </div>\n      <div class=\&quot;gist-meta\&quot;>\n        <a href=\&quot;https://gist.github.com/chrismichaelanderson/e5bbadaecd49b3f4ac172eb5d291ac5c/raw/aba7a18807e31d9553f2f0d8742129e65c913e6f/trades_api.md\&quot; style=\&quot;float:right\&quot; class=\&quot;Link--inTextBlock\&quot;>view raw</a>\n        <a href=\&quot;https://gist.github.com/chrismichaelanderson/e5bbadaecd49b3f4ac172eb5d291ac5c#file-trades_api-md\&quot; class=\&quot;Link--inTextBlock\&quot;>\n          trades_api.md\n        </a>\n        hosted with &amp;#10084; by <a class=\&quot;Link--inTextBlock\&quot; href=\&quot;https://github.com\&quot;>GitHub</a>\n      </div>\n    </div>\n</div>\n&quot;,&quot;stylesheet&quot;:&quot;https://github.githubassets.com/assets/gist-embed-68783a026c0c.css&quot;}" data-component-name="GitgistToDOM"><link rel="stylesheet" href="https://github.githubassets.com/assets/gist-embed-68783a026c0c.css"><div id="gist144199974" class="gist">
    <div class="gist-file" data-color-mode="light" data-light-theme="light">
      <div class="gist-data">
        <div class="js-gist-file-update-container js-task-list-container">
  <div id="file-trades_api-md" class="file my-2">
      <div id="file-trades_api-md-readme" class="Box-body readme blob p-5 p-xl-6 " style="overflow:auto">
    <article class="markdown-body entry-content container-lg" itemprop="text"><div class="highlight highlight-source-graphql"><pre><span class="pl-k">type</span> <span class="pl-c1">Subscription</span> {
  <span class="pl-v">trades</span>(<span class="pl-v">where</span>: <span class="pl-c1">tradesExpression</span>): <span class="pl-c1">tradesUpdate</span><span class="pl-k">!</span>
}

<span class="pl-k">type</span> <span class="pl-c1">Mutation</span> {
  <span class="pl-v">create_trades_trigger</span>(<span class="pl-v">input</span>: <span class="pl-c1">tradesTriggerInput</span><span class="pl-k">!</span>): <span class="pl-c1">Trigger</span><span class="pl-k">!</span>
  <span class="pl-v">delete_trades_trigger</span>(<span class="pl-v">name</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>): <span class="pl-c1">Trigger</span><span class="pl-k">!</span>
}

<span class="pl-k">type</span> <span class="pl-c1">Query</span> {
  <span class="pl-v">trades_triggers</span>: [<span class="pl-c1">Trigger</span><span class="pl-k">!</span>]<span class="pl-k">!</span>
  <span class="pl-v">trades_trigger</span>(<span class="pl-v">name</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>): <span class="pl-c1">Trigger</span>
}

<span class="pl-k">type</span> <span class="pl-c1">tradesUpdate</span> {
  <span class="pl-v">operation</span>: <span class="pl-c1">RowOperation</span><span class="pl-k">!</span>
  <span class="pl-v">data</span>: <span class="pl-c1">trades</span>
  <span class="pl-v">fields</span>: [<span class="pl-c1">String</span><span class="pl-k">!</span>]<span class="pl-k">!</span>
}

<span class="pl-k">type</span> <span class="pl-c1">trades</span> {
  <span class="pl-v">id</span>: <span class="pl-c1">Int</span><span class="pl-k">!</span>
  <span class="pl-v">instrument_id</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">side</span>: <span class="pl-c1">side</span>
  <span class="pl-v">quantity</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">price</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">executed_at</span>: <span class="pl-c1">String</span>
}

<span class="pl-k">type</span> <span class="pl-c1">Trigger</span> {
  <span class="pl-v">name</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>
  <span class="pl-v">webhook</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>
  <span class="pl-v">fire</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>
  <span class="pl-v">clear</span>: <span class="pl-c1">String</span>
}

<span class="pl-k">enum</span> <span class="pl-c1">RowOperation</span> {
<span class="pl-c1">  INSERT</span>
<span class="pl-c1">  UPDATE</span>
<span class="pl-c1">  DELETE</span>
}

<span class="pl-k">enum</span> <span class="pl-c1">side</span> {
<span class="pl-c1">  buy</span>
<span class="pl-c1">  sell</span>
}

<span class="pl-k">input</span> <span class="pl-c1">tradesTriggerInput</span> {
  <span class="pl-v">name</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>
  <span class="pl-v">webhook</span>: <span class="pl-c1">String</span><span class="pl-k">!</span>
  <span class="pl-v">fire</span>: <span class="pl-c1">tradesExpression</span><span class="pl-k">!</span>
  <span class="pl-v">clear</span>: <span class="pl-c1">tradesExpression</span>
}

<span class="pl-k">input</span> <span class="pl-c1">tradesExpression</span> {
  <span class="pl-v">id</span>: <span class="pl-c1">IntComparison</span>
  <span class="pl-v">instrument_id</span>: <span class="pl-c1">IntComparison</span>
  <span class="pl-v">side</span>: <span class="pl-c1">sideComparison</span>
  <span class="pl-v">quantity</span>: <span class="pl-c1">IntComparison</span>
  <span class="pl-v">price</span>: <span class="pl-c1">FloatComparison</span>
  <span class="pl-v">executed_at</span>: <span class="pl-c1">StringComparison</span>
  <span class="pl-v">_and</span>: [<span class="pl-c1">tradesExpression</span><span class="pl-k">!</span>]
  <span class="pl-v">_or</span>: [<span class="pl-c1">tradesExpression</span><span class="pl-k">!</span>]
  <span class="pl-v">_not</span>: <span class="pl-c1">tradesExpression</span>
}

<span class="pl-k">input</span> <span class="pl-c1">IntComparison</span> {
  <span class="pl-v">_eq</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">_neq</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">_gt</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">_lt</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">_gte</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">_lte</span>: <span class="pl-c1">Int</span>
  <span class="pl-v">_in</span>: [<span class="pl-c1">Int</span><span class="pl-k">!</span>]
  <span class="pl-v">_nin</span>: [<span class="pl-c1">Int</span><span class="pl-k">!</span>]
  <span class="pl-v">_is_null</span>: <span class="pl-c1">Boolean</span>
}

<span class="pl-k">input</span> <span class="pl-c1">FloatComparison</span> {
  <span class="pl-v">_eq</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">_neq</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">_gt</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">_lt</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">_gte</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">_lte</span>: <span class="pl-c1">Float</span>
  <span class="pl-v">_in</span>: [<span class="pl-c1">Float</span><span class="pl-k">!</span>]
  <span class="pl-v">_nin</span>: [<span class="pl-c1">Float</span><span class="pl-k">!</span>]
  <span class="pl-v">_is_null</span>: <span class="pl-c1">Boolean</span>
}

<span class="pl-k">input</span> <span class="pl-c1">StringComparison</span> {
  <span class="pl-v">_eq</span>: <span class="pl-c1">String</span>
  <span class="pl-v">_neq</span>: <span class="pl-c1">String</span>
  <span class="pl-v">_in</span>: [<span class="pl-c1">String</span><span class="pl-k">!</span>]
  <span class="pl-v">_nin</span>: [<span class="pl-c1">String</span><span class="pl-k">!</span>]
  <span class="pl-v">_is_null</span>: <span class="pl-c1">Boolean</span>
}

<span class="pl-k">input</span> <span class="pl-c1">sideComparison</span> {
  <span class="pl-v">_eq</span>: <span class="pl-c1">side</span>
  <span class="pl-v">_neq</span>: <span class="pl-c1">side</span>
  <span class="pl-v">_in</span>: [<span class="pl-c1">side</span><span class="pl-k">!</span>]
  <span class="pl-v">_nin</span>: [<span class="pl-c1">side</span><span class="pl-k">!</span>]
  <span class="pl-v">_is_null</span>: <span class="pl-c1">Boolean</span>
}</pre></div>
</article>
  </div>

  </div>
</div>

      </div>
      <div class="gist-meta">
        <a href="https://gist.github.com/chrismichaelanderson/e5bbadaecd49b3f4ac172eb5d291ac5c/raw/aba7a18807e31d9553f2f0d8742129e65c913e6f/trades_api.md" style="float:right" class="Link--inTextBlock">view raw</a>
        <a href="https://gist.github.com/chrismichaelanderson/e5bbadaecd49b3f4ac172eb5d291ac5c#file-trades_api-md" class="Link--inTextBlock">
          trades_api.md
        </a>
        hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com">GitHub</a>
      </div>
    </div>
</div>
</div><h2><strong>Under The Hood</strong></h2><p>tycostream is built with TypeScript using <a href="https://nestjs.com/">NestJS</a>, <a href="https://rxjs.dev/">RxJS</a>, and <a href="https://www.apollographql.com/docs/apollo-server/">Apollo Server</a>. NestJS provides database connectivity and WebSocket support out of the box, along with modules for authentication, observability, and deployment for later down the line. RxJS handles the reactive stream processing, and Apollo Server provides the GraphQL layer with an interactive explorer for testing queries.</p><p>The project is <a href="https://github.com/tycoworks/tycostream/blob/main/docs/architecture.md">architected</a> into three core modules:</p><ul><li><p>A <code>database</code> module that manages Materialize connections and parses the data stream</p></li><li><p>A <code>view</code> module that maintains an in-memory cache and handles client-side filtering</p></li><li><p>An <code>api</code> module that generates the GraphQL schema from view definitions</p></li></ul><p>tycostream lazily instantiates a cache and Materialize connection for each view exposed over the API. When the first request for a given view comes in, tycostream opens a new connection, caches incoming updates, and streams them to the client. Additional requests for the same view are first served a snapshot from the cache, then connected to a live stream of inserts, updates, and deletes. When requests have <code>where</code> clauses, tycostream compiles them into JavaScript functions to filter each row as it streams through.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!98qw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 424w, https://substackcdn.com/image/fetch/$s_!98qw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 848w, https://substackcdn.com/image/fetch/$s_!98qw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 1272w, https://substackcdn.com/image/fetch/$s_!98qw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!98qw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png" width="1456" height="301" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:301,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:935371,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://tycoworks.substack.com/i/183244691?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!98qw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 424w, https://substackcdn.com/image/fetch/$s_!98qw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 848w, https://substackcdn.com/image/fetch/$s_!98qw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 1272w, https://substackcdn.com/image/fetch/$s_!98qw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F67080ae1-773a-4d9d-a582-96d14eb21a94_5203x1076.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div><figcaption class="image-caption">tycostream architecture</figcaption></figure></div><h2><strong>Integration Testing</strong></h2><p>The project includes integration test infrastructure built on <a href="https://testcontainers.com/">Testcontainers</a>, <a href="https://www.apollographql.com/docs/react/">Apollo Client</a>, and <a href="https://expressjs.com/">Express</a>. You define a test scenario as:</p><ol><li><p>A sequence of database operations for tycostream to process</p></li><li><p>The number of clients to spin up and how they should interact with tycostream&#8217;s subscriptions and triggers</p></li><li><p>The expected state each client should see at the end</p></li></ol><p>For example, a test with one client subscribed to positions and a loss alert trigger looks like this:</p><div class="github-gist" data-attrs="{&quot;innerHTML&quot;:&quot;<div id=\&quot;gist144306254\&quot; class=\&quot;gist\&quot;>\n    <div class=\&quot;gist-file\&quot; translate=\&quot;no\&quot; data-color-mode=\&quot;light\&quot; data-light-theme=\&quot;light\&quot;>\n      <div class=\&quot;gist-data\&quot;>\n        <div class=\&quot;js-gist-file-update-container js-task-list-container\&quot;>\n  <div id=\&quot;file-test-md\&quot; class=\&quot;file my-2\&quot;>\n      <div id=\&quot;file-test-md-readme\&quot; class=\&quot;Box-body readme blob p-5 p-xl-6 \&quot;\n    style=\&quot;overflow: auto\&quot; tabindex=\&quot;0\&quot; role=\&quot;region\&quot;\n    aria-label=\&quot;test.md content, created by chrismichaelanderson on 05:09PM today.\&quot;\n  >\n    <article class=\&quot;markdown-body entry-content container-lg\&quot; itemprop=\&quot;text\&quot;><div class=\&quot;highlight highlight-source-ts\&quot; dir=\&quot;auto\&quot;><pre><span class=\&quot;pl-k\&quot;>const</span> <span class=\&quot;pl-s1\&quot;>testEnv</span> <span class=\&quot;pl-c1\&quot;>=</span> <span class=\&quot;pl-k\&quot;>await</span> <span class=\&quot;pl-v\&quot;>TestEnvironment</span><span class=\&quot;pl-kos\&quot;>.</span><span class=\&quot;pl-en\&quot;>create</span><span class=\&quot;pl-kos\&quot;>(</span><span class=\&quot;pl-kos\&quot;>{</span>\n  <span class=\&quot;pl-c1\&quot;>appPort</span>: <span class=\&quot;pl-c1\&quot;>4001</span><span class=\&quot;pl-kos\&quot;>,</span>\n  <span class=\&quot;pl-c1\&quot;>schemaPath</span>: <span class=\&quot;pl-s\&quot;>'test/schema.yaml'</span><span class=\&quot;pl-kos\&quot;>,</span>\n<span class=\&quot;pl-kos\&quot;>}</span><span class=\&quot;pl-kos\&quot;>)</span><span class=\&quot;pl-kos\&quot;>;</span>\n\n<span class=\&quot;pl-k\&quot;>const</span> <span class=\&quot;pl-s1\&quot;>client</span> <span class=\&quot;pl-c1\&quot;>=</span> <span class=\&quot;pl-s1\&quot;>testEnv</span><span class=\&quot;pl-kos\&quot;>.</span><span class=\&quot;pl-en\&quot;>createClient</span><span class=\&quot;pl-kos\&quot;>(</span><span class=\&quot;pl-s\&quot;>'positions-client'</span><span class=\&quot;pl-kos\&quot;>)</span><span class=\&quot;pl-kos\&quot;>;</span>\n\n<span class=\&quot;pl-k\&quot;>await</span> <span class=\&quot;pl-s1\&quot;>client</span><span class=\&quot;pl-kos\&quot;>.</span><span class=\&quot;pl-en\&quot;>subscribe</span><span class=\&quot;pl-kos\&quot;>(</span><span class=\&quot;pl-s\&quot;>'positions'</span><span class=\&quot;pl-kos\&quot;>,</span> <span class=\&quot;pl-kos\&quot;>{</span>\n  <span class=\&quot;pl-c1\&quot;>query</span>: <span class=\&quot;pl-s\&quot;>`subscription { positions { operation data { id quantity } } }`</span><span class=\&quot;pl-kos\&quot;>,</span>\n  <span class=\&quot;pl-c1\&quot;>expectedState</span>: <span class=\&quot;pl-k\&quot;>new</span> <span class=\&quot;pl-v\&quot;>Map</span><span class=\&quot;pl-kos\&quot;>(</span><span class=\&quot;pl-kos\&quot;>[</span><span class=\&quot;pl-kos\&quot;>[</span><span class=\&quot;pl-c1\&quot;>1</span><span class=\&quot;pl-kos\&quot;>,</span> <span class=\&quot;pl-kos\&quot;>{</span> <span class=\&quot;pl-c1\&quot;>id</span>: <span class=\&quot;pl-c1\&quot;>1</span><span class=\&quot;pl-kos\&quot;>,</span> <span class=\&quot;pl-c1\&quot;>quantity</span>: <span class=\&quot;pl-c1\&quot;>100</span> <span class=\&quot;pl-kos\&quot;>}</span><span class=\&quot;pl-kos\&quot;>]</span><span class=\&quot;pl-kos\&quot;>]</span><span class=\&quot;pl-kos\&quot;>)</span><span class=\&quot;pl-kos\&quot;>,</span>\n<span class=\&quot;pl-kos\&quot;>}</span><span class=\&quot;pl-kos\&quot;>)</span><span class=\&quot;pl-kos\&quot;>;</span>\n\n<span class=\&quot;pl-k\&quot;>await</span> <span class=\&quot;pl-s1\&quot;>client</span><span class=\&quot;pl-kos\&quot;>.</span><span class=\&quot;pl-en\&quot;>trigger</span><span class=\&quot;pl-kos\&quot;>(</span><span class=\&quot;pl-s\&quot;>'loss-alert'</span><span class=\&quot;pl-kos\&quot;>,</span> <span class=\&quot;pl-kos\&quot;>{</span>\n  <span class=\&quot;pl-c1\&quot;>query</span>: <span class=\&quot;pl-s\&quot;>`mutation($url: String!) { create_trigger(name: \&quot;loss\&quot;, view: \&quot;positions\&quot;, ...) }`</span><span class=\&quot;pl-kos\&quot;>,</span>\n  <span class=\&quot;pl-c1\&quot;>expectedEvents</span>: <span class=\&quot;pl-kos\&quot;>[</span><span class=\&quot;pl-kos\&quot;>{</span> <span class=\&quot;pl-c1\&quot;>event_type</span>: <span class=\&quot;pl-s\&quot;>'FIRE'</span><span class=\&quot;pl-kos\&quot;>,</span> <span class=\&quot;pl-c1\&quot;>data</span>: <span class=\&quot;pl-kos\&quot;>{</span> <span class=\&quot;pl-c1\&quot;>id</span>: <span class=\&quot;pl-c1\&quot;>1</span> <span class=\&quot;pl-kos\&quot;>}</span> <span class=\&quot;pl-kos\&quot;>}</span><span class=\&quot;pl-kos\&quot;>]</span><span class=\&quot;pl-kos\&quot;>,</span>\n<span class=\&quot;pl-kos\&quot;>}</span><span class=\&quot;pl-kos\&quot;>)</span><span class=\&quot;pl-kos\&quot;>;</span>\n\n<span class=\&quot;pl-k\&quot;>await</span> <span class=\&quot;pl-s1\&quot;>testEnv</span><span class=\&quot;pl-kos\&quot;>.</span><span class=\&quot;pl-en\&quot;>waitForCompletion</span><span class=\&quot;pl-kos\&quot;>(</span><span class=\&quot;pl-kos\&quot;>)</span><span class=\&quot;pl-kos\&quot;>;</span></pre></div>\n</article>\n  </div>\n\n  </div>\n</div>\n\n      </div>\n      <div class=\&quot;gist-meta\&quot;>\n        <a href=\&quot;https://gist.github.com/chrismichaelanderson/92f78a88dc086cd511cce9190c8d3859/raw/1626b8e6736a6f8b62d71656c5bf82a65ed0d70c/test.md\&quot; style=\&quot;float:right\&quot; class=\&quot;Link--inTextBlock\&quot;>view raw</a>\n        <a href=\&quot;https://gist.github.com/chrismichaelanderson/92f78a88dc086cd511cce9190c8d3859#file-test-md\&quot; class=\&quot;Link--inTextBlock\&quot;>\n          test.md\n        </a>\n        hosted with &amp;#10084; by <a class=\&quot;Link--inTextBlock\&quot; href=\&quot;https://github.com\&quot;>GitHub</a>\n      </div>\n    </div>\n</div>\n&quot;,&quot;stylesheet&quot;:&quot;https://github.githubassets.com/assets/gist-embed-68783a026c0c.css&quot;}" data-component-name="GitgistToDOM"><link rel="stylesheet" href="https://github.githubassets.com/assets/gist-embed-68783a026c0c.css"><div id="gist144306254" class="gist">
    <div class="gist-file" data-color-mode="light" data-light-theme="light">
      <div class="gist-data">
        <div class="js-gist-file-update-container js-task-list-container">
  <div id="file-test-md" class="file my-2">
      <div id="file-test-md-readme" class="Box-body readme blob p-5 p-xl-6 " style="overflow:auto">
    <article class="markdown-body entry-content container-lg" itemprop="text"><div class="highlight highlight-source-ts"><pre><span class="pl-k">const</span> <span class="pl-s1">testEnv</span> <span class="pl-c1">=</span> <span class="pl-k">await</span> <span class="pl-v">TestEnvironment</span><span class="pl-kos">.</span><span class="pl-en">create</span><span class="pl-kos">(</span><span class="pl-kos">{</span>
  <span class="pl-c1">appPort</span>: <span class="pl-c1">4001</span><span class="pl-kos">,</span>
  <span class="pl-c1">schemaPath</span>: <span class="pl-s">'test/schema.yaml'</span><span class="pl-kos">,</span>
<span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-k">const</span> <span class="pl-s1">client</span> <span class="pl-c1">=</span> <span class="pl-s1">testEnv</span><span class="pl-kos">.</span><span class="pl-en">createClient</span><span class="pl-kos">(</span><span class="pl-s">'positions-client'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-k">await</span> <span class="pl-s1">client</span><span class="pl-kos">.</span><span class="pl-en">subscribe</span><span class="pl-kos">(</span><span class="pl-s">'positions'</span><span class="pl-kos">,</span> <span class="pl-kos">{</span>
  <span class="pl-c1">query</span>: <span class="pl-s">`subscription { positions { operation data { id quantity } } }`</span><span class="pl-kos">,</span>
  <span class="pl-c1">expectedState</span>: <span class="pl-k">new</span> <span class="pl-v">Map</span><span class="pl-kos">(</span><span class="pl-kos">[</span><span class="pl-kos">[</span><span class="pl-c1">1</span><span class="pl-kos">,</span> <span class="pl-kos">{</span> <span class="pl-c1">id</span>: <span class="pl-c1">1</span><span class="pl-kos">,</span> <span class="pl-c1">quantity</span>: <span class="pl-c1">100</span> <span class="pl-kos">}</span><span class="pl-kos">]</span><span class="pl-kos">]</span><span class="pl-kos">)</span><span class="pl-kos">,</span>
<span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-k">await</span> <span class="pl-s1">client</span><span class="pl-kos">.</span><span class="pl-en">trigger</span><span class="pl-kos">(</span><span class="pl-s">'loss-alert'</span><span class="pl-kos">,</span> <span class="pl-kos">{</span>
  <span class="pl-c1">query</span>: <span class="pl-s">`mutation($url: String!) { create_trigger(name: "loss", view: "positions", ...) }`</span><span class="pl-kos">,</span>
  <span class="pl-c1">expectedEvents</span>: <span class="pl-kos">[</span><span class="pl-kos">{</span> <span class="pl-c1">event_type</span>: <span class="pl-s">'FIRE'</span><span class="pl-kos">,</span> <span class="pl-c1">data</span>: <span class="pl-kos">{</span> <span class="pl-c1">id</span>: <span class="pl-c1">1</span> <span class="pl-kos">}</span> <span class="pl-kos">}</span><span class="pl-kos">]</span><span class="pl-kos">,</span>
<span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-k">await</span> <span class="pl-s1">testEnv</span><span class="pl-kos">.</span><span class="pl-en">waitForCompletion</span><span class="pl-kos">(</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
</article>
  </div>

  </div>
</div>

      </div>
      <div class="gist-meta">
        <a href="https://gist.github.com/chrismichaelanderson/92f78a88dc086cd511cce9190c8d3859/raw/1626b8e6736a6f8b62d71656c5bf82a65ed0d70c/test.md" style="float:right" class="Link--inTextBlock">view raw</a>
        <a href="https://gist.github.com/chrismichaelanderson/92f78a88dc086cd511cce9190c8d3859#file-test-md" class="Link--inTextBlock">
          test.md
        </a>
        hosted with &#10084; by <a class="Link--inTextBlock" href="https://github.com">GitHub</a>
      </div>
    </div>
</div>
</div><p>The test passes when all clients converge to their expected state, and fails if it times out or the liveness check detects that data has stopped flowing.</p><h2><strong>Why This Matters</strong></h2><p>In Part 1, I asked whether we&#8217;d see business logic pulled out of applications and centralized in streaming databases. Turns out there&#8217;s already a name for this pattern: the <a href="https://www.oreilly.com/library/view/streaming-data-mesh/9781098130718/">streaming data mesh</a>. The idea is that individual teams can publish well-defined, versioned &#8220;data products&#8221; that anyone can use, thereby creating internal APIs for live data.</p><p>I believe streaming databases like Materialize are the unlock for this pattern. They let developers join and transform live data using standard SQL, making it much easier to build scalable, real-time applications. And as demand for streaming databases grows, I expect we&#8217;ll need an ecosystem of tools around them &#8212; API layers for access, entitlements for security, and ways to trigger logic on certain conditions. This is where tycostream fits in.</p><p>Imagine vibe coding a positions dashboard in <a href="https://retool.com/">Retool</a>. Under the hood, it&#8217;s a simple case of connecting workflows and UI elements to live data products via GraphQL, making streaming applications as easy to build as CRUD.</p><h2><strong>Try It Out</strong></h2><p>Want to see tycostream in action? There&#8217;s a <a href="https://github.com/tycoworks/tycostream?tab=readme-ov-file#running-the-demo">positions dashboard demo</a> in the repo you can spin up locally with a single command. It uses a lightweight simulator to generate trades and market data, a Materialize emulator instance to calculate real-time positions as materialized views, and tycostream subscriptions to stream everything to the frontend. There&#8217;s also an alerts panel showing when realized losses exceed a threshold, implemented via tycostream triggers.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;b025e6f7-8665-4952-a02b-014dbe129bd9&quot;,&quot;duration&quot;:null}"></div><p>If you&#8217;re building real-time applications with SQL and GraphQL, I&#8217;d love to hear from you. What problems are you solving? What would make this useful for your use case? Drop me a line here or at <a href="mailto:chris@tycoworks.com">chris@tycoworks.com</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.tycoworks.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading tycoworks! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Can a Streaming Database Power Trading Applications?]]></title><description><![CDATA[Building a Real-Time Positions UI with Materialize]]></description><link>https://www.tycoworks.com/p/can-a-streaming-database-power-a</link><guid isPermaLink="false">https://www.tycoworks.com/p/can-a-streaming-database-power-a</guid><dc:creator><![CDATA[Chris]]></dc:creator><pubDate>Fri, 02 Jan 2026 17:22:39 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!R6-A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Updated November 2026: I reworked this experiment to use Kafka and Zilla instead of a custom Rust WebSocket relay, so the backend is composed entirely of off-the-shelf parts. I also swapped Perspective for AG Grid, to match later posts in this series.</em></p><p>I used to work at <a href="https://genesis.global/">Genesis</a>, a platform for rapidly building financial markets applications. The core idea behind the platform is simple: make it easier to build complex, real-time applications through familiar concepts like database tables and views. For example, to build a real-time view of stock positions, you could join a trades table with a market data table, calculate positions using an SQL-like <code>SUM()</code> function, and serve that to a UI with minimal code.</p><p>These ideas have been around for a while in capital markets, and are now becoming common in real-time data processing. Products like <a href="https://materialize.com/">Materialize</a> and <a href="https://risingwave.com/">RisingWave</a>, for example, let developers build live views over streaming data with Postgres-compatible SQL, kept up to date with millisecond-level latency. These &#8216;<a href="https://www.oreilly.com/library/view/streaming-databases/9781098154820/">streaming databases</a>&#8216; blur the line between databases and stream processors, and are gaining traction in areas like capital markets, manufacturing, and e-commerce.</p><p>This got me thinking recently about the trading desk of the future &#8212; what role might these technologies play, especially in the parts that don&#8217;t need ultra-low latency? Will we see business logic pulled out of applications and centralized in tools like these? Could they power a real-time financial markets application?</p><p>To explore this, I set out to build a positions view powered by a streaming database, to see what&#8217;s possible and where the friction lies.</p><h2><strong>Designing the Application</strong></h2><p>For the UI, I wanted a simple grid showing live positions, market value, and theoretical P&amp;L per instrument. It&#8217;s a small scope, but it covers many of the things you&#8217;d expect in a financial markets application, such as live and static data, real-time calculations, and ticking frontend updates. On the backend, I wanted to approximate a production setup &#8212; secure, scalable, and fast &#8212; but built with open standards and as little custom code as possible.</p><p>The high-level architecture was therefore simple: trades and instruments flowing from a Postgres instance; market prices piped in from a cloud WebSocket feed; a materialized view for the positions logic; and results streamed out to a grid in the browser.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R6-A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R6-A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 424w, https://substackcdn.com/image/fetch/$s_!R6-A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 848w, https://substackcdn.com/image/fetch/$s_!R6-A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 1272w, https://substackcdn.com/image/fetch/$s_!R6-A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R6-A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png" width="1456" height="368" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:368,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:125103,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.tycoworks.com/i/183244478?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R6-A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 424w, https://substackcdn.com/image/fetch/$s_!R6-A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 848w, https://substackcdn.com/image/fetch/$s_!R6-A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 1272w, https://substackcdn.com/image/fetch/$s_!R6-A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1bd8e331-e688-40f2-8a4e-81d77d61ed3d_1568x396.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">High-level target architecture</figcaption></figure></div><h2>Choosing the Streaming Database</h2><p>One of the first big decisions was picking a streaming database to power the backend. I went with Materialize because it ticked all the boxes: broad SQL support, streaming over a standard Postgres connection, and strong consistency guarantees. But this space is evolving fast, and there were other solid contenders.</p><h3><strong>Materialize</strong></h3><p><a href="https://materialize.com/">Materialize</a> positions itself as a 'live data layer' for apps and agents. It supports Postgres-compatible SQL, is built in Rust, and uses <a href="https://github.com/TimelyDataflow">Timely and Differential Dataflow</a> under the hood, enabling low latency and strong consistency across all views. You can query results on-demand or push updates out over Kafka or a Postgres connection, and it's easy to get started with their hosted offering and local development emulator.</p><h3><strong>RisingWave</strong></h3><p><a href="https://risingwave.com/">RisingWave</a> describes itself as a &#8216;real-time event streaming platform&#8217; and appears similar to Materialize at first glance - SQL-first, Rust-based, and designed to make stream processing feel like working with Postgres. It&#8217;s fully open source, built around open storage formats like Apache Iceberg, and has a high-performance &#8216;<a href="https://risingwave.com/risingwave-ultra/">Ultra</a>&#8216; edition designed for use cases in financial markets and online betting. Unlike Materialize however, it doesn&#8217;t guarantee strict consistency, meaning views can be temporarily out of sync.</p><h3><strong>ksqlDB</strong></h3><p><a href="https://ksqldb.io/">ksqlDB</a> is a streaming SQL engine for working with real-time data in Kafka. It supports SQL-like queries over Kafka topics, is source-available, and has built-in support for other Confluent products like Schema Registry. However, it uses its own SQL dialect rather than Postgres-compatible SQL, and <a href="https://docs.confluent.io/platform/current/ksqldb/overview.html">Confluent now recommends Flink</a> for new stream processing work.</p><h3><strong>Deephaven</strong></h3><p><a href="https://deephaven.io/">Deephaven</a> is a Python-based real-time engine originally built at a hedge fund, tailored for financial data workflows. It comes with its own UI server and scripting environment, so you can build full-stack UIs with just a few lines of Python. It&#8217;s powerful and polished, and would be a great choice if I were building this scope &#8220;for real.&#8221; But it&#8217;s a bit of an outlier &#8212; most streaming databases don&#8217;t bundle a UI server &#8212; so it didn&#8217;t feel like a good fit for this experiment.</p><h2><strong>Building the Data Layer</strong></h2><p>With the streaming database chosen, I spun up a Materialize emulator instance &#8212; a single-container version of Materialize with the same developer experience as a production setup. Then, I could plug in trades and instruments from Postgres and market prices from a live feed, and build the positions logic on top.</p><h3>Trades and Instruments</h3><p>For trades and instruments, I used <a href="https://neon.com/">Neon</a> &#8212; a managed Postgres service that&#8217;s quick to set up and has a free tier. I used Claude to generate the tables and some fake data, and created a &#8216;publication&#8217; so Materialize could subscribe to updates in real-time. I registered Neon as a source in Materialize, and the data was live.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ep1X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ep1X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 424w, https://substackcdn.com/image/fetch/$s_!ep1X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 848w, https://substackcdn.com/image/fetch/$s_!ep1X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 1272w, https://substackcdn.com/image/fetch/$s_!ep1X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!ep1X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png" width="1456" height="709" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a38e8c17-2600-43f4-880f-555ea063e021_1919x934.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:709,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:148478,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.tycoworks.com/i/165572415?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ep1X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 424w, https://substackcdn.com/image/fetch/$s_!ep1X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 848w, https://substackcdn.com/image/fetch/$s_!ep1X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 1272w, https://substackcdn.com/image/fetch/$s_!ep1X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa38e8c17-2600-43f4-880f-555ea063e021_1919x934.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Querying data in Neon</figcaption></figure></div><h3>Market Data</h3><p>To keep things realistic, I wanted to use real market data delivered over WebSockets. Claude suggested a few great options &#8212; like <a href="https://databento.com/">Databento</a> (great for latency-sensitive use cases) and <a href="https://polygon.io/">Polygon</a> (broad instrument coverage) &#8212; but I decided to go with <a href="https://alpaca.markets/">Alpaca</a> for its free tier and clean API.</p><p>That&#8217;s when I realized I&#8217;d missed something: Materialize doesn&#8217;t support WebSockets as a source. Oops. I could&#8217;ve switched streaming database (to e.g. Deephaven) or written a custom process to transform the data, but I wanted to avoid changing stack or adding glue code.</p><p>That&#8217;s when I found <a href="https://estuary.dev/">Estuary Flow</a>: a real-time ETL tool with native support for both Alpaca and Materialize. Within a few minutes, I&#8217;d configured Estuary&#8217;s Alpaca connector for the IEX trades stream, mapped it to a Kafka topic, and registered it as a source in Materialize. Live prices were streaming in.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NGaM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NGaM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 424w, https://substackcdn.com/image/fetch/$s_!NGaM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 848w, https://substackcdn.com/image/fetch/$s_!NGaM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 1272w, https://substackcdn.com/image/fetch/$s_!NGaM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!NGaM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png" width="1456" height="709" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:709,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:185150,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.tycoworks.com/i/165572415?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NGaM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 424w, https://substackcdn.com/image/fetch/$s_!NGaM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 848w, https://substackcdn.com/image/fetch/$s_!NGaM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 1272w, https://substackcdn.com/image/fetch/$s_!NGaM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59751353-6dc6-4de3-bb3d-cd1db2257228_1919x934.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Market data flowing into Estuary</figcaption></figure></div><h3>Positions</h3><p>The final step was to pull trades, instruments, and market prices together into a live positions view. I had Claude generate two materialized views: one to surface the latest trade price per instrument, and another to join everything together, aggregate trades into positions, and calculate market value and P&amp;L.</p><p>I ran <code>SELECT * FROM live_pnl;</code> and, after a short &#8216;hydration&#8217; period, watched my positions update in real time as trades and prices streamed in.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!84C6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!84C6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 424w, https://substackcdn.com/image/fetch/$s_!84C6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 848w, https://substackcdn.com/image/fetch/$s_!84C6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 1272w, https://substackcdn.com/image/fetch/$s_!84C6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!84C6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png" width="1456" height="709" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:709,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:172248,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.tycoworks.com/i/165572415?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!84C6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 424w, https://substackcdn.com/image/fetch/$s_!84C6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 848w, https://substackcdn.com/image/fetch/$s_!84C6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 1272w, https://substackcdn.com/image/fetch/$s_!84C6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad7c7f7e-cbac-4a0d-bddc-74d3230eb3c5_1920x935.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Streaming workflow in Materialize</figcaption></figure></div><p>So far so good! I had a real-time view of positions, aggregated from trades and live prices, with just a few lines of SQL.</p><h2><strong>Getting Data to the Browser</strong></h2><p>The next step was getting data from Materialize to a UI in real time, with minimal code. I began by looking into whether anyone had done this before, and what tools they&#8217;d used to make it work.</p><p>I found surprisingly little. There was a <a href="https://grafana.com/grafana/plugins/bsull-materialize-datasource/">Materialize plugin for Grafana</a>, which wasn&#8217;t a good fit given Grafana&#8217;s focus on monitoring. Some people were piping views to database tables to feed Streamlit or Metabase dashboards, which wasn&#8217;t truly real-time. But most commonly, people were building their own lightweight processes to stream data over WebSockets, like <a href="https://dev.to/bobbyiliev/building-a-live-chart-with-deno-websockets-chartjs-and-materialize-3cd7">this blog post</a> using Deno and Chart.js.</p><p>So I started looking more broadly. Were there other approaches that could work for my use case?</p><h3><strong>REST and GraphQL</strong></h3><p>First I explored exposing Materialize over REST or GraphQL, both common APIs for front-end data access. REST was a non-starter &#8212; it isn&#8217;t built for real-time &#8212; but GraphQL supports subscriptions and is well supported by most frontend frameworks.</p><p>That led me to <a href="https://hasura.io/">Hasura</a>, a data access layer for Postgres. It exposes your database as a GraphQL API with minimal setup, featuring built-in filtering, joining, and access control. Since Materialize is Postgres-compatible it seemed like a perfect fit, but Hasura polls periodically rather than streaming changes in real time, which wouldn&#8217;t give me the tick-by-tick updates I needed.</p><p>I also looked at <a href="https://www.apollographql.com/">Apollo</a>, a more flexible way to build GraphQL APIs over any data source. It looked promising, but I&#8217;d still have needed to build an integration for Materialize. Since the main goal was to get streaming data into a UI with minimal effort, I didn&#8217;t want to add another platform and a bunch of glue code.</p><h3><strong>View Servers</strong></h3><p>Next I explored view servers, which are common in many financial markets applications. These components typically stream data from a data source to frontends over WebSockets, with built-in rate limiting and access control. Genesis and Deephaven, for example, both feature UI servers as part of their larger platforms (e.g. <a href="https://docs.genesis.global/docs/develop/server-capabilities/real-time-queries-data-server/">Genesis&#8217; Data Server</a>), but these aren&#8217;t available as standalone components.</p><p><a href="https://vuu.finos.org/">FinOS Vuu</a> was promising as an open source project designed for fast-moving data and many connected users. While it seemed straightforward to integrate and get running, I would&#8217;ve also needed to adopt its built-in grid, whereas I was more looking to compose a solution from standalone components.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pFIf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pFIf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 424w, https://substackcdn.com/image/fetch/$s_!pFIf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 848w, https://substackcdn.com/image/fetch/$s_!pFIf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 1272w, https://substackcdn.com/image/fetch/$s_!pFIf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!pFIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png" width="803" height="360" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:360,&quot;width&quot;:803,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:31980,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://tycoworks.substack.com/i/183244593?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!pFIf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 424w, https://substackcdn.com/image/fetch/$s_!pFIf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 848w, https://substackcdn.com/image/fetch/$s_!pFIf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 1272w, https://substackcdn.com/image/fetch/$s_!pFIf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F16066cb2-ab22-45c1-bc3d-89f323e36c17_803x360.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Vuu architecture diagram from the <a href="https://vuu.finos.org/desktop/docs/introduction/how_does_it_work/">documentation</a>. Copyright &#169; 2025 VUU - UBS.</figcaption></figure></div><h3><strong>Streaming API Gateways</strong></h3><p>Finally I looked at streaming API gateways, which serve Kafka topics over standard web protocols. The idea was to stream the positions view out through a Kafka sink, so a gateway could cache the latest rows and push them to any number of connected UIs without adding load on Materialize.</p><p>I started with <a href="https://lightstreamer.com/">Lightstreamer</a>, a push server widely used for real-time apps in finance, gaming, and aerospace. It&#8217;s built for high-volume, low-latency data, with an open-source Kafka connector and throttling for slow clients. However, it uses its own protocol and client library, whereas I was looking to use open standards. I also looked at <a href="https://www.gravitee.io/">Gravitee</a>, a full API management platform with its own console and database, but its Kafka support is only in the paid Enterprise edition.</p><p>Then I came across <a href="https://github.com/aklivity/zilla">Zilla</a>, a lightweight gateway that&#8217;s quick to set up, with everything defined in a single YAML file. It was a great fit, since it supports open standards like server-sent events (SSE), gRPC, and MQTT, and has authentication, row-level entitlements, and filtering available in its community edition. I configured Zilla to read positions from a compacted Kafka topic and stream them over SSE, ready for the UI to pick up.</p><h2><strong>Building the UI</strong></h2><p>Since the UI was simple &#8212; just a grid connected to an SSE stream &#8212; I went with plain HTML and JavaScript. I used <a href="https://vite.dev/">Vite</a> to build and serve it locally, so I could see changes instantly in the browser as I iterated.</p><p>For the grid, I needed something that could handle large, streaming datasets. I chose <a href="https://www.ag-grid.com/">AG Grid</a> as it had worked well for similar applications at Genesis, given its support for <a href="https://www.ag-grid.com/javascript-data-grid/dom-virtualisation/">row virtualization</a> and <a href="https://www.ag-grid.com/javascript-data-grid/data-update-transactions/">transaction updates</a>.</p><p>To get data into the grid, I subscribed to Zilla&#8217;s SSE stream using the standard <code>EventSource</code> API. As events arrived, the UI determined whether each was an insert, update, or delete, and applied it to AG Grid as a transaction. You can find the code on GitHub: <a href="https://github.com/tycoworks/positions-app/tree/v1">tycoworks/positions-app</a>.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;a576bdbd-9fc6-4e5f-8b08-35c510b43e20&quot;,&quot;duration&quot;:null}"></div><h2><strong>Conclusion</strong></h2><p>So, can a streaming database power a real-time financial markets application? I believe so. I built a real-time positions UI with Materialize at its core, with about 30 lines of SQL and 120 of JavaScript. And since it used proven, off-the-shelf parts, there was a clear path to production, even if I hadn&#8217;t optimized for latency, scale, or security.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ECNa!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 424w, https://substackcdn.com/image/fetch/$s_!ECNa!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 848w, https://substackcdn.com/image/fetch/$s_!ECNa!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 1272w, https://substackcdn.com/image/fetch/$s_!ECNa!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!ECNa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png" width="1456" height="197" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20321e18-1819-463f-819d-ffa654b59d89_1568x212.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:197,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:92400,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.tycoworks.com/i/183244478?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ECNa!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 424w, https://substackcdn.com/image/fetch/$s_!ECNa!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 848w, https://substackcdn.com/image/fetch/$s_!ECNa!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 1272w, https://substackcdn.com/image/fetch/$s_!ECNa!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20321e18-1819-463f-819d-ffa654b59d89_1568x212.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div><figcaption class="image-caption">Final architecture</figcaption></figure></div><p>While getting data into Materialize was straightforward, getting data out was fiddlier than expected. Zilla itself was easy to get up and running, <a href="https://www.aklivity.io/post/how-zilla-works">designed for performance</a>, and had entitlements, filtering, and scalability built in. However, going through Kafka meant defining every view up front in Materialize, whereas many applications need arbitrary queries and joins on the fly.</p><p>Is there a product here? Something based on GraphQL, like Hasura but for streaming databases? Is there demand for streaming data into UIs, or is the bigger opportunity in powering AI agents? That&#8217;s what I&#8217;ll <a href="https://www.tycoworks.com/p/tycostream-turn-materialize-views">look at next</a>.</p>]]></content:encoded></item></channel></rss>