<?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 marketing-as-code, real-time infrastructure, 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>Thu, 16 Apr 2026 07:26:06 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[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><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!98qw!,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" 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="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="100vw" loading="lazy"></picture><div></div></div></a><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 a Trading Desk UI? (Part 2)]]></title><description><![CDATA[Getting Real-Time Data to the UI]]></description><link>https://www.tycoworks.com/p/can-a-streaming-database-power-a-f49</link><guid isPermaLink="false">https://www.tycoworks.com/p/can-a-streaming-database-power-a-f49</guid><dc:creator><![CDATA[Chris]]></dc:creator><pubDate>Fri, 02 Jan 2026 18:08:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d6049557-c333-47e0-8b84-b647fcf1158b_1456x1048.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><p>This post continues from <a href="https://www.tycoworks.com/p/can-a-stream-processor-power-a-trading">Part One</a>, where I built a real-time core for a positions UI using Materialize. Trades, instruments, and market data were flowing into a live view of quantity, market value, and theoretical P&amp;L, with millisecond-level latency. The next step was getting that data into a UI in real time, with minimal code and infrastructure.</p><h2>Exploring the Options</h2><p>As in Part One, the goal was to build the UI with as little custom code as possible. 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'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 using Deno and Chart.js</a>.</p><p>So I started looking more broadly. Were there other approaches that could work for my use case?</p><h3>Low-Code Platforms and Admin Panel Builders</h3><p>I started with low-code platforms like <a href="https://retool.com/">Retool</a>. Retool excels at creating UIs and workflows on top of APIs and data sources, so in theory it felt like a good fit. But it doesn't support real-time subscriptions &#8212; only polling or manual refresh.</p><p>I also looked briefly at <a href="https://refine.dev/">Refine</a>. It&#8217;s a more developer-oriented React framework for building admin panels with strong Postgres support. While it does support real-time data, I would have needed to build a custom Materialize integration, so it wouldn&#8217;t have saved any coding.</p><p>There are many other similar low-code platforms and admin panel builders, but I couldn't find any with true, off-the-shelf real-time support. </p><h3>REST and GraphQL</h3><p>I also 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'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>View Servers</h3><p>Finally I explored view servers, which are common in many financial markets applications. These components typically stream data 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="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;: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/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="" 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="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"><svg role="img" 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"><g><title></title><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"><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"><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><h2>Implementation</h2><h3>Overview</h3><p>Having found no plug-and-play options, it was time to build. I decided to follow the approach outlined in the Deno + Chart.js example: a backend process subscribing to Materialize, pushing updates over WebSockets to a grid. Another option was to use a Materialize sink, but that would&#8217;ve needed a Kafka cluster and made it harder to send data to the frontend.</p><p>As I hadn&#8217;t coded in over a decade, I was going to need some help. I wanted to treat AI as if it were a developer colleague &#8212; I&#8217;d provide the spec, and it would generate and update the code as the spec evolved. This '<a href="https://ainativedev.io/">AI-native development</a>' approach &#8212; championed by startups like <a href="https://tessl.io/">Tessl</a> &#8212; is gaining ground as a more deterministic alternative to vibe coding. I worked with Claude to write detailed specifications, and used Cursor to implement the code.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ll5e!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ll5e!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 424w, https://substackcdn.com/image/fetch/$s_!ll5e!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 848w, https://substackcdn.com/image/fetch/$s_!ll5e!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 1272w, https://substackcdn.com/image/fetch/$s_!ll5e!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ll5e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png" width="842" height="595" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:842,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ll5e!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 424w, https://substackcdn.com/image/fetch/$s_!ll5e!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 848w, https://substackcdn.com/image/fetch/$s_!ll5e!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 1272w, https://substackcdn.com/image/fetch/$s_!ll5e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ede4301-1ba6-4fe3-8609-3adcf7761a52_842x595.png 1456w" sizes="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"><svg role="img" 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"><g><title></title><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"><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"><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 AI native development landscape</figcaption></figure></div><h3>Backend</h3><p>I chose Rust for the backend relay. It's fast, memory-safe, and widely used for low-level infrastructure &#8212; including Materialize itself. Deno was another solid option, but I&#8217;d already seen that path explored and wanted to try something different.</p><p>Next, I used Claude to create a detailed specification for Cursor to work from, covering the context, architecture, and requirements. Cursor implemented the code using <a href="https://tokio.rs/">tokio</a> and, after a few issues with authentication and connection handling, got everything working. I&#8217;m no stranger to vibe coding, but watching Cursor write code and fix bugs on the fly still felt like magic.</p><p>Once up and running locally, I connected to the relay with <code>wscat</code> and saw real-time data flowing into the terminal. The data refreshed as the market moved and as new trades were inserted, meaning everything was in place for the UI.</p><p>You can find the backend code on GitHub: <a href="https://github.com/tycoworks/rust-relay">tycoworks/rust-relay</a>.</p><h3>Frontend</h3><p>Since the UI was simple &#8212; just a grid connected to a WebSocket &#8212; I went with plain HTML and JavaScript. I used <a href="https://vite.dev/">Vite</a> to build and test the app locally, which automatically detected file changes and refreshed the browser as I worked.</p><p>For the grid, I needed something that could handle large, streaming datasets. Two options stood out: <a href="https://www.ag-grid.com/">AG Grid</a>, a high-performance data grid which we used at Genesis; and <a href="https://perspective.finos.org/">Perspective</a>, a grid and charting component compiled to WebAssembly. AG Grid would&#8217;ve been a perfect fit, but I went with Perspective to try something new, and because I was curious about its built-in analytics features.</p><p>As with the backend, I created a specification file describing the UI layout and requirements, but implementation was a little trickier. Cursor struggled with CDN imports, WebAssembly loading, and even tried to swap out Perspective for another grid component a couple of times. After nearly 30 iterations and some manual debugging in the browser, we finally got Perspective up and running &#8212; but the grid was empty.</p><p>After a bit of debugging, I realized the problem was with the relay. It was correctly forwarding <em>updates</em> from Materialize, but wasn&#8217;t sending the <em>initial</em> state of the data when the frontend connected. This turned out to be a gap in the spec, so I asked Cursor to add it and regenerate the code. After a couple of iterations on the snapshot logic, I had a live positions view ticking with the market.</p><p>Frontend code is available here: <a href="https://github.com/tycoworks/ui-grid">tycoworks/ui-grid</a>.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;61fa4979-c44c-4aca-904f-3195591be690&quot;,&quot;duration&quot;:null}"></div><h2>Conclusion</h2><p>So, can a streaming database power a real-time financial markets UI? I believe so. I built a real-time positions UI with Materialize at its core and, although updates ticked a little more slowly than I expected, I suspect that was due to things like using trades instead of BBO, the Estuary bridge, or running with stock Materialize config.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XapR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XapR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 424w, https://substackcdn.com/image/fetch/$s_!XapR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 848w, https://substackcdn.com/image/fetch/$s_!XapR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 1272w, https://substackcdn.com/image/fetch/$s_!XapR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XapR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png" width="1456" height="354" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:354,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:207503,&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/166119478?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.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_!XapR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 424w, https://substackcdn.com/image/fetch/$s_!XapR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 848w, https://substackcdn.com/image/fetch/$s_!XapR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 1272w, https://substackcdn.com/image/fetch/$s_!XapR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdca057e8-db0e-4b1e-8997-279a7e554f24_2990x726.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Final architecture</figcaption></figure></div><p>While getting data into Materialize was a little fiddly, getting data out was the bigger surprise. Most people seemed to be creating custom processes to stream data into the browser, with no clear off-the-shelf option. It feels like there should be a better way &#8212; especially if you also need entitlements, filtering, and scalability.</p><p>Is there a product here? A "Hasura for streaming databases", providing streaming data over GraphQL and WebSockets? Is there demand for streaming data into UIs, or is the bigger opportunity in powering AI agents? <a href="https://tycoworks.substack.com/p/tycostream-turn-materialize-views">That's what I'll look at next</a>.</p>]]></content:encoded></item><item><title><![CDATA[Can a Streaming Database Power a Trading Desk UI? (Part 1)]]></title><description><![CDATA[Building a Real-Time Core 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_!XcZh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<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>&#8217; 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 UI?</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>Specification and Setup</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'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 without glue code or heavy infrastructure.</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_!XcZh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XcZh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 424w, https://substackcdn.com/image/fetch/$s_!XcZh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 848w, https://substackcdn.com/image/fetch/$s_!XcZh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 1272w, https://substackcdn.com/image/fetch/$s_!XcZh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XcZh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png" width="1456" height="543" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:543,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:101418,&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/165572415?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.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_!XcZh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 424w, https://substackcdn.com/image/fetch/$s_!XcZh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 848w, https://substackcdn.com/image/fetch/$s_!XcZh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.png 1272w, https://substackcdn.com/image/fetch/$s_!XcZh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7921cb92-24a2-451c-8043-921cd2bb778d_1945x726.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"><svg role="img" 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"><g><title></title><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"><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"><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 'real-time event streaming platform' and appears similar to Materialize at first glance - SQL-first, Rust-based, and designed to make stream processing feel like working with Postgres. It's fully open source, built around open storage formats like Apache Iceberg, and has a high-performance '<a href="https://risingwave.com/risingwave-ultra/">Ultra</a>' edition designed for use cases in financial markets and online betting. Unlike Materialize however, it doesn'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 open source, and has built-in support for other Confluent products like Schema Registry. However, it requires a running Kafka cluster and uses a proprietary SQL dialect &#8212; both of which I was looking to avoid.</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>Wiring It All Up</h2><p>With the streaming database chosen, I spun up a Materialize Cloud instance &#8212; a managed service that&#8217;s secure, scalable, and production-ready out of the box. Then, I needed data: trades and instruments from Postgres, and market prices from a live feed.</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="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="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"><svg role="img" 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"><g><title></title><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"><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"><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="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="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"><svg role="img" 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"><g><title></title><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"><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"><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="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="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"><svg role="img" 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"><g><title></title><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"><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"><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><h2>Wrapping Up</h2><p>So far so good. I had a real-time view of positions, fed from live market data and a Postgres database, without any glue code. And since it ran on production-grade managed services, it didn&#8217;t feel far off a real setup, even if I hadn&#8217;t optimized for latency, scale, or security.</p><p>The next question: can this view drive the UI, and how easy is that in practice? That&#8217;s the focus of <a href="https://tycoworks.substack.com/p/can-a-streaming-database-power-a-f49">Part Two</a>.</p>]]></content:encoded></item></channel></rss>