<?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:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Dominic Bachmann on Medium]]></title>
        <description><![CDATA[Stories by Dominic Bachmann on Medium]]></description>
        <link>https://medium.com/@dominicbachmann?source=rss-2c2e1b14b6f0------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*cze_0Ay4ZzQSyKFa_RjbHA.jpeg</url>
            <title>Stories by Dominic Bachmann on Medium</title>
            <link>https://medium.com/@dominicbachmann?source=rss-2c2e1b14b6f0------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 06:10:59 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@dominicbachmann/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Building an AI Assistant Into a Web App — What I Learned and Where I Think This Is Going]]></title>
            <link>https://medium.com/@dominicbachmann/building-an-ai-assistant-into-a-web-app-what-i-learned-and-where-i-think-this-is-going-8d8642a9623c?source=rss-2c2e1b14b6f0------2</link>
            <guid isPermaLink="false">https://medium.com/p/8d8642a9623c</guid>
            <category><![CDATA[ai-assistant]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[frontend-development]]></category>
            <category><![CDATA[angular]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <dc:creator><![CDATA[Dominic Bachmann]]></dc:creator>
            <pubDate>Sun, 29 Mar 2026 18:53:36 GMT</pubDate>
            <atom:updated>2026-03-29T20:12:22.993Z</atom:updated>
            <content:encoded><![CDATA[<p>I’ve been experimenting with something that I believe will become the standard for how we build applications: embedding a capable AI assistant directly into a web app. Not a chatbot that answers FAQs. Not a search bar with slightly better autocomplete. A real assistant that understands the app, can pull data, navigate pages, and render actual UI — all through natural conversation.</p><p>To test this idea, I threw together a small proof of concept — a simple personal finance app with an AI chat layer on top. The app itself isn’t the point. What’s interesting are the patterns that emerged and what they suggest about where application development is heading.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*MhzQeaNNGcT05vFPXc0rWg.png" /></figure><h3>The Idea</h3><p>The core question I wanted to explore was: <em>What happens when you give an LLM full access to your application’s functionality?</em></p><p>Most AI integrations I’ve seen treat the assistant as something separate from the app — a sidebar that can answer questions but can’t actually <em>do</em> anything. I wanted the opposite. I wanted the AI to be a first-class citizen that can do everything a user can do through the traditional UI, and potentially even more.</p><p>So I spun up a quick POC — a finance tracker with the usual pages (accounts, transactions, budgets, etc.) — and wired an AI chat layer into every feature. The app took a single prompt to scaffold. The interesting part was what came next: giving the assistant real tools to work with.</p><h3>The Patterns That Emerged</h3><p>Once you commit to giving the assistant real tools — actual functions that interact with the app’s services and database — a few powerful patterns emerge.</p><h3>Navigation as Conversation</h3><p>The simplest tool: letting the assistant navigate the user to any page. Say “take me to my budgets” and it routes you there. This sounds trivial, but it changes how you think about app navigation entirely. Instead of hunting through menus, you just say where you want to go. Now imagine this in an app with dozens or hundreds of pages — the value scales fast.</p><p>Here’s what defining a navigation tool looks like in practice:</p><pre>const PAGE_ROUTES = {<br>  dashboard: [&#39;/dashboard&#39;],<br>  accounts: [&#39;/accounts&#39;],<br>  transactions: [&#39;/transactions&#39;],<br>  budgets: [&#39;/budgets&#39;],<br>  // ...<br>};<br><br>const navigateToPage = createTool({<br>  name: &#39;navigate_to_page&#39;,<br>  description: &#39;Navigate to a page in the app.&#39;,<br>  schema: s.object({<br>    page: s.string(&#39;Page name to navigate to&#39;),<br>  }),<br>  handler: async (input) =&gt; {<br>    const route = PAGE_ROUTES[input.page];<br>    if (route) await router.navigate(route);<br>    return { navigated: !!route, page: input.page };<br>  },<br>});</pre><p>That’s it. A route map, a name, a description, and a handler. The LLM reads the description, decides when to call it, and fills in the parameters.</p><h3>Data Retrieval With Generative UI</h3><p>An assistant that can only respond with text is limited. The real unlock is giving it the ability to render actual UI components inline in the chat — cards, tables, progress bars, charts. The LLM decides which component fits best for what the user asked and renders it with real data.</p><p>In my POC, asking “what’s my net worth?” returns a formatted account summary with balances. “Show me my spending this month” renders a doughnut chart by category. The responses feel native to the app because they <em>are</em> native — they’re the same frontend components, just rendered in the chat context. This is a pattern that works for any domain: dashboards, admin panels, analytics tools, CRMs — anywhere you’re presenting structured data.</p><p>You expose your existing components to the LLM with typed schemas that describe what each one does and what data it needs:</p><pre>exposeComponent(ChatAccountCardComponent, {<br>  description: &#39;Shows a single account with name, type, balance, and icon.&#39;,<br>  input: {<br>    name: s.string(&#39;account name&#39;),<br>    type: s.string(&#39;account type like checking, savings&#39;),<br>    balance: s.number(&#39;current balance in USD&#39;),<br>    color: s.string(&#39;hex color&#39;),<br>    icon: s.string(&#39;material icon name&#39;),<br>  },<br>}),<br><br>exposeComponent(ChatSpendingChartComponent, {<br>  description: &#39;Shows a doughnut or bar chart for spending data.&#39;,<br>  input: {<br>    chartType: s.enumeration(&#39;chart type&#39;, [&#39;bar&#39;, &#39;doughnut&#39;]),<br>    labels: s.array(&#39;chart labels&#39;, s.string(&#39;label&#39;)),<br>    data: s.array(&#39;data values&#39;, s.number(&#39;value&#39;)),<br>    colors: s.array(&#39;hex colors&#39;, s.string(&#39;color&#39;)),<br>    title: s.string(&#39;chart title&#39;),<br>  },<br>}),</pre><p>The LLM picks the right component for each response and fills in the props with real data. Ask about your net worth — it renders an account card. Ask about spending — it renders a chart. You don’t code that routing logic. The LLM just figures it out from the descriptions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3XSHbtMUTtCmnxel9pwiKw.png" /></figure><h3>Dynamic Database Queries — The Most Interesting Part</h3><p>Here’s what I find truly fascinating. Beyond pre-defined tools like “get accounts” or “search transactions,” I gave the assistant a generic query_database tool. It knows the database schema — tables, columns, relationships, supported filter operators — and it can construct whatever query it needs to answer the user&#39;s question.</p><p>This means the assistant isn’t limited to the specific queries I anticipated when building the app. It can join tables, apply complex filters, sort and limit results — whatever the question requires. “Show me all expenses over $100 from the last six months” isn’t a feature I explicitly built. The LLM figures out the right query, executes it, and presents the results.</p><p>The tool definition gives the LLM full schema awareness — table structures, relationships, and filter operators:</p><pre>const SCHEMA_DESCRIPTION = `<br>DATABASE SCHEMA:<br>- accounts (id, name, type[checking|savings|credit_card|...], balance, ...)<br>- transactions (id, account_id FK-&gt;accounts, category_id FK-&gt;categories,<br>    type[income|expense|transfer], amount, description, date, ...)<br>- budgets (id, category_id FK-&gt;categories, amount, period, ...)<br><br>RELATIONSHIPS:<br>- transactions.account_id -&gt; accounts<br>- transactions.category_id -&gt; categories<br>- budgets.category_id -&gt; categories<br>FILTER OPERATORS: eq, neq, gt, gte, lt, lte, like, ilike, in, is`;<br><br>const queryDatabase = createTool({<br>  name: &#39;query_database&#39;,<br>  description: SCHEMA_DESCRIPTION,<br>  schema: s.object({<br>    table: s.enumeration(&#39;Table&#39;, [&#39;accounts&#39;, &#39;transactions&#39;, &#39;budgets&#39;, ...]),<br>    select: s.string(&#39;Select string with join syntax&#39;),<br>    filters: s.array(&#39;Filter conditions&#39;, filterSchema),<br>    order: orderSchema,<br>    limit: s.number(&#39;Max rows to return&#39;),<br>  }),<br>  handler: async (input) =&gt; {<br>    let query = supabase.from(input.table).select(input.select);<br>    for (const filter of input.filters) {<br>      query = query[filter.operator](filter.column, filter.value);<br>    }<br>    if (input.order) {<br>      query = query.order(input.order.column, { ascending: input.order.ascending });<br>    }<br>    const { data } = await query.limit(input.limit ?? 50);<br>    return data;<br>  },<br>});</pre><p>That’s one tool. And it replaces what would otherwise be dozens of filter combinations, custom report builders, and specific views in a traditional UI.</p><p>This is the pattern that made me rethink what an in-app AI assistant really is. It’s not just a nicer interface to existing features. It’s a way to unlock functionality that would be impractical to build as traditional UI. You’d need dozens of filter combinations, custom report builders, and specific views to cover what the LLM can do with a single flexible tool. Give it schema awareness and query access, and it can generate whatever the user asks for — no one has to anticipate every possible question upfront.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pMXNMvogCt3K825U2drLQg.png" /></figure><h3>Why Complexity Makes This More Valuable</h3><p>One thing that struck me during this experiment: the more complex an app becomes, the more valuable an embedded AI assistant gets.</p><p>A simple app with three screens doesn’t really need an assistant — the UI is already easy to navigate. But think about software with hundreds of features, nested settings, data spread across dozens of views. An AI assistant that knows the entire app — every page, every data source, every capability — becomes incredibly powerful in that context. It’s like having a power user sitting next to you who knows every shortcut and every hidden feature.</p><p>And it scales in a way traditional UI doesn’t. Every new feature you add to the app automatically becomes something the assistant can help with, without building new onboarding flows or help documentation. The assistant <em>is</em> the documentation. It <em>is</em> the onboarding.</p><h3>Accessibility — The Angle That Excites Me Most</h3><p>There’s an aspect of this that I think deserves much more attention: accessibility.</p><p>When you can fully interact with an application through natural conversation, you fundamentally change who can use it. This is convenient for everyone — nobody <em>loves</em> clicking through nested menus to find a specific setting. But for some people, it’s transformative.</p><p>Think about users who aren’t technically versed. The kind of people who call their kids to help them with an app. An AI assistant that they can just <em>talk to</em> and say “show me how much I spent on groceries last month” removes the barrier entirely. They don’t need to understand filters, date pickers, or category dropdowns. They just ask.</p><p>And for users with disabilities — motor impairments that make clicking difficult, visual impairments that make navigating complex UIs challenging — a conversational interface isn’t just convenient. It’s enabling. It means they can use the app at all, or use it without the workarounds they’re currently forced into.</p><p>I think this is one of the most underappreciated benefits of in-app AI assistants. We talk a lot about productivity gains and developer experience, but the accessibility implications are enormous.</p><p>And here’s the kicker for developers: this accessibility comes almost for free. Traditional a11y is notoriously hard to do well — ARIA attributes, screen reader testing, keyboard navigation, focus management, color contrast, alternative text. It’s a massive effort that many teams struggle to prioritize. But a conversational interface sidesteps most of that complexity. If your assistant can already fetch data and perform actions, you’ve given every user — regardless of ability — a fully accessible way to interact with your app, with virtually no additional development cost.</p><h3>What I Want to Try Next</h3><p>The dynamic query tool was the most interesting part of this experiment, and I want to push that idea much further. The next step I have in mind: combine the same flexible data fetching with a rich library of generative UI building blocks — cards, tables, charts, layouts, grids — and let the LLM compose entire pages on the fly.</p><p>Right now the assistant renders individual components in the chat. But what if it could assemble a full dashboard view from a single request? “Build me a page that shows my monthly spending trend, my top five expense categories, and my upcoming bills” — and the LLM picks the right components, arranges them in a layout, pulls the data, and renders a complete view.</p><p>The part that really interests me: letting users save these generated queries and views. At that point, the user is essentially building their own application within a framework of data and components — without writing code, without navigating a complex UI builder. They just describe what they want, refine it through conversation, and keep what works.</p><h3>Where I Think This Is Going</h3><p>I’m genuinely convinced that in the near future, most applications will ship with a capable on-page AI assistant. Not as a gimmick or a marketing checkbox, but as a core part of the user experience.</p><p>The technology is already there. LLMs can call tools, render UI, maintain context across a conversation, and handle nuanced requests. The patterns for integrating them into frontend frameworks are emerging. What’s left is mostly execution and refinement — making it reliable, fast, and natural enough that users reach for the chat before they reach for the menu.</p><p>When that happens, the way we design applications will shift. We’ll spend less time optimizing navigation hierarchies and more time defining what tools and data the assistant has access to. The UI won’t go away — visual interfaces are still the best way to browse and explore — but the assistant will become the primary way users <em>act</em> on the app. Find this, show me that, create this, take me there.</p><p>I think it will make our lives easier and more efficient. And for a lot of people, it will make software accessible in a way it never was before.</p><p>I’m very curious to see which direction this takes. But if this small experiment is any indication, the direction is promising.</p><p><em>The POC is built with Angular, Supabase, and Claude, using </em><a href="https://hashbrown.dev/"><em>hashbrown.dev</em></a><em> for AI tool calling and generative UI. Check out the </em><a href="https://github.com/dominicbachmann/finance-app-with-ai-chat-assistant"><em>source code on GitHub</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8d8642a9623c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Protecting against cross-site scripting with trusted types]]></title>
            <link>https://medium.com/@dominicbachmann/protecting-against-cross-site-scripting-with-trusted-types-94f0b3c32129?source=rss-2c2e1b14b6f0------2</link>
            <guid isPermaLink="false">https://medium.com/p/94f0b3c32129</guid>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[cross-site-scripting]]></category>
            <category><![CDATA[web-security]]></category>
            <dc:creator><![CDATA[Dominic Bachmann]]></dc:creator>
            <pubDate>Tue, 23 Sep 2025 05:35:22 GMT</pubDate>
            <atom:updated>2025-09-23T05:35:22.937Z</atom:updated>
            <content:encoded><![CDATA[<p>Last week at <a href="https://angularconnect.com/">AngularConnect</a>, I heard for the first time — thanks to a talk by <a href="https://github.com/martinakraus">Martina Krauss</a> — about a powerful new feature that reshapes how we can defend against cross-site scripting. I found it so interesting that I want to give you a brief introduction to the feature. Cross-site scripting (XSS) attacks (<a href="https://owasp.org/www-community/attacks/xss/">what is XSS?</a>) have been one of the most common attack vectors in web apps for years. With the Trusted Types feature, a huge class of DOM-based XSS issues becomes much harder to exploit — great news for all of us.</p><h3>What are Trusted Types?</h3><p>JavaScript offers lots of ways to create or inject HTML and script from strings — think innerHTML, insertAdjacentHTML, document.write, script.src, iframe.srcdoc, and so on. These are “injection sinks.” They’re essential, but they’re risky: they’ll accept whatever you pass to them. Attackers abuse this to inject malicious code.</p><p>If you’re assigning HTML strings to these sinks, you should sanitize them first (w<a href="https://en.wikipedia.org/wiki/HTML_sanitization">hat is sanitizing?</a>). But people forget, code paths are tricky, and third‑party libraries don’t always sanitize correctly.</p><p>Enter Trusted Types. With Trusted Types enabled, the browser refuses plain strings in dangerous sinks. Instead, those sinks require special values — TrustedHTML, TrustedScript, or TrustedScriptURL — that can only be created by policies you define. Your policy can sanitize and validate input first, then hand the browser a “trusted” value. That closes off a big DOM XSS attack surface.</p><p>Important: Trusted Types don’t magically sanitize for you. You’re responsible for writing (or using) a safe policy that sanitizes/validates input.</p><h3>How do Trusted Types work?</h3><p>Trusted Types have two parts working together: a Content Security Policy (CSP) directive and a trustedTypes global you use to create policies.</p><h4>CSP</h4><p>You enforce and configure Trusted Types with CSP. For example:</p><ul><li>Header form: Content-Security-Policy: trusted-types app dompurify; require-trusted-types-for &#39;script&#39;;</li><li>Meta tag form: &lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;trusted-types app dompurify; require-trusted-types-for &#39;script&#39;&quot;&gt;</li></ul><p>This has two directives:</p><ul><li>trusted-types app dompurify:<br>Lists the policy names your page is allowed to create (e.g., “app”, “dompurify”). You can list one or many. You can also use &#39;none&#39; to block policy creation or &#39;*&#39; to allow any (not recommended).</li><li>require-trusted-types-for &#39;script&#39;:<br>Enforces Trusted Types for script-creating and HTML-parsing sinks (e.g., innerHTML, insertAdjacentHTML, iframe.srcdoc, document.write and URLs for script.src/Workers). The only token currently is &#39;script&#39;.</li></ul><p>Tip: You can start with Report-Only to measure impact before enforcing:<br>Content-Security-Policy-Report-Only: trusted-types app dompurify; require-trusted-types-for &#39;script&#39;;</p><h4>trustedTypes global</h4><p>The second piece is the global trustedTypes object on window. You create “policies” with it—named objects whose methods you implement to sanitize and return trusted values. The browser will then accept those values in dangerous sinks.</p><pre>// Example: using DOMPurify inside a policy to sanitize<br><br>const policy = window.trustedTypes.createPolicy(&#39;app&#39;, {<br>  createHTML: (input) =&gt; DOMPurify.sanitize(input),<br>  createScript: (input) =&gt; input, // This could sanitize for script usage<br>  createScriptURL: (input) =&gt; input, // This could sanitize for scriptURL<br>});<br><br>const untrustedHTML = &#39;&lt;h1&gt;Hello World&lt;/h1&gt;&#39;;<br><br>document.getElementById(&#39;container&#39;).innerHTML = policy.createHTML(untrustedHTML);</pre><p>Note: You can define a special policy named ‘default’. In enforcement mode, if code accidentally passes a string to a sink, the browser will run it through your ‘default’ policy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fk-YaKZ3E5sdKRWGp6zhSA.png" /><figcaption>Enforcing, creating, and using the Trusted Types policy foo (pseudocode)</figcaption></figure><h3>How can I start using Trusted Types today?</h3><p>Trusted Types are supported in Chromium-based browsers like Chrome and Edge. In Firefox, implementation work is ongoing and it isn’t enabled by default yet. Safari/WebKit is also in progress.</p><p>If you want to adopt the pattern ahead of full native coverage, you can use the WICG/W3C Trusted Types polyfill. It won’t enforce protections to the full extent of native support, but it’s very useful for surfacing violations and preparing your app for enforcement.</p><h3>Summary</h3><p>The new Trusted Types feature greatly helps to protect us against XSS attacks. It consists of two parts: enforcement rules and policies, which help ensure that dynamic content added to the DOM is trustworthy. It is currently only supported natively in Chromium-based browsers, with development in Firefox and Safari ongoing. However, polyfills are available, so Trusted Types can be adopted today.</p><h4>Small demo</h4><pre>&lt;!doctype html&gt;  <br>&lt;html lang=&quot;en&quot;&gt;  <br>&lt;head&gt;  <br>    &lt;meta http-equiv=&quot;content-security-policy&quot; content=&quot;trusted-types foo; require-trusted-types-for &#39;script&#39;;&quot;&gt;  <br>&lt;/head&gt;  <br>&lt;body&gt;  <br>    &lt;div id=&quot;container&quot;&gt;&lt;/div&gt;  <br>    &lt;script&gt;        <br>        const myPolicy = window.trustedTypes.createPolicy(&quot;foo&quot;, {  <br>            createHTML: (input) =&gt; input,  <br>            createScript: (input) =&gt; input,  <br>            createScriptURL: (input) =&gt; input,  <br>        });  <br>  <br>        const untrustedHTML = &quot;&lt;h1&gt;Hello World&lt;/h1&gt;&quot;;  <br>  <br>        // Works  <br>        document.getElementById(&quot;container&quot;).innerHTML = myPolicy.createHTML(untrustedHTML);  <br>  <br>        try {  <br>            // Throws because untrustedHTML is a string, not TrustedHTML  <br>            document.getElementById(&quot;container&quot;).innerHTML = untrustedHTML;  <br>        }catch (e){  <br>            console.error(e);  <br>        }  <br>  <br>        // Throws because bar was not allowed in the CSP  <br>        const myPolicy2 = window.trustedTypes.createPolicy(&quot;bar&quot;, {  <br>            createHTML: (input) =&gt; input,  <br>            createScript: (input) =&gt; input,  <br>            createScriptURL: (input) =&gt; input,  <br>        });  <br>    &lt;/script&gt;  <br>&lt;/body&gt;  <br>&lt;/html&gt;</pre><h3>Sources</h3><p><a href="https://github.com/w3c/trusted-types">https://github.com/w3c/trusted-types</a><br><a href="https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API">https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API</a><br><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types">https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types</a><br><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/http-equiv">https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/http-equiv</a><br><a href="https://github.com/cure53/DOMPurify">https://github.com/cure53/DOMPurify</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=94f0b3c32129" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>