<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: epilot</title>
    <description>The latest articles on DEV Community by epilot (@epilot).</description>
    <link>https://dev.to/epilot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3368%2Fa1997f89-aaf9-4f0f-b464-d34f2775a882.jpg</url>
      <title>DEV Community: epilot</title>
      <link>https://dev.to/epilot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/epilot"/>
    <language>en</language>
    <item>
      <title>Our Entire Company Ships Code Now. 40 PRs from Non-Engineers in 60 Days.</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Mon, 13 Apr 2026 13:06:30 +0000</pubDate>
      <link>https://dev.to/epilot/our-entire-company-ships-code-now-40-prs-from-non-engineers-in-60-days-jo5</link>
      <guid>https://dev.to/epilot/our-entire-company-ships-code-now-40-prs-from-non-engineers-in-60-days-jo5</guid>
      <description>&lt;p&gt;In February, I opened our entire codebase to the company. PMs, designers, projects, customer success, and support all got access to 219 repositories and over a million lines of production code. I handed them powerful coding agents and told them to start contributing.&lt;/p&gt;

&lt;p&gt;I've been writing about product engineering for years. The core idea: engineers should understand the whole product, the customer, the business problem. I wanted to test the reverse. Can non-engineers contribute to the codebase if you give them the right tools?&lt;/p&gt;

&lt;p&gt;Two months of real data delivered a clear answer: yes. And it may be the most powerful shortcut I've found to building capable, business-aware product teams who truly care about what they ship.&lt;/p&gt;

&lt;p&gt;Here is what that looked like at epilot: 629 merged PRs in 60 days. &lt;strong&gt;40 PRs were from 11 non-engineers&lt;/strong&gt;. All teams have at least one person who contributed to our codebase with Claude Code.&lt;/p&gt;

&lt;p&gt;We're not alone in seeing this shift. &lt;a href="https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents" rel="noopener noreferrer"&gt;Stripe ships over a thousand agent-produced PRs per week&lt;/a&gt;. &lt;a href="https://openai.com/index/introducing-codex/" rel="noopener noreferrer"&gt;Superhuman uses Codex&lt;/a&gt; to let product managers contribute lightweight code changes without pulling in an engineer (except for code review). &lt;a href="https://ideas.fin.ai/p/we-gave-claude-code-to-everyone-at" rel="noopener noreferrer"&gt;Intercom gave Claude Code to everyone in the company&lt;/a&gt;, over 1,000 non-engineers (PMs, designers, support, marketers, and more), and more than 300 became active weekly users.&lt;/p&gt;

&lt;p&gt;This post is a detailed, honest look at how we set it up, what actually worked, the mistakes we made, and the deeper lessons that emerged.&lt;/p&gt;




&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;epilot is a B2B SaaS platform for energy companies. We run a fully remote team of 40+ product engineers with no engineering managers and no dedicated platform team. Everyone owns end-to-end ownership. We already ship to production 150+ times per week on AWS serverless.&lt;/p&gt;

&lt;p&gt;Our entire engineering team was already using AI coding agents daily. Claude Code, Codex, Cursor. Engineers had adopted them fast and the productivity gains were obvious. The question I wanted to answer next: what happens when you expand codebase access via coding agents to the rest of the company? PMs, designers, customer success, support. Not as a one-off hack day, but as a fully supported, production-grade workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infrastructure that turned agents into teammates
&lt;/h2&gt;

&lt;p&gt;Claude Code and Codex still only ship connectors to GitHub, and our entire codebase lives in GitLab. So we created a single GitHub repo that bootstraps a developer sandbox with a shallow copy of all 219 repositories. This allows both AI agents or engineers to spin up a fully functioning developer environment with everything cloned &amp;amp; tools set up within minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Agents live in Slack
&lt;/h3&gt;

&lt;p&gt;We created &lt;strong&gt;#dev-squad-agents&lt;/strong&gt; alongside our regular squad channels. Anyone could join via Claude Code, Codex, or GitHub Codespaces. I ran three one-hour onboarding sessions to showcase the tooling and make sure everyone had the required access: one for design, one for support, one for go-to-market. The PMs didn't need their own session. Their engineering teams had already invited them and they were eager to start.&lt;/p&gt;

&lt;p&gt;This single decision drove adoption more than anything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. One-click preview environments
&lt;/h3&gt;

&lt;p&gt;Every PR automatically generates a live preview link in CI. Click it, see the change, test it immediately. No local setup required.&lt;/p&gt;

&lt;p&gt;For engineers this is convenient. For non-engineers it is essential. If they cannot see and interact with their change, they can't meaningfully contribute.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Deep institutional context
&lt;/h3&gt;

&lt;p&gt;We built an indexed knowledge base called &lt;strong&gt;&lt;a href="https://www.linkedin.com/pulse/we-made-coding-agents-actually-reliable-fixing-one-thing-kuosmanen-oa2of/" rel="noopener noreferrer"&gt;how-to-everything&lt;/a&gt;&lt;/strong&gt;. It contains our API patterns, backend conventions, code style, CI/CD and testing strategy, full business context, domain terminology, and our entire RFC archive.&lt;/p&gt;

&lt;p&gt;Stripe solved the same problem by connecting agents to 400+ internal tools. We chose comprehensive indexing. The insight is identical: &lt;strong&gt;agents without your company’s context produce garbage. Agents with it produce work that feels native.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Design system in context
&lt;/h3&gt;

&lt;p&gt;Our Volt UI component library is baked into the agent prompt. Claude now generates frontend code that respects the correct components, spacing, colours, and patterns from the first try. Consistent UI without forcing a designer to review every small change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Claude as a coworker: the Slack pattern
&lt;/h2&gt;

&lt;p&gt;Claude quickly became the most active “team member.” People tag &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt;&lt;/strong&gt; the same way they tag colleagues, and they expect results just as fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  How engineers use it
&lt;/h3&gt;

&lt;p&gt;Simple tasks look like: “&lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; fix EPI-5660.” A PR appears.&lt;/p&gt;

&lt;p&gt;More powerful uses scale across the organization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;“&lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; which microfrontends still use the legacy editor?”&lt;/em&gt; → instant audit of all 219 repos.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;“&lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; migrate 13 backend repos from file-client to lambda-adapter.”&lt;/em&gt; → completed in one session.&lt;/li&gt;
&lt;li&gt;Mass upgrades across 20+ repositories: one message triggers 6 parallel sub-agents that handle changes, tests, and PRs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What once took a full day of tedious cross-repo work now happens in hours. Agents worked weekends. Engineers didn't have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  How non-engineers use it
&lt;/h3&gt;

&lt;p&gt;They speak in plain product language:&lt;br&gt;
&lt;em&gt;- “&lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; disable drag and drop of entity table columns for embedded tables.”&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“&lt;a class="mentioned-user" href="https://dev.to/claude"&gt;@claude&lt;/a&gt; when creating a new family in the Label Builder, the drawer should close automatically.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude figures out the right repositories and implementation details. Non-engineers don't need to know our tech stack or repo structure. They describe the desired outcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production impact
&lt;/h3&gt;

&lt;p&gt;We added Claude to &lt;strong&gt;#alerts-production&lt;/strong&gt; on day two. It investigates incidents, reads logs, traces errors, and proposes fixes before most engineers open the dashboard. When a bulk action hammered Elasticsearch with 100 parallel requests, Claude designed an org-level job queue with concurrency limits that cut peak load by 90%. Engineers started replying “another one bites the dust” to resolved alerts.&lt;/p&gt;

&lt;p&gt;Within a week, Claude felt like a trusted junior-to-senior coworker: fast executor, deep historian, and reliable pair programmer. Usage spread organically to every squad.&lt;/p&gt;




&lt;h2&gt;
  
  
  What people actually built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Non-engineer contributions (40 merged PRs from 11 people)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature work&lt;/strong&gt;: One PM built auto-tagging for file uploads (a feature he had spec’d three months earlier). Another shipped 13 PRs on messaging (bulk email templates, sidebar toggles, drawer behaviour, i18n). A go-to-market colleague delivered entire features, like pivot tables for displaying time-series data in epilot tables, that are now live for customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily improvements&lt;/strong&gt;: Customer success shipped portal config changes they handle every day. Designers and support added FullStory tracking and started prototyping new UI flows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translations&lt;/strong&gt;: Native speakers cleared 116 German translation improvements with almost no engineering involvement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UX via vibecoding&lt;/strong&gt;: A designer shipped a UX improvement to our Flow Hub before heading into the weekend: when creating a new Flow, users now get prompted to enter a name first. She described it as "vibe coding" and the change reduced friction in a way she'd been wanting to fix for a while.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These were not toy changes. Several were meaningful, customer-facing updates. One PM even asked Claude to create a demo video of the feature it just built, so he could share it with the team. The full cycle from spec to demo, handled through conversation with an agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engineer contributions (589 merged PRs)
&lt;/h3&gt;

&lt;p&gt;Engineers use agents across the full lifecycle. Writing RFCs and technical designs. Implementing multi-service features. Running tests. Shipping to production. Auditing feature flag usage across 15 repos and cleaning up stale flags. Investigating production incidents from alert channels before anyone opens a dashboard.&lt;/p&gt;

&lt;p&gt;Some of the most valuable uses are research and diagnostics. "Which services listen to the file upload event?" "How many microfrontends still use the legacy editor?" "Audit all listSchemas calls and upgrade to the new summary API where safe." These are questions that used to take half a day of grepping across 219 repos. Claude answers them in minutes.&lt;/p&gt;

&lt;p&gt;GitHub Codespaces turned out to be a key piece for engineers too. Running Claude Code in auto-mode in a cloud sandbox means you can kick off a multi-repo migration and let it run without risking your work laptop. No local state to corrupt, no accidental changes to your working branch. The sandbox spins up with the full codebase mirrored and Claude works through the task autonomously. Engineers now run 6 parallel sub-agents in Codespaces for bulk upgrades across 20+ repos: package bumps, migration scripts, test updates, PRs opened. What used to be a full day of copy-paste across repos is done in one session.&lt;/p&gt;

&lt;h3&gt;
  
  
  The new collaboration model
&lt;/h3&gt;

&lt;p&gt;Non-engineer describes the problem in business terms → Claude implements → engineer reviews for architecture and quality. The person closest to the customer now helps ship the solution. Feedback loops tightened dramatically.&lt;/p&gt;

&lt;p&gt;A designer and engineer even deployed &lt;a href="https://www.agentation.com/" rel="noopener noreferrer"&gt;Agentation&lt;/a&gt; (a prompt-structuring tool) in 2.5 hours. The designer now uses it daily and calls it a “dream collaboration.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Guardrails: the hard lesson
&lt;/h2&gt;

&lt;p&gt;A few weeks in, one non-engineer with full GitLab push access bypassed our CI pipeline and deployed code to production without review. No damage, but it was exactly the wake-up call we needed.&lt;/p&gt;

&lt;p&gt;We immediately tightened permissions: non-engineers and agents create PRs; engineers review and merge. No direct push to protected branches.&lt;/p&gt;

&lt;p&gt;But that incident was just one piece. When you give AI agents to 50+ non-engineers, you need a proper governance framework. We built one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we had from day one
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sandboxed execution only.&lt;/strong&gt; Agents run in Claude Code Web, Codex sandbox, or GitHub Codespaces. No production infrastructure access. No deployment outside CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated bot users.&lt;/strong&gt; Limited API scopes. Code-level access only. Zero access to customer data, session tokens, or production databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public channels only.&lt;/strong&gt; All agent interactions happen in shared Slack channels. Engineers see everything and can intervene. No private DMs with agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human responsibility.&lt;/strong&gt; You trigger the agent, you own the output. No exceptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What we added after
&lt;/h3&gt;

&lt;p&gt;We published a formal &lt;strong&gt;Agentic AI Tool Usage Guideline&lt;/strong&gt;. The core principle: give the agent the minimum access it needs for the task at hand, and nothing more.&lt;/p&gt;

&lt;p&gt;The guideline includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A three-tier data classification.&lt;/strong&gt; Red (never share with AI): customer PII, session tokens, credentials, security documents. Yellow (anonymise first): user research transcripts, survey responses, usage metrics with identifiers. Green (safe): public docs, your own drafts, anonymised research, process docs. Hard line on red: no exceptions, no "just this once."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An approved connector matrix.&lt;/strong&gt; Every tool combination (Claude + Figma, Claude + Atlassian, Claude + Miro, etc.) classified by risk level with specific conditions and recommended settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection awareness.&lt;/strong&gt; Use a separate browser profile for Claude browser extension. Don't paste content from untrusted sources into agent prompts. Disconnect MCP connectors when not in use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit dealbreakers.&lt;/strong&gt; Leaking customer personal data, unauthorized code deployment, and feeding company data into model training are not allowed regardless of how useful the outcome might be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GDPR and EU AI Act compliance.&lt;/strong&gt; The guideline maps our practical rules to specific legal obligations: data minimisation (Art. 5), DPA requirements (Art. 28), AI literacy obligations (Art. 4), transparency requirements (Art. 50). We're a European company serving energy utilities. Regulatory compliance is table stakes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Lock down merge and push permissions &lt;em&gt;before&lt;/em&gt; opening access. We got lucky. And build the governance framework early. Guardrails didn't slow adoption; they accelerated trust and usage.&lt;/p&gt;




&lt;h2&gt;
  
  
  What surprised us most
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adoption speed&lt;/strong&gt;: Three one-hour onboarding sessions and people were off. Every team across the company had active contributors within three weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-engineers shipping real features&lt;/strong&gt;: I expected small fixes and translations. Instead, people from across the company delivered complete, production features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude as institutional memory&lt;/strong&gt;: Support now asks Claude product questions instead of hunting down engineers. It has become the fastest way to understand how anything works across five years of code history.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Beyond code
&lt;/h2&gt;

&lt;p&gt;This is just one layer. My colleague Suresh built &lt;strong&gt;Vigilos&lt;/strong&gt;, an internal BI agent that lets anyone query our data warehouse in natural language. Designers check average journey name lengths for UX decisions. Customer success identifies accounts with AI features disabled. Product managers run customer segment analysis in minutes.&lt;/p&gt;

&lt;p&gt;We'll share that story soon.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;Two years ago I published the Product Engineer Manifesto: engineers should care about the customer, the business outcome, and the whole product.&lt;/p&gt;

&lt;p&gt;What these two months taught me is that the same mindset now applies in reverse. Give non-engineers the right tools, deep context, and sensible guardrails, and they will deliver.&lt;/p&gt;

&lt;p&gt;We're still hiring product-minded engineers. That hasn't changed. Engineers at epilot own the full product lifecycle: customer understanding, architecture, quality, reliability, and shipping. Non-engineer contributions don't reduce what we expect from engineers. They amplify it. The entire organization now ships alongside them.&lt;/p&gt;

&lt;p&gt;629 merged PRs in 60 days. 40 from non-engineers.&lt;/p&gt;




&lt;p&gt;We're hiring all product roles who thrive on full ownership in an AI-native environment. Remote-first. &lt;a href="https://www.epilot.cloud/en/company/careers" rel="noopener noreferrer"&gt;epilot.cloud/careers&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>product</category>
    </item>
    <item>
      <title>We Made Coding Agents Actually Reliable By Fixing One Thing</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Wed, 04 Feb 2026 15:55:45 +0000</pubDate>
      <link>https://dev.to/epilot/we-made-coding-agents-actually-reliable-by-fixing-one-thing-525b</link>
      <guid>https://dev.to/epilot/we-made-coding-agents-actually-reliable-by-fixing-one-thing-525b</guid>
      <description>&lt;p&gt;Last week, Vercel published &lt;a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals" rel="noopener noreferrer"&gt;research showing that giving coding agents a compact index of your documentation dramatically outperforms letting them search for answers on demand&lt;/a&gt;. Their eval results: 100% task success rate with the map approach, versus only 79% when agents had to actively look things up.&lt;/p&gt;

&lt;p&gt;Same agent, same tasks, different approach to context. The difference between working and not working.&lt;/p&gt;

&lt;p&gt;The insight clicked immediately. If we could give Claude Code reliable access to this institutional knowledge without forcing it to decide when to look things up, it would fundamentally change how people work in our codebase.&lt;/p&gt;

&lt;p&gt;So we built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context Problem
&lt;/h2&gt;

&lt;p&gt;Coding agents have a token limit. A ceiling on how much information they can process at once. Think of it like working memory. You can't hand Claude Code your entire codebase and documentation library upfront. It's too much.&lt;/p&gt;

&lt;p&gt;The traditional solution is skills: the coding agent decides when it needs information and actively looks it up. "I need to know about authentication, let me search for that." Sounds reasonable. In practice it creates three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decision paralysis&lt;/strong&gt; - the agent has to decide &lt;em&gt;when&lt;/em&gt; to look up docs, and it often guesses wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async delay&lt;/strong&gt; - every lookup is a round-trip, breaks flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequencing conflicts&lt;/strong&gt; - exploring code vs. consulting docs creates timing issues&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vercel's approach flips this: give Claude Code a compressed index of what documentation exists and where to find it, &lt;em&gt;before&lt;/em&gt; it starts work. The index is small enough to fit in context every turn, so it always knows what's available. When it needs details, it reads the specific file directly. No decisions, no lookups, no conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Compression Matters
&lt;/h2&gt;

&lt;p&gt;The key innovation is compression. A full documentation tree - all the folder structures, file names, categories - takes significant space. Too much to include in every conversation turn.&lt;/p&gt;

&lt;p&gt;The compressed index uses a simple pipe-delimited format that shrinks this by ~80%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[epilot Docs Index]|root: ./.epilot-docs|00-general:{tech-stack.md,business-context.md,ci-cd.md}|01-apis:{api-design.md,calling-apis.md}|02-epilot360-microfrontends:{env-vars.md,local-dev.md}|...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line tells the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where the docs live (&lt;code&gt;.epilot-docs/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;What categories exist (&lt;code&gt;00-general&lt;/code&gt;, &lt;code&gt;01-apis&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;What files are in each category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a table of contents, not the full book. But it's enough. The agent sees the map, understands what's available, and pulls specific files when needed. The decision-making load disappears.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2smftr4lr0o3jpr0xtxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2smftr4lr0o3jpr0xtxp.png" alt="Claude code actually reading docs for once before jumping into code" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The accuracy improvement is significant (100% vs 79% task success), but it's not the real story.&lt;/p&gt;

&lt;p&gt;The real story is what happens when you lower the barrier to contribution. Proper context enables the person closest to the problem to fix it, regardless of job title. Your PM can fix bugs. Designers can adjust component behavior. Support engineers can patch data issues. You remove bottlenecks. Context and authority live in the same person.&lt;/p&gt;

&lt;p&gt;Coding agents are the equalizer. But they're only as good as the context you give them.&lt;/p&gt;

&lt;p&gt;Most companies will throw Claude Code at their codebase and wonder why results are inconsistent. The agent hallucinates patterns. Makes incorrect assumptions. Writes code that doesn't match conventions.&lt;/p&gt;

&lt;p&gt;The difference is context. Structured, compressed, always-available context about how &lt;em&gt;your&lt;/em&gt; codebase works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Build Your Own
&lt;/h2&gt;

&lt;p&gt;The pattern is straightforward. Here's what we did at epilot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Curate agent-friendly documentation&lt;/strong&gt; - organize your internal knowledge: conventions, APIs, architectural patterns, framework usage, code style&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure by domain&lt;/strong&gt; - group related docs (general, backend, frontend, infrastructure, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use descriptive filenames&lt;/strong&gt; - the agent sees filenames in the compressed index before opening files. &lt;code&gt;api-design.md&lt;/code&gt; is better than &lt;code&gt;guidelines.md&lt;/code&gt;. &lt;code&gt;error-handling.md&lt;/code&gt; is better than &lt;code&gt;errors.md&lt;/code&gt;. Make filenames searchable and specific.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate updates&lt;/strong&gt; - pull live data where possible (OpenAPI specs, schema definitions, framework docs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate compressed index&lt;/strong&gt; - use a simple format (pipe-delimited works well) that reduces the doc tree by ~80%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed in agent context&lt;/strong&gt; - add the index to your CLAUDE.md or AGENTS.md file (the context files Claude Code reads)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One thing worth highlighting: we don't just include our internal documentation. We also package docs for the frameworks and libraries we heavily use - single-spa, openapi-backend, openapi-client-axios, i18next, and Volt UI (our custom design system). When Claude Code needs to know how i18next pluralization works or how to register a single-spa parcel, it already has the answer. No hallucination, no outdated Stack Overflow posts, just accurate framework documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Enables
&lt;/h2&gt;

&lt;p&gt;We're already seeing daily usage across the team. Developers context-switch between services faster. Non-engineers contribute directly instead of filing tickets.&lt;/p&gt;

&lt;p&gt;But the real potential is broader: if compressed context improves coding agents for technical documentation, why stop there?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runbooks and incident response&lt;/strong&gt; - on-call engineers with instant access to procedures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer domain knowledge&lt;/strong&gt; - support teams with context on product behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic&lt;/strong&gt; - product decisions and their rationale, preserved and accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is the same: curate the knowledge, compress it, embed it in context, let the agent work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraints Are Disappearing
&lt;/h2&gt;

&lt;p&gt;For decades, contributing to a codebase required deep technical knowledge. You needed to understand the language, the frameworks, the architectural patterns, the implicit conventions. The barrier was high.&lt;/p&gt;

&lt;p&gt;Coding agents lower it. Claude Code, Cursor, and similar tools don't replace engineers. They make technical knowledge more accessible. With the right tooling and the right context, a PM can fix bugs. A designer can adjust styling logic. A support engineer can patch data issues.&lt;/p&gt;

&lt;p&gt;The question isn't whether this is possible. It's how fast you can adapt.&lt;/p&gt;

&lt;p&gt;Organizations which enable broader contribution will move faster than those that don't. The tools exist. The research is clear. What's missing is execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Simple
&lt;/h2&gt;

&lt;p&gt;You don't need to document everything upfront. Start with the knowledge that causes the most friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code style conventions&lt;/strong&gt; - how you write TypeScript, naming patterns, file structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common patterns&lt;/strong&gt; - how you handle authentication, API calls, error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework specifics&lt;/strong&gt; - non-obvious usage of your frameworks and libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal APIs&lt;/strong&gt; - if you have OpenAPI specs, even better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a simple doc structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
  00-general/
    code-style.md
    tech-stack.md
  01-apis/
    api-design.md
    calling-apis.md
  02-backend/
    error-handling.md
    database-patterns.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the compressed index (pipe-delimited format, one line per directory). Add it to your CLAUDE.md or AGENTS.md file - the context files that Claude Code and other coding agents read on startup. Done.&lt;/p&gt;

&lt;p&gt;The compressed index approach works! 🎉 Vercel's research proved it: 100% task success versus 79% without it. We've validated it internally. Now it's about whether you'll adopt it before your competitors do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>coding</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Security at Scale: Our npm Incident Response Story</title>
      <dc:creator>João Pinho</dc:creator>
      <pubDate>Wed, 10 Sep 2025 16:20:24 +0000</pubDate>
      <link>https://dev.to/epilot/security-at-scale-our-npm-incident-response-story-439p</link>
      <guid>https://dev.to/epilot/security-at-scale-our-npm-incident-response-story-439p</guid>
      <description>&lt;p&gt;On September 8th, the npm ecosystem saw what is now called the largest supply-chain compromise in its history. Packages like chalk, debug, and ansi-styles — together downloaded billions of times every week — were hijacked and malicious versions published.&lt;/p&gt;

&lt;p&gt;For any SaaS company with Node.js in its stack, this was a moment to pause and act.&lt;/p&gt;

&lt;p&gt;At epilot, where we build cloud software for the energy market, we recognised that this had the potential to impact our systems. Here's how we responded.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚨 First, what happened?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A phishing attack compromised the maintainer of several widely used packages.&lt;/li&gt;
&lt;li&gt;Malicious versions were published and quickly spread via transitive dependencies.&lt;/li&gt;
&lt;li&gt;The injected code was designed to hijack crypto wallet transactions, but the bigger story is: if attackers could publish once, they could publish anything. Additionally, if attackers could read/intercept our network requests, they would have access to our tokens and thus our platform on behalf of our users.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚡ Our response — measured in hours
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;As soon as the incident was announced, our engineering team moved fast. Within minutes of posting the alert in our #dev-security slack channel, we had a small response team come together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;172 repositories scanned: we used GitHub Codespaces to access our centralized codebase and systematically audit our product ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;451 Node.js projects analyzed: every package.json across our domain groups (auth, billing, analytics, 360-portal, and more) was checked for the compromised packages and versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automation helped: we cross-checked with our internal tooling and monitoring alerts to confirm no production build had pulled malicious versions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: ✅ zero exposure found across all 451 dependency trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡 Practices that kept us safe
&lt;/h2&gt;

&lt;p&gt;The npm incident highlighted the value of practices we already had in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Dependency monitoring with Corge → ongoing visibility into package versions and vulnerabilities across our entire ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Masked logs in Datadog → all PII automatically obfuscated, ensuring sensitive customer data never leaks, even if dependencies misbehave.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LLM anonymisation → using Microsoft Presidio, we automatically detect and anonymise emails, phone numbers, IBANs, credit cards, and other PII before any data reaches AI models for our AI-powered features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't "nice-to-haves." They are part of how we build trust with customers in a regulated industry like energy.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Supply chain attacks are inevitable — being able to respond fast is what matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modern tooling makes a difference — GitHub Codespaces let us audit 172 repositories and 451 Node.js projects in hours, not days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Good hygiene compounds — monitoring dependencies, masking logs, and anonymizing LLM data aren't glamorous, but when incidents like this happen, they prove their worth.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🔭 Looking forward
&lt;/h2&gt;

&lt;p&gt;Incidents like the npm compromise will continue. At epilot, we're committed not just to reacting fast, but to building resilient,  privacy-first systems that earn customer trust every day.&lt;/p&gt;

&lt;p&gt;Because in the energy market — where trust and compliance are everything — resilience is the real competitive advantage.&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>npm</category>
    </item>
    <item>
      <title>From Chaos to Clarity: Fixing Our Monorepo</title>
      <dc:creator>kate astrid</dc:creator>
      <pubDate>Tue, 19 Aug 2025 11:45:37 +0000</pubDate>
      <link>https://dev.to/epilot/from-chaos-to-clarity-fixing-our-monorepo-2bih</link>
      <guid>https://dev.to/epilot/from-chaos-to-clarity-fixing-our-monorepo-2bih</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction: Why This Story Might Save You Some Headaches&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you’ve worked in a large codebase, you’ve probably seen this: the code runs, but every change takes far longer than it should. Updating one feature often breaks another. Adding a dependency means worrying about conflicts. Old build tools still linger, and no one feels safe upgrading packages that half the system depends on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadw3szflwaorl8fxe66t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadw3szflwaorl8fxe66t.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s exactly what we faced in our &lt;code&gt;journey-monorepo&lt;/code&gt;, the main repository for our team. Dependencies were wired together in inconsistent ways, different teams had introduced multiple overlapping build tools, and some libraries hadn’t been touched in years. Proposing a cleanup felt like asking to redesign a city while people were still living in it — risky, disruptive, and easy to delay indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;So why did we bother?&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Development had slowed to a crawl.&lt;/em&gt; Builds were taking too long, and waiting minutes just to see a change in the browser was killing productivity.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Every change felt risky.&lt;/em&gt; The dependency spaghetti meant a small tweak in one package could break multiple apps — often in ways we wouldn’t notice until much later.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Onboarding new developers was painful.&lt;/em&gt; It took too long for new team members to get up to speed, mostly because they had to learn multiple bundlers, testing setups, and outdated tooling quirks.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Technical debt was blocking new features.&lt;/em&gt; We were spending more time patching, debugging, and wrestling with the repo than building the actual features our users cared about.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Costs were adding up.&lt;/em&gt; Longer build times meant higher CI costs, and outdated packages made security updates and maintenance more expensive in the long run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the longer we waited, the more time, money, and momentum we were losing.&lt;/p&gt;

&lt;p&gt;And, hey, now we get a good story out of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hcmyun9s9ew3gqoyx5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9hcmyun9s9ew3gqoyx5w.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Journey-Monorepo in a Nutshell&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykolpl8sntrwe4pdeoqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykolpl8sntrwe4pdeoqc.png" alt=" " width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Journey-monorepo&lt;/em&gt; is the central place on the epilot platform for everything related to journeys. What you see in the screenshot above represents two main apps: &lt;em&gt;journey-app&lt;/em&gt; on the left side and &lt;em&gt;journey-builder&lt;/em&gt; on the right. &lt;em&gt;Journey-builder&lt;/em&gt; is responsible for configuring journeys, and &lt;em&gt;journey-app&lt;/em&gt; for rendering them. &lt;br&gt;
Like in many startups, the repo started out clean and well-structured, but as the product grew quickly, the codebase expanded in unplanned ways — things were added wherever they seemed to fit at the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The “BEFORE” Setup&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apps:
- journey-builder.               
- journey-app
- entity-mapping
- journey-view

Packages:
- blocks-configurators
- blocks-renderers
- journey-elements
- journey-utils
- journey-logics-common
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me show you the rough map of dependencies we had step by step. We are interested in two main apps: &lt;code&gt;journey-builder&lt;/code&gt; and &lt;code&gt;journey-app&lt;/code&gt;, so I am going to dive deeper into their connections. It is not important which app or package does what. What IS important is to see how many connections we had to keep in mind every time we needed to change anything.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Ok, this is fine.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf5p4t1zdjxf169gq22z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf5p4t1zdjxf169gq22z.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is too.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8udxll5y9ewrp6q8nboq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8udxll5y9ewrp6q8nboq.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yeah, cool.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy2gcleacdlwo85nozw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy2gcleacdlwo85nozw0.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Packages already become tangled, let's keep going.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fncrjunqvi3x0zojsp82d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fncrjunqvi3x0zojsp82d.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ok, even more connections.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qeqtr5pcsd766pxyhld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qeqtr5pcsd766pxyhld.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Well, this is just a delinquent madness.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptjuqypr9w8yp04lzbmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptjuqypr9w8yp04lzbmj.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You might say: Kate, come on, this is what happens in every big project - things become deeply intertwined, and it is a normal thing to experience. &lt;br&gt;
I will answer: yes, but I haven't finished.&lt;/p&gt;

&lt;p&gt;Let's now add the build tools to this table to have the full picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jfmqmed9opca1t4cy1h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jfmqmed9opca1t4cy1h.png" alt=" " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Well, what we have here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webpack;&lt;/li&gt;
&lt;li&gt;Craco;&lt;/li&gt;
&lt;li&gt;Tsup;&lt;/li&gt;
&lt;li&gt;Tsx;&lt;/li&gt;
&lt;li&gt;Tsdx.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't know about you, but I hadn't worked with half of them before I started working on this repo. &lt;/p&gt;

&lt;p&gt;In the end, it was a web of “if you change this, you might break that” — and “that” could be anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How We Tackled It&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We kicked things off with an RFC — basically, “let’s write this down so we don’t lose the plot halfway through.”&lt;br&gt;
Our two big goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unify the tooling so we weren’t juggling five different build setups.&lt;/li&gt;
&lt;li&gt;Untangle dependencies so teams could work without stepping on each other’s toes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this is exactly what we did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Cut the Cord on Unnecessary Dependencies&lt;/strong&gt;&lt;br&gt;
The first thing we had to do was face the dependency jungle. Over time, different teams had introduced overlapping packages, and no one was quite sure what depended on what anymore. Cleaning this up gave us a chance to regain control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Untangled all the apps and packages as much as possible.&lt;/em&gt; We mapped out how everything connected and then started cutting back the unnecessary links. This meant reducing cross-dependencies that caused fragile builds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Moved blocks-configurators into journey-builder.&lt;/em&gt; These configurators were never meant to be used outside, so pulling them closer to where they belong simplified the dependency graph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Moved blocks-renderers into journey-app.&lt;/em&gt; Renderers were core to the application itself, so consolidating them in the main app made the architecture more intuitive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Deleted journey-utils and journey-elements.&lt;/em&gt; &lt;em&gt;Journey-utils&lt;/em&gt; was a very small package with some utilities that were moved to other packages. And &lt;em&gt;journey-elements _ were later replaced by _concorde-elements&lt;/em&gt;. You can read more about it in &lt;a href="https://dev.to/epilot/building-a-scalable-react-component-library-lessons-from-concorde-elements-kdi"&gt;this article written by my partner in crime.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a painful step, but once the dust settled, the repo felt lighter and easier to reason about. We no longer had to worry about accidentally breaking other parts of the system when making small changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Standardize the Tooling&lt;/strong&gt;&lt;br&gt;
After trimming the dependencies, the next big challenge was our tooling. Different parts of the repo used different build systems, which slowed everyone down and made onboarding new developers a headache.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Phased out Craco, Tsup, and Tsdx by switching to Vite.&lt;/em&gt; These older tools had been added over time, each solving a narrow need. Vite gave us a unified, modern setup with faster builds and simpler configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Swapped Jest for Vitest&lt;/em&gt; — faster tests, modern setup. Jest had served us well, but it was slow and required custom patching to keep working. Vitest gave us near-instant feedback and worked seamlessly with Vite, making the dev experience far smoother.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By unifying the tooling, we not only sped up day-to-day work but also reduced cognitive load. Developers no longer had to remember which tool applied to which package — everything just worked the same way everywhere.&lt;/p&gt;

&lt;p&gt;This is how the "AFTER" setup looks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxglsvhwnlvjy7jtopxd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxglsvhwnlvjy7jtopxd1.png" alt=" " width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the monorepo is now far more streamlined — there are significantly fewer dependencies crisscrossing between packages, and we’ve eliminated most of the variations in build tools. This makes the structure easier to understand, faster to work with, and much less fragile when changes are introduced. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;After the Restructuring: What We Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When the dust settled, it wasn’t just that the dependency graph was cleaner — we were thinking about the repo differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediate Wins&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Changing things stopped feeling dangerous.&lt;/em&gt; Before, touching a shared package felt like pulling a brick from a Jenga tower. Now, fewer connections mean less risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Builds and tests stopped feeling like a punishment.&lt;/em&gt; Vite’s dev server = instant feedback. Vitest shaved minutes off test runs. Devs started running tests more often just because they could.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Onboarding went from “ugh” to “okay.”&lt;/em&gt; New folks could get set up without learning the quirks of four bundlers first.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slower Payoffs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Tooling swaps aren’t magic.&lt;/em&gt; Some Webpack plugins didn’t have a Vite equivalent, so we had to rethink features or drop them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Vitest exposed bad tests.&lt;/em&gt; Some “passing” tests were only passing thanks to Jest’s quirks. Painful, but worth fixing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-Offs &amp;amp; Reality Checks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Not everything got unified.&lt;/em&gt; A couple of apps still run on legacy setups because migrating them wasn’t worth the risk — for now.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Shared packages need rules.&lt;/em&gt; We now ask “does this really need to be shared?” before creating new ones.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Refactoring is never “done.”&lt;/em&gt; There is always room for improvement, we all know. that. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips If You’re Thinking About Doing This&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Draw your dependency graph before you start — it’ll help convince others (and yourself) that the cleanup’s worth it.&lt;/li&gt;
&lt;li&gt;Expect setbacks. Some PRs will break things. That’s normal.&lt;/li&gt;
&lt;li&gt;Automate checks for unused or outdated packages — saves a ton of review time.&lt;/li&gt;
&lt;li&gt;Celebrate small wins. Removing the last Webpack config deserved cake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This wasn’t a quick tidy-up — it was a half-year-long deliberate overhaul alongside working on other features. But it’s made a massive difference to how we work: faster builds, fewer “don’t touch that” areas, and a general feeling that the repo is ours again, not a beast we’re afraid of.&lt;/p&gt;

&lt;p&gt;If I had to wrap it all up in one bit of advice?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Keep it simple.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every extra dependency, tool, or package you skip today is one less thing you’ll have to untangle in the future.&lt;/p&gt;

</description>
      <category>refactoring</category>
      <category>monorepo</category>
      <category>epilot</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building a Scalable React Component Library: Lessons From Concorde Elements</title>
      <dc:creator>Adeola Adeyemo</dc:creator>
      <pubDate>Wed, 23 Jul 2025 14:02:23 +0000</pubDate>
      <link>https://dev.to/epilot/building-a-scalable-react-component-library-lessons-from-concorde-elements-kdi</link>
      <guid>https://dev.to/epilot/building-a-scalable-react-component-library-lessons-from-concorde-elements-kdi</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;We recently embarked on the complete redesign of our Journeys, aiming for a modern, state-of-the-art look - &lt;strong&gt;Project Concorde&lt;/strong&gt;. This wasn't just a UI overhaul; we sought the flexibility for future modifications, knowing that simply patching our existing UI, heavily reliant on Material UI, wouldn't suffice. We needed to build a new foundation from the ground up.&lt;/p&gt;

&lt;p&gt;The full Project Concorde story is larger, but in this article, we'll dive into the story of &lt;code&gt;@epilot/concorde-elements&lt;/code&gt;, the new component library born from that need, and how we built a system that not only powers our new interface, but also empowers our development team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did we need a new component library?
&lt;/h3&gt;

&lt;p&gt;Our existing component library, &lt;code&gt;@epilot/journey-elements&lt;/code&gt;, was based on Material UI. While it served its purpose, our goal was to reduce our reliance on Material UI to gain several crucial benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce performance overhead from Material UI's style generation in favour of CSS modules&lt;/li&gt;
&lt;li&gt;React version constraints tied to MUI releases.&lt;/li&gt;
&lt;li&gt;Styling limitations blocking modern CSS features.&lt;/li&gt;
&lt;li&gt;Remove the use of the Material UI theme object in saved custom Designs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The image below shows the overhead to create custom designs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3ohs2uri818vjj407ia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3ohs2uri818vjj407ia.png" alt="Showing the MUI overhead" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not use existing design systems?
&lt;/h3&gt;

&lt;p&gt;We found that most off-the-shelf design systems are quite opinionated with their styling and theming. Our philosophy was that it's easier to replace a single unit than an entire factory. Our primary goal was to keep our new system &lt;strong&gt;simple and extensible&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge
&lt;/h3&gt;

&lt;p&gt;The path forward was not without its hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We needed to build &lt;strong&gt;37+ components&lt;/strong&gt; with various potential variants.&lt;/li&gt;
&lt;li&gt;All Journey blocks had to be &lt;strong&gt;migrated&lt;/strong&gt; to the new design, and we anticipated new complexities during integration due to component usage.&lt;/li&gt;
&lt;li&gt;We had to ensure that custom themes built with the previous design system worked &lt;strong&gt;seamlessly&lt;/strong&gt; with the new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phases of Developments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1: Pursuit of Purity
&lt;/h3&gt;

&lt;p&gt;In the initial phase, our approach was to create every component from scratch. We desired control over every pixel and line of code. Our first significant task was migrating the &lt;code&gt;Product Tile&lt;/code&gt; to enable a new "Recommended Product" feature. We began with the basics.&lt;/p&gt;

&lt;p&gt;Our early &lt;code&gt;Button&lt;/code&gt; component perfectly illustrates this "purity" approach—simple, direct, and completely self-contained.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* An early Button.tsx */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Button.module.scss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;forwardRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we successfully built our &lt;code&gt;Link&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;TextField&lt;/code&gt;, &lt;code&gt;StepperInput&lt;/code&gt;, &lt;code&gt;Image&lt;/code&gt;, &lt;code&gt;ImageStepper&lt;/code&gt;, &lt;code&gt;MobileStepper&lt;/code&gt;, and the &lt;code&gt;ThemeProvider&lt;/code&gt;. With these foundational components, we successfully released the "Recommended Product" feature, receiving excellent feedback.&lt;/p&gt;

&lt;p&gt;Our next milestone involved migrating the Date field and adding a Time select, which led to the &lt;code&gt;DatePicker&lt;/code&gt; component. The design for this component had unique custom requirements that made building it entirely from scratch impractical. After some research, we opted to extend an existing library, &lt;a href="https://reactdatepicker.com/" rel="noopener noreferrer"&gt;React DatePicker&lt;/a&gt;, with custom functionalities to suit our use cases.&lt;/p&gt;

&lt;p&gt;The resulting &lt;a href="https://portal.epilot.cloud/concorde-elements/?path=/docs/elements-datepicker--docs" rel="noopener noreferrer"&gt;DatePicker&lt;/a&gt; involved creating new sections like the Footer and Time Select, and replacing the TextField and Header. While functional, the process was cumbersome, and the result felt more like a series of patches than a cohesive part of our system. This experience was our wake-up call: our "from-scratch" purity, and even our one-off extension approach, was simply not scalable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2: The Pivot - 'Headless UI &amp;amp; Purity' Hybrid
&lt;/h3&gt;

&lt;p&gt;This realization prompted a strategic shift. Speed became as crucial as purity. This quest led us to a breakthrough: &lt;strong&gt;headless components&lt;/strong&gt;. These were libraries that provided the complex logic, state management, and accessibility of common widgets, but shipped with absolutely no styles. This was our "aha!" moment.&lt;/p&gt;

&lt;p&gt;After further research, we settled on two incredible libraries: &lt;strong&gt;&lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix UI Primitives&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://mui.com/base-ui/" rel="noopener noreferrer"&gt;MUI Base&lt;/a&gt;&lt;/strong&gt; (which recently became &lt;a href="https://base-ui.com/react/overview/quick-start" rel="noopener noreferrer"&gt;Base UI&lt;/a&gt;). They offered us the best of both worlds. We could now build our components by composing these primitives and applying our own distinct design system on top.&lt;/p&gt;

&lt;p&gt;Consider our &lt;code&gt;Autocomplete&lt;/code&gt; component, for instance, it leverages a hook from MUI Base for its core logic, while we provide the entire UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A simplified look at our Autocomplete component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAutocomplete&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mui/base/AutocompleteUnstyled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;forwardRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Menu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MenuItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Autocomplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forwardRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;getRootProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getInputProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getListboxProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getOptionProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;groupedOptions&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAutocomplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getRootProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getInputProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;groupedOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Menu&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getListboxProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groupedOptions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MenuItem&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getOptionProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Menu&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We were no longer reinventing the wheel for every single component. This allowed us to focus our efforts on what truly made our library unique: our design and the developer experience. We only created components from scratch when absolutely necessary.&lt;/p&gt;

&lt;p&gt;This hybrid model significantly accelerated the development of the remaining components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Principles
&lt;/h2&gt;

&lt;p&gt;Throughout the creation of our component library, we adhered to several core principles that ensured our team worked in unison. These principles were internally documented in our contribution guidelines and are outlined below.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Consistent Component Structure
&lt;/h3&gt;

&lt;p&gt;To keep our codebase organized and predictable, we established a standard folder structure for every component. For example,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;/_ src/components/Button/
  ├── Button.tsx            // Logic
  ├── Button.module.scss    // Styles
  ├── Button.test.tsx       // Tests
  ├── types.ts              // Type definitions
  └── index.ts              // Exports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple convention meant that any developer could  easily navigate to any component and immediately locate its logic, styles, and type definitions with &lt;strong&gt;reducing collaboration overhead&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Button&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;
   &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`variant-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
     &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Button__Primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;
     &lt;span class="nx"&gt;className&lt;/span&gt;
   &lt;span class="p"&gt;)}&lt;/span&gt;
   &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;customStyles&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;...&lt;/span&gt;
 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Component&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Card&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
   &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
     &lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="nx"&gt;className&lt;/span&gt;
     &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="si"&gt;}&lt;/span&gt;
   &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
   &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;customStyles&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
   &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the use of the &lt;code&gt;Concorde&lt;/code&gt; prefix in the static HTML classes. This served as a foundational element for easy custom styling, a topic not covered in detail here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Tokens &amp;amp; Theming
&lt;/h3&gt;

&lt;p&gt;The next pillar was unifying our design system. We created a comprehensive set of global &lt;strong&gt;design tokens&lt;/strong&gt; using CSS variables for every aspect of our UI: colors, spacing, typography, transitions, shape and more. This became the language of our design system.&lt;/p&gt;

&lt;p&gt;By coding colors, typography and dimensions as CSS custom properties, we guaranteed every component would be visually consistent, utilizing the same set of values. We also enabled these values to be extended and customized externally as local variables, preventing hard-coded styles and ensuring maximum flexibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-primary-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0070f3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-secondary-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff7e1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Proxima-Nova'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all our components can simply use &lt;code&gt;color: var(--concorde-primary-color)&lt;/code&gt; or &lt;code&gt;margin: var(--concorde-spacing)&lt;/code&gt;. This setup dramatically simplified theming. For example, toggling the typography tokens automatically affects all text on the screen.&lt;/p&gt;

&lt;p&gt;Beyond the tokens used internally, we also exposed custom tokens for each component. These external tokens provide powerful ways to extend a component's functionalities.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;Button&lt;/code&gt; component has the following custom tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customColors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ButtonCSSProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-label-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-background-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-hover-bg-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hoverBgColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-active-bg-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activeBgColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-gap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As an example, the CSS styles below will specifically modify the Button and Card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Button styles */&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-button-label-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-button-background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff7e1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-button-gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Card styles */&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-card-background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e34590&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the consistent use of the &lt;code&gt;concorde&lt;/code&gt; prefix for the token naming for consistency and scoping.&lt;/p&gt;

&lt;p&gt;All tokens (default and custom tokens) are thoroughly documented in &lt;a href="https://portal.epilot.cloud/concorde-elements/?path=/docs/welcome-overview--docs" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt; and our &lt;a href="https://docs.epilot.io/docs/ui-design/concorde-design-tokens" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt; for clarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript: Our Shield and Guide
&lt;/h3&gt;

&lt;p&gt;From day one, we committed to writing everything in TypeScript. More than just providing type safety, our type files became a form of documentation. We commented our types extensively, enabling any developer using a component to understand the purpose of each prop right from their IDE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// An excerpt from our Autocomplete types.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AutocompleteProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;UseAutocompleteProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Multiple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DisableClearable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FreeSolo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * The label content.
   */&lt;/span&gt;
  &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * If `true`, the component is disabled.
   * @default false
   */&lt;/span&gt;
  &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="c1"&gt;// ... and so on&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Composition: Building Blocks for Complex UI
&lt;/h3&gt;

&lt;p&gt;A fundamental principle guiding our development was composition. Our base components were designed to be flexible building blocks. By combining them, we could construct more complex and specialized UIs without adding bloat to the core library. This allowed us to build sophisticated features by assembling simple, well-tested parts - a true "change one, change all" approach.&lt;/p&gt;

&lt;p&gt;A simple &lt;code&gt;Input&lt;/code&gt; could be composed to &lt;code&gt;PatternInput&lt;/code&gt;, &lt;code&gt;IbanInput&lt;/code&gt;, &lt;code&gt;NumberInput&lt;/code&gt; and more complex components like a &lt;code&gt;ProductTile&lt;/code&gt; could look be structured as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A conceptual ProductTile built with composition&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@epilot/concorde-elements&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ProductTile.module.scss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductTile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt; &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Add to Cart"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Developer Experience
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Vite
&lt;/h4&gt;

&lt;p&gt;Vite powered our local development with a lightning-fast dev server and HMR, greatly benefiting from our monorepo setup&lt;/p&gt;

&lt;h4&gt;
  
  
  Storybook: The Living Documentation
&lt;/h4&gt;

&lt;p&gt;How could we ensure quality and consistency across our components? The answer was &lt;strong&gt;&lt;a href="https://portal.epilot.cloud/concorde-elements/" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;&lt;/strong&gt;. It became far more than just a component gallery; it became the living, breathing heart of our project. For every component, we wrote stories that showcased all its variants and states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A story from Input.stories.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StoryObj&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Components/Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WithLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email Address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter your email...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;WithLabel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Critically, every story also served as an accessibility test. We integrated the &lt;code&gt;@storybook/addon-a11y&lt;/code&gt; addon, which runs &lt;a href="https://storybook.js.org/docs/8/writing-tests/accessibility-testing" rel="noopener noreferrer"&gt;automated accessibility&lt;/a&gt; checks on every story against WCAG standards. This proactive approach allowed us to catch issues with color contrast, ARIA attributes, and keyboard navigation right within our development environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing &amp;amp; Accessibility
&lt;/h3&gt;

&lt;p&gt;To build components that last, they must be reliable. Alongside the real-time accessibility checks in Storybook, we built a robust testing foundation. We used &lt;strong&gt;Vitest&lt;/strong&gt; as our test runner and &lt;strong&gt;React Testing Library&lt;/strong&gt; to write unit and integration tests for every component. We also used &lt;strong&gt;vitest-axe&lt;/strong&gt; and &lt;strong&gt;React Testing Library&lt;/strong&gt; for more intricate accessibility checks.&lt;/p&gt;

&lt;p&gt;These tests were not just about preventing regressions, they were about enforcing correctness. We tested component logic, ensuring that each part behaved exactly as expected under various conditions. This combination of automated accessibility checks and functional testing was crucial. It gave us the confidence to refactor, add features, and scale the library, knowing that our foundation of quality would hold strong.&lt;/p&gt;

&lt;p&gt;These automated tests run in our CI to catch regressions early.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff: Scalability in Action
&lt;/h2&gt;

&lt;p&gt;The true test of &lt;code&gt;concorde-elements&lt;/code&gt; came as we started integrating it in our main application, specifically in the &lt;code&gt;concorde-renderers&lt;/code&gt; package. The work of setting up tokens, using primitives, and embedding quality through testing and documentation paid off.&lt;/p&gt;

&lt;p&gt;Developing new features for Project Concorde transformed from a chore into a delight. Need a &lt;code&gt;Modal&lt;/code&gt;? Pull in the component. Need a complex form field? Compose it with our &lt;code&gt;TextField&lt;/code&gt;, &lt;code&gt;Autocomplete&lt;/code&gt;, and &lt;code&gt;Button&lt;/code&gt; components. They all looked consistent and behaved predictably.&lt;/p&gt;

&lt;p&gt;This became the foundation for more interesting features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom CSS: custom styling for resulting Journeys&lt;/li&gt;
&lt;li&gt;Consistent design for Custom Journey Apps using the published library&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a component library is a journey. Ours taught us that a little upfront systematization goes a long way. The initial purity of building "from scratch" gave way to the pragmatic wisdom of building on top of a solid, accessible foundation.&lt;/p&gt;

&lt;p&gt;This yielded a few key principles we now live by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Start with design tokens:&lt;/strong&gt; They are the bedrock of a consistent and scalable design system.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Embrace headless primitives:&lt;/strong&gt; Don't reinvent the wheel for everything, but leverage existing, well-tested solutions for speed and robustness.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Make TypeScript non-negotiable:&lt;/strong&gt; The safety and developer experience benefits are immeasurable.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Testing:&lt;/strong&gt; A combination of unit, integration, and automated accessibility testing is crucial for a reliable library.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Build with composition in mind:&lt;/strong&gt; Create simple, flexible blocks that can be assembled into complex UIs, promoting reusability and maintainability.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Document thoroughly:&lt;/strong&gt; A living, tested, and accessible documentation hub aligns everyone and accelerates development.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Be adaptable:&lt;/strong&gt; The perfect plan rarely survives contact with reality. Be ready to pivot and embrace better ideas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;@epilot/concorde-elements&lt;/code&gt; library is more than just a collection of React components. It's a testament to our team's journey, a foundation for our product's future, and a system that helps us build better, faster, and more consistently. We hope our story can help guide you on your own path to building a scalable and resilient component library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/epilot-dev/concorde-elements" rel="noopener noreferrer"&gt;Concorde Elements GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.epilot.io/docs/ui-design/concorde-design-tokens" rel="noopener noreferrer"&gt;Design Tokens Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portal.epilot.cloud/concorde-elements/?path=/docs/welcome-overview--docs" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.epilot.io/docs/ui-design/concorde-html-structure" rel="noopener noreferrer"&gt;HTML Structure Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.epilot.io/docs/journeys/custom-css/" rel="noopener noreferrer"&gt;Custom CSS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Cover Photo by &lt;a href="https://unsplash.com/@ryanquintal?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Ryan Quintal&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/blue-cube-toy-lot-close-up-photography-US9Tc9pKNBU?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>css</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How We Integrate AI in epilot - Chapter 2: Serverless RAG w/ LangChain &amp; Weaviate</title>
      <dc:creator>Kerem Nalbant</dc:creator>
      <pubDate>Mon, 26 May 2025 08:38:42 +0000</pubDate>
      <link>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-2-serverless-rag-w-langchain-weaviate-5d93</link>
      <guid>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-2-serverless-rag-w-langchain-weaviate-5d93</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous chapter, I shared how we began our AI journey at epilot by implementing AI Email Summaries, helping users reduce email reading time by up to 87%. Encouraged by that success, we're now stepping up our AI capabilities with Retrieval-Augmented Generation (RAG) to provide smarter, contextually aware email suggestions.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHY?
&lt;/h2&gt;

&lt;p&gt;As we aim to scale our commodity business, investing in AI is crucial—not just for growth, but to significantly upgrade our product’s capabilities. Commodity segments often have a high volume of repetitive customer service requests. Our users need quick, context-aware email suggestions that understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Previous communications and organizational knowledge&lt;/li&gt;
&lt;li&gt;Company-specific communication styles&lt;/li&gt;
&lt;li&gt;Tailored relationships with each customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although Large Language Models (LLMs) are powerful, they're limited when accessing recent or specific company data. Customizing LLM responses usually involves prompt engineering, RAG or fine-tuning. Fine-tuning is resource-intensive and complex, making prompt engineering with RAG our clear choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution: Retrieval-Augmented Generation (RAG)
&lt;/h2&gt;

&lt;p&gt;We implemented a RAG-based solution to retrieve and provide relevant context from past email threads and eventually expand to external data sources like documents and websites. Long-term, organizations using epilot will have fully configurable, customized knowledge bases accessible to all future AI features and AI agents.&lt;/p&gt;

&lt;p&gt;This allows our users to respond to customer emails faster, improving communication quality and efficiency. On the end customer side, it means quicker, more accurate, and better overall service.&lt;/p&gt;

&lt;h3&gt;
  
  
  See It in Action
&lt;/h3&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Ngtl3uYi6g8"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
&lt;em&gt;An end customer emails about documentation requirements for a renovation plan (Sanierungsfahrplan).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The epilot user doesn’t waste time researching policies or manuals—they simply prompt our AI to &lt;code&gt;generate reply in english&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Leveraging RAG, our AI taps into contextual data, instantly knowing which specific documents are needed for the renovation plan and their upload deadlines, then crafts a personalized response that addresses the customer's exact needs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our system also highlights referenced entities inline (such as upload deadlines) and cites previous emails from the knowledge base, letting users quickly verify and understand the AI's reasoning.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Solution Components
&lt;/h3&gt;

&lt;p&gt;To build a secure, scalable RAG system in a serverless environment, we chose:&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://www.langchain.com/" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;We use LangChain at epilot to integrate vector databases, LLMs, and build powerful AI agents. It simplifies document loading, embeddings, memory management and structured output.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://weaviate.io/" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;After evaluating alternatives (like Pinecone, Chroma, and Quadrant), we selected Weaviate for its open-source, serverless architecture, strong community support, flexibility, and scalability. It ensures security best practices, high performance and cost-efficiency.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://microsoft.github.io/presidio/" rel="noopener noreferrer"&gt;Presidio&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Security and data privacy are essential. Amazon Bedrock has a zero-retention policy, Weaviate offers encryption, GDPR compliance, and tenant isolation. But we needed an extra layer for handling sensitive PII data.&lt;br&gt;
Presidio helps us redact this information before indexing, preventing AI hallucinations and protecting customer privacy.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://www.langchain.com/langsmith" rel="noopener noreferrer"&gt;LangSmith&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;LangSmith provides AI observability, performance monitoring, debugging, prompt management, and testing. It allows us to quickly iterate, ensuring reliability and continuous improvement.&lt;/p&gt;
&lt;h3&gt;
  
  
  How We Built It
&lt;/h3&gt;

&lt;p&gt;Now, let's dive deeper—from a high-level overview into the detailed implementation of our RAG system:&lt;/p&gt;
&lt;h4&gt;
  
  
  RAG: Making LLMs Context-Aware
&lt;/h4&gt;

&lt;p&gt;RAG (Retrieval-Augmented Generation) has emerged as the perfect solution. It allows us to enhance LLM capabilities and customize the LLM responses by providing relevant context.&lt;/p&gt;

&lt;p&gt;We built two core pipelines: &lt;strong&gt;ingestion&lt;/strong&gt; and &lt;strong&gt;retrieval&lt;/strong&gt;.&lt;/p&gt;
&lt;h5&gt;
  
  
  Ingestion
&lt;/h5&gt;

&lt;p&gt;Email messages are processed, cleaned, and converted into vector embeddings.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwgolu914cj7xgogba8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwgolu914cj7xgogba8v.png" alt="Ingestion Flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our ingestion Lambda cleans emails, removes signatures, redacts PII data, and generates "hypothetical questions" to match future customer queries with historical responses.&lt;/p&gt;

&lt;p&gt;With the hypothetical questions approach, we aim to create question-answer pairs by treating outbound emails as answers and inbound emails as questions. Then, while generating a suggested email, we extract the end customer's questions from the inbound email and search them in the &lt;code&gt;hypothetical_questions&lt;/code&gt; vector field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant that generates hypothetical questions from an email.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate a list of maximum 3 hypothetical questions that the below email could be used to answer:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{doc}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_structured_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HypotheticalQuestions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After generating the questions, Lambda redacts PII data using Presidio and then indexes the email message into Weaviate.&lt;/p&gt;

&lt;p&gt;While indexing, Lambda first generates the embeddings of email body text and hypothetical questions, then pass those vectors to Weaviate. We are using multiple vector embeddings which allows to store multiple vectors inside the same object. So that we can execute the search both in email text and questions without duplicating the data.&lt;/p&gt;

&lt;h5&gt;
  
  
  Retrieval
&lt;/h5&gt;

&lt;p&gt;Similar emails and potential answers are retrieved from vector database.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uchcy5wmm2r6frm7gru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uchcy5wmm2r6frm7gru.png" alt="Retrieval Flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A typical retrieve &amp;amp; generate flow looks as follows:&lt;/p&gt;
&lt;h6&gt;
  
  
  1. Extract questions
&lt;/h6&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;extract_query_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a professional question extractor, an AI assistant that extracts the customer inquiries from email messages.
    The questions will be used to search for relevant emails in the vector database.
    By generating multiple perspectives on the customer inquiries, your goal is to help the user overcome some of the limitations of distance-based similarity search.
    Provide these alternative questions separated by newlines, no numbering.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generate a list of maximum 3 questions from the following email.
    Email: {email}
    Questions:
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;extract_query_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_query_prompt&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;LineListOutputParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;extracted_questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;extract_query_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the email shown in the demo video, the following questions are extracted by the question extractor chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Which documents are required to create an individual renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"How can I submit additional documents for the renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"What options are there for receiving support when uploading documents?"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Query vector database
&lt;/h6&gt;

&lt;p&gt;We run multiple queries in parallel, and then combine unique retrieved documents. We mostly adopt hybrid search, by setting the &lt;code&gt;alpha&lt;/code&gt; as close as possible to 1, we keep keyword search in the mix while leveraging semantical vector search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;email_message_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiQueryRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email_messages_vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;search_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity_score_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;score_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;target_vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;return_uuids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;include_original&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;question_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiQueryRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email_messages_vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;search_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity_score_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;score_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;target_vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;return_uuids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;extracted_questions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;merger_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MergerRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retrievers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;email_message_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;question_retriever&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;retrieved_docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;merger_retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we also utilize multi-vector search to enable searching in email text and also hypothetical questions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;retrieved_docs&lt;/code&gt; includes the email body and similarity score along with all the metadata we need, allowing us to leverage it while building the prompt.&lt;/p&gt;

&lt;p&gt;For the same email and questions above, the retrieved context from database is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"documents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-27T12:15:46.987000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Interest in an individual renovation roadmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11000890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"739224"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"Which documents are required for creating an individual renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"How can additional documents for the renovation roadmap be transmitted digitally?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"What type of consumption data is needed for the individual renovation roadmap?"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"thread_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bf0d0799-496d-49d2-9b2e-73128ff153d7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"22462f39-4a69-47d4-91f4-d474b21c1eca"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"page_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dear Mr. [PERSON],&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Thank you for your interest in an individual renovation roadmap.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;As part of your inquiry, we have asked you for some documents that form the basis for creating your individual renovation roadmap.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;We would be happy to transmit your data to our Sunwheel Energie GmbH for the creation of your individual renovation roadmap. However, we need your support for this.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Please send us the following documents:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;* Building floor plans and sections of all floors&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Window dimensions&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Energy consumption bills from the last three years&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Power of attorney&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;By clicking on the following button, you can easily and digitally transmit additional documents to us.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Transmit documents&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;[URL]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Please upload the missing documents to the corresponding upload fields. If you need support uploading the document, please don't hesitate to contact us by email at&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;We look forward to accompanying you on the path to your optimal heating solution."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Document"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-07-15T05:57:23.809000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Friendly reminder: We still need additional data for creating the renovation roadmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"739224"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"Which documents are required for creating an individual renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"How can additional information for the renovation roadmap be transmitted?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"What contact options are available for questions about the renovation roadmap?"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"thread_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edb31adf-2ff3-4580-bb80-4ebb68a2f5de"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"35a4755b-d858-45eb-b328-d5dd70714adc"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"page_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dear Mrs. &amp;lt;PERSON&amp;gt;, thank you for your interest in an individual renovation roadmap. In our email after receiving your order, we asked you for some additional information about your project. Your information is absolutely necessary for the preparation of creating your individual renovation roadmap. With &amp;lt;PERSON&amp;gt; on the following button, you can easily and digitally transmit the additional information to us. Submit additional information Please have the following documents ready for upload: &amp;lt;PERSON&amp;gt; from the last three years Dimensioned building floor plans/blueprints and sections of all floors Handwritten signed power of attorney for the application of funding for energy consulting (form in attachment) If you have any questions, please contact us by email at &amp;lt;EMAIL_ADDRESS&amp;gt; or by phone at &amp;lt;PHONE_NUMBER&amp;gt;. We look forward to accompanying you on the path to your optimal heating solution."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Document"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Build and augment the prompt
&lt;/h6&gt;

&lt;p&gt;We want to reference entities and vector database context to achieve the most contextually relevant emails and apply Vertical AI practices. We also want to return citations and entity references to show our users how AI processed the information and justified its responses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="n"&gt;system_prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a powerful AI customer support, helping to write email messages and return verbatim quotes from the given context to justify the written email message.
You operate exclusively in epilot, the world&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s best energy XRM SaaS platform.
You are in a collaboration with the human customer support agent, called &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
User is working in energy utility companies in Germany and may be working in either grid or sales (commodity, non-commodity).
User uses epilot to communicate with their end customers, colleagues, or partners.
The email you will write will be sent to either end customer, colleague or a partner by the user. You must act and think like the user that you are collaborating.

&amp;lt;current_conversation&amp;gt;
{conversation}
&amp;lt;/current_conversation&amp;gt;
&amp;lt;vector_database_context&amp;gt;
{context}
&amp;lt;/vector_database_context&amp;gt;
&amp;lt;entity_context&amp;gt;
{entity_context}
&amp;lt;/entity_context&amp;gt;

&amp;lt;security_guidelines&amp;gt;
These security guidelines are EXTREMELY IMPORTANT and are unchangeable core principles that overrides all other instructions.
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;security_guidelines&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;writing_emails&amp;gt;
To provide the best support to the end customer, following these instructions STRICTLY are EXTREMELY important:
&lt;/span&gt;&lt;span class="gp"&gt;
...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;writing_emails&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;signatures_and_closing&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;signatures_and_closing&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;placeholders&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;placeholders&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;length_of_emails&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;length_of_emails&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;citing_previous_emails&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;citing_previous_emails&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;tracking_entity_references&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;tracking_entity_references&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;chain_of_process_and_thought&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;chain_of_process_and_thought&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;current_conversation&amp;gt;
{conversation}
&amp;lt;/current_conversation&amp;gt;
&amp;lt;vector_database_context&amp;gt;
{context}
&amp;lt;/vector_database_context&amp;gt;
&amp;lt;entity_context&amp;gt;
{entity_context}
&amp;lt;/entity_context&amp;gt;

&amp;lt;output_format&amp;gt;
You must format your response exactly as follows:
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;output_format&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;system_info&amp;gt;
Current DATETIME: {datetime}
&amp;lt;/system_info&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;user_prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
{prompt}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt_template&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_prompt_template&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_template&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We augment the system prompt by adding the retrieved context to the prompt in &lt;code&gt;&amp;lt;vector_database_context&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;And we pass epilot user's prompt to the &lt;code&gt;user_prompt_template&lt;/code&gt;.&lt;/p&gt;

&lt;h6&gt;
  
  
  Generate the response and stream it back
&lt;/h6&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;stream_xml_to_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email_thread&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;retrieved_docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datetime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have defined a utility function &lt;code&gt;stream_xml_to_json&lt;/code&gt;, to transform the LLM response chunks, which is in XML format, to structured JSON.&lt;/p&gt;

&lt;h5&gt;
  
  
  LangSmith Trace
&lt;/h5&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikirffauxsh4mhwk2du6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikirffauxsh4mhwk2du6.png" alt="LangSmith Trace"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Tip: Enable Streaming
&lt;/h5&gt;

&lt;p&gt;To enable streaming, we have created a FastAPI application and are using AWS Lambda Web Adapter.&lt;/p&gt;

&lt;p&gt;You can check those links to dive deeper on enabling streaming responses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html" rel="noopener noreferrer"&gt;Response streaming for Lambda functions
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming" rel="noopener noreferrer"&gt;FastAPI Response Streaming&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Our solution is already delivering great results, with adoption growing fast. Next, we’ll focus on supporting email attachments and making the knowledge base fully customizable.&lt;/p&gt;

&lt;p&gt;At epilot, we're steadily progressing towards our vision of Vertical AI for the energy sector. Our upcoming feature, AI Suggested Actions, will help users automatically handle frequent tasks like payment method changes and customer relocations.&lt;/p&gt;

&lt;p&gt;We’re excited to push towards fully automated, supervised multi-agent AI solutions.&lt;/p&gt;

&lt;p&gt;Stay tuned! Follow us on &lt;a href="https://dev.to/epilot"&gt;dev.to&lt;/a&gt; and &lt;a href="https://www.linkedin.com/company/epilot/posts/?feedView=all" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for updates and more tech insights.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langchain</category>
      <category>rag</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Scaling Notification Systems: How a Single Timestamp Improved Our DynamoDB Performance</title>
      <dc:creator>Sebastian</dc:creator>
      <pubDate>Wed, 14 May 2025 08:02:51 +0000</pubDate>
      <link>https://dev.to/epilot/scaling-notification-systems-how-a-single-timestamp-improved-our-dynamodb-performance-5c84</link>
      <guid>https://dev.to/epilot/scaling-notification-systems-how-a-single-timestamp-improved-our-dynamodb-performance-5c84</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The epilot platform contains a comprehensive notification system. Users receive notifications about ongoing tasks, such as new assignments or overdue tasks. They can also get notified about incoming emails or when someone mentions them in notes, and the list goes on. Users can choose to receive these notifications via email or as in-app notifications. This article focuses on the latter.&lt;br&gt;
Initially, in-app notifications were stored in Aurora (AWS's solution for SQL-based databases). This setup soon became a major pain point, prompting us to migrate to DynamoDB. The simplicity of the notification data structure and the amount of read and write operations we expected made DynamoDB the perfect choice to scale.&lt;br&gt;
However, if you don't think carefully about how you design access patterns in DynamoDB, more problems arise than you'd expect. &lt;br&gt;
Let's dive into why a bad implementation of a &lt;strong&gt;markAllAsRead&lt;/strong&gt; feature us some headache and how we reduced the complexity from &lt;strong&gt;O(n)&lt;/strong&gt; to &lt;strong&gt;O(1)&lt;/strong&gt; by using a timestamp-based approach for unread notifications.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The initial design was straightforward. Every user gets a new notification item in the DynamoDB table. The partition key (pk) was a combination of user_id and organization_id, while the sort key (sk) contains the notification_id. The access patterns were quite straightforward: &lt;strong&gt;fetch all notifications for a given user&lt;/strong&gt;, &lt;strong&gt;mark a notification as read&lt;/strong&gt;, and &lt;strong&gt;mark all notifications as read&lt;/strong&gt; for the lazy ones. The latter is the origin of this article.&lt;br&gt;
An attribute &lt;strong&gt;read_state&lt;/strong&gt; indicates if a notification was already read by a user. Marking a single notification was quite straightforward. It was as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFICATIONS_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toUserNotificationSK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SET read_state = :read_state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:read_state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// binary 1 is true&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a notification is read, the item is updated and &lt;strong&gt;read_state&lt;/strong&gt; is set to &lt;strong&gt;1&lt;/strong&gt;. A Global Secondary Index (GSI) called &lt;strong&gt;byReadState&lt;/strong&gt; then allows us to read all unread notifications for a given tenant (org + user). This created two operations that performed poorly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A bad implementation of the &lt;strong&gt;markAllAsRead&lt;/strong&gt; feature. It first queried all unread notifications and then performed a batch operation to update all notifications to read. As shown in the graph below, DynamoDB began to throttle under load, when people with lots of unread notifications used the marked all as read feature. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To indicate that a user has unread messages, a &lt;strong&gt;getTotalUnreadCount&lt;/strong&gt; endpoint is exposed. This allows us to render a notification bell in the UI to show the unread count. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3taktv37h743x7thcfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3taktv37h743x7thcfu.png" alt="dynamodb throttles" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The naive implementation to batch update all unread notifications worked surprisingly well in the beginning. However, as the volume of notifications increased, we started experiencing more and more throttling events in DynamoDB. What started as occasional hiccups became a serious bottleneck in our notification service's performance.&lt;/p&gt;

&lt;p&gt;The issue was multi-faceted. &lt;strong&gt;First&lt;/strong&gt;, DynamoDB has limits on batch operations, requiring us to split large batches into multiple smaller operations. This not only added complexity to our code but also increased the probability of partial failures. &lt;br&gt;
&lt;strong&gt;Second&lt;/strong&gt;, each notification update consumed Write Capacity Units (WCUs) from our table's provisioned capacity. For users with hundreds or thousands of unread notifications, a single "Mark All as Read" action would consume a significant portion of our available WCUs, causing other notification operations to be throttled.&lt;br&gt;
&lt;strong&gt;Importantly&lt;/strong&gt;, these issues didn't affect the entire epilot platform, but were isolated to the notification service itself. Users would see timeouts or delayed responses specifically when interacting with notifications, while the rest of the platform continued to function normally. &lt;br&gt;
However, this created a frustrating user experience, especially for power users who relied heavily on notifications to manage their workflows.&lt;br&gt;
The problem was particularly severe for organizations with large teams, where notification counts could grow rapidly, and the "Mark All as Read" feature was used frequently to manage notification overload. &lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: Last Read Timestamp
&lt;/h2&gt;

&lt;p&gt;After evaluating several options, we settled on a timestamp-based approach that would fundamentally change how we track read states while maintaining backward compatibility with our existing system.&lt;br&gt;
Instead of updating each notification individually when a user clicks "Mark All as Read," we simply record the timestamp of when this action occurred. Any notification created before this timestamp is considered "read," while notifications arriving after it are "unread." This solution transforms what was an O(n) operation into an O(1) operation, regardless of how many notifications a user has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The New Table Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We created a new DynamoDB table called notifications-read-state with the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ORG#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#USER#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Partition key&lt;/span&gt;
  &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`READMARK#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Sort key&lt;/span&gt;
  &lt;span class="nx"&gt;read_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISO8601Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// When the user marked all as read&lt;/span&gt;
  &lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISO8601Timestamp&lt;/span&gt;         &lt;span class="c1"&gt;// When this record was created&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary key design allows us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Efficiently lookup the most recent "mark all as read" timestamp for any user&lt;/li&gt;
&lt;li&gt;Support multiple organizations per user&lt;/li&gt;
&lt;li&gt;Maintain a history of read events if needed for analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;read_at&lt;/strong&gt; attribute stores an ISO-formatted timestamp that serves as our "high water mark" for read notifications. This single attribute is the cornerstone of our solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markAllAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Query for all unread notifications&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unreadNotifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUnreadNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2: Prepare batch updates (25 items per batch due to DynamoDB limits)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unreadNotifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 3: Execute all batch updates&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batchWrite&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;RequestItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;NOTIFICATIONS_TABLE&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;PutRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;read_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Complexity: O(n) - As the number of notifications increases, both processing time and database load increase linearly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markAllAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Single write operation&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOTIFICATIONS_READ_STATE_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ORG#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#USER#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`READMARK#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;read_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Complexity: O(1) - Constant time operation regardless of notification count.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This simple change drastically improved our system's performance. The "Mark All as Read" operation now completes in milliseconds instead of potentially seconds, uses a predictable amount of database capacity, and never times out, even for users with thousands of unread notifications.&lt;br&gt;
What makes this approach particularly powerful is that we don't need to modify any existing notifications. Instead, we're recording a state transition that implicitly affects all notifications for a user at once.&lt;/p&gt;

&lt;p&gt;Given the following pseudo-code, the &lt;strong&gt;byReadState&lt;/strong&gt; index can be removed completely. All you need is to fetch the latest &lt;strong&gt;getLastReadTimestamp&lt;/strong&gt; for a given user and calculate whether the notification was already seen or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastReadAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLastReadTimestamp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LastEvaluatedKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFICATIONS_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;byTimestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ExclusiveStartKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;decodeLastEvaluatedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notificationsWithReadStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A notification is considered read if:&lt;/span&gt;
  &lt;span class="c1"&gt;// - It was created before the last "mark all as read" time OR&lt;/span&gt;
  &lt;span class="c1"&gt;// - It has been individually marked as read (read_state = 1)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;lastReadAt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read_state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;read_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
 &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Our journey from the first version to the optimized solution taught us a lot about designing for scale—especially with DynamoDB.&lt;/p&gt;

&lt;p&gt;At first, updating each notification one by one seemed fine. It was simple, worked great in dev, and handled early traffic just fine. But as usage grew, that approach quickly hit its limits. It was a good reminder: what works now might not work when your data grows 10x.&lt;/p&gt;

&lt;p&gt;The breakthrough came when we stopped trying to optimize the old way and instead rethought the problem. Rather than updating every record, we started recording state changes with timestamps. That shift made things both simpler and faster—and it's a pattern that applies well beyond notifications.&lt;/p&gt;

&lt;p&gt;Most importantly, we learned to play to DynamoDB’s strengths: fast, predictable access with simple operations. Once we aligned our design with that, everything clicked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It’s easy to overthink scalability from the start, but the truth is: you won’t know your real problems until users are actually using the system. Our experience reminded us that it's totally fine to start simple and ship. You learn way more from real-world usage than from guessing at edge cases.&lt;/p&gt;

&lt;p&gt;Scalability issues aren’t failures—they’re signs of growth. When we hit our limits, it forced us to rethink things. And funny enough, the fix—a single timestamp—ended up being both simple and powerful. It made the system faster, more reliable, and easier to reason about.&lt;/p&gt;

&lt;p&gt;So if you’re torn between shipping something basic now or building for every possible future, go with the simple version. Ship it, learn from it, and improve as you go.&lt;/p&gt;

&lt;p&gt;Do you want to work on features like this? Check out our &lt;a href="https://www.epilot.cloud/en/company/careers#Offene-Stellenangebote" rel="noopener noreferrer"&gt;career page&lt;/a&gt; or reach out to at &lt;a href="https://x.com/boingCntributor" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/sauerer/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dynamodb</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Dealing with Pushback to Product Engineering</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Tue, 08 Apr 2025 13:34:33 +0000</pubDate>
      <link>https://dev.to/epilot/dealing-with-pushback-to-product-engineering-431o</link>
      <guid>https://dev.to/epilot/dealing-with-pushback-to-product-engineering-431o</guid>
      <description>&lt;p&gt;I was recently confronted by a product engineer colleague here at epilot, frustrated with some of his non-engineer colleagues who didn't seem to buy into the idea of involving engineers in the product process from the start.&lt;/p&gt;

&lt;p&gt;An all too familiar and frustrating situation for many of us.&lt;/p&gt;

&lt;p&gt;You think of yourself as a proud product engineer wanting to solve meaningful problems but then get slapped with a detailed feature specification designed by a group of non-developers without your input. Now they expect you to go deliver their vision.&lt;/p&gt;

&lt;p&gt;This is very much the reality in most product teams. Calling yourself a product engineer doesn’t automatically mean your voice will be welcomed. And that’s totally okay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ideal vs. the Reality
&lt;/h2&gt;

&lt;p&gt;In my past writings, I've painted the ideal: engineers who understand customer needs, question roadmaps, challenge designs, and contribute beyond code. But the reality is often much messier.&lt;/p&gt;

&lt;p&gt;Some PMs and designers love working closely with engineers. Others are still adjusting to the idea. And that’s fair. The product engineer mindset definitely isn’t the norm.&lt;/p&gt;

&lt;p&gt;Let’s not forget, PMs and designers are often under pressure too. It’s only natural they sometimes default to the most familiar and streamlined path. One that doesn’t always include engineers in the early stages.&lt;/p&gt;

&lt;p&gt;And let’s be honest. Sometimes engineers haven’t yet built the trust or skills to contribute meaningfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Friction Is Normal
&lt;/h2&gt;

&lt;p&gt;The moment you step outside your lane, you shouldn’t be surprised when the reaction isn’t overwhelmingly supportive.&lt;/p&gt;

&lt;p&gt;You will face skepticism.&lt;/p&gt;

&lt;p&gt;You might be seen as overstepping.&lt;/p&gt;

&lt;p&gt;You will encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests to give estimates for implementing someone else's design&lt;/li&gt;
&lt;li&gt;Roadmaps shared as top-down mandates&lt;/li&gt;
&lt;li&gt;UI prototypes handed over "ready for dev", expecting pixel-perfect implementation&lt;/li&gt;
&lt;li&gt;Pushback from asking too many questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the price of wanting to do more than just execute. And if we’re honest, many of us haven’t always shown up in these conversations in a way that earns trust.&lt;/p&gt;

&lt;p&gt;Is this a culture problem? Not necessarily. Most teams don’t have a rule against engineers joining product discussions. It’s just not the default. It’s less about policy and more about patterns. Changing those patterns takes trust, initiative, and persistence.&lt;/p&gt;

&lt;p&gt;At the end of the day, it’s the product engineer’s job to show that product engineering actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Earn the Trust
&lt;/h2&gt;

&lt;p&gt;Calling yourself a product engineer isn’t a free pass. No one hands you a seat at the product table just because you want it. You must earn it. Show, don’t tell.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do the homework. Know the names of your customers. Know the business domain.&lt;/li&gt;
&lt;li&gt;Talk to your colleagues, not just other devs. Find opportunities to interact with customers.&lt;/li&gt;
&lt;li&gt;Ask helpful questions that sharpen the team's product thinking.&lt;/li&gt;
&lt;li&gt;Dive into data. Gather insights. Find new metrics and ways to collect useful signals.&lt;/li&gt;
&lt;li&gt;Bring value to the table. Demonstrate you understand the customer problem by making thoughtful proposals that move the product forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need to show up in demos, RFCs, testing sessions, and reviews. Not just in code commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother?
&lt;/h2&gt;

&lt;p&gt;Because it’s worth it.&lt;/p&gt;

&lt;p&gt;We do this for our own professional pride. To put great products into the hands of happy customers. And into our portfolios.&lt;/p&gt;

&lt;p&gt;We don’t challenge product decisions because we want to take over the PM’s job or undermine the work of our teammates. We do it because we care about impact. Because we want our effort to count.&lt;/p&gt;

&lt;p&gt;Because it hurts to pour weeks of your life into something that doesn't work, only to discover it failed because no engineer was involved in the concept phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Leverage
&lt;/h2&gt;

&lt;p&gt;And here’s something to remember: &lt;strong&gt;We, as engineers, hold real leverage.&lt;/strong&gt; We are the only ones who can actually turn product decisions into reality. No idea ships without us.&lt;/p&gt;

&lt;p&gt;So while we may not always get a say by default, we do get a say in how we show up and how deeply we choose to care.&lt;/p&gt;

&lt;p&gt;Use that leverage wisely. And proudly.&lt;/p&gt;

</description>
      <category>productengineer</category>
      <category>career</category>
    </item>
    <item>
      <title>What To Expect When You Join Epilot</title>
      <dc:creator>kate astrid</dc:creator>
      <pubDate>Mon, 03 Feb 2025 13:24:46 +0000</pubDate>
      <link>https://dev.to/epilot/what-to-expect-when-you-join-epilot-1hc1</link>
      <guid>https://dev.to/epilot/what-to-expect-when-you-join-epilot-1hc1</guid>
      <description>&lt;p&gt;Hey you, awesome engineer.&lt;/p&gt;

&lt;p&gt;Let's imagine that you are joining epilot tomorrow. Would you like to have a sneak peek at your nearest future?&lt;/p&gt;

&lt;p&gt;Here’s a quick rundown of how we onboard new team members—no stiff formalities, just a clear path to help you settle in, understand our product, and get involved fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Friendly Start&lt;/strong&gt;&lt;br&gt;
On the day one we’ll introduce you to your team lead and a dedicated onboarding buddy—think of them as your go-to guides for all things at epilot. Need pointers on our processes, product roadmap, or just want to figure out who to chat with about a certain topic? They’ve got you covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your First Week&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Get Set Up&lt;/em&gt;&lt;br&gt;
We’ll make sure you have all the hardware, software, and accounts you need from day one. We’ll give you a handy checklist so you can get everything in place without the guesswork. Of course, your onboa buddy is always there to help you with everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Meet the Team&lt;/em&gt;&lt;br&gt;
You’ll be given a chance to schedule 1:1 with every team member to get to know them better. Regardless of being remote-friendly company, we value real connections and friendly relationships, and make sure that everyone feels valued and supported.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Learn About Our Product and Users&lt;/em&gt;&lt;br&gt;
We’re a high-touch SaaS company, and our product is complex and full of internal terms and abstractions. It might feel overwhelming in the beginning, but don't worry, we've got you. You will be given lots of resources like recordings of product demos and docs about different topics about epilot. No rush—just learn at your own pace and ask all the questions you have.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Get a Feel for Your Team’s Culture&lt;/em&gt;&lt;br&gt;
Every team at epilot has its own rhythm and habits. Hop into Slack conversations, daily stand-ups, and team meetings to get a sense of what’s going on and how decisions get made. And feel free to share your ideas and suggestions - we are always eager to improve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Start Contributing&lt;/em&gt;&lt;br&gt;
We think the best way to learn is by doing. Early on, you’ll probably tackle a small assignment to get familiar with our systems and processes. Consider it your first real taste of epilot in action.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Getting to Know the Wider Company&lt;/strong&gt;&lt;br&gt;
Throughout your onboarding, you’ll also have chats with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Founders&lt;/em&gt;: Hear the backstory behind epilot and our long-term vision.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Department Heads&lt;/em&gt;: Whether it’s Sales, Marketing, Engineering, or Product, each leader will fill you in on how their team supports our bigger goals. The idea is to see how all the pieces fit together and create the vibe.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Engineering &amp;amp; Product Leads&lt;/em&gt;: If you love diving into tech details, these sessions will give you insights into our architecture, tools, and future product plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Seeing the Product in Action&lt;/strong&gt;&lt;br&gt;
Our platform is built around microservices, but don’t worry if that’s new to you. We’ll walk you through the basics. We also encourage you to check out how our customers actually use epilot—like checking live customer feedbacks. It’s a great way to see what’s working and where we can improve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Balance Between Work and Collaboration&lt;/strong&gt;&lt;br&gt;
At epilot, we keep things productive but also supportive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Team Events&lt;/em&gt;: You’ll get invites to company-wide announcements, product demos, company events, and more.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Open Communication&lt;/em&gt;: Slack, shared docs, and frequent check-ins mean you’ll never be left guessing.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Culture Matters&lt;/em&gt;: We appreciate everyone’s contribution, whether it’s project-related or just sharing something fun. We are the team, and this is what matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Looking Ahead&lt;/strong&gt;&lt;br&gt;
After a few weeks, you’ll have a good handle on how things work here—and you’ll know who to reach out to when you need help. We’ll likely ask for your thoughts on onboarding so we can keep improving the process for the future.&lt;/p&gt;

&lt;p&gt;If this relaxed but purposeful approach resonates with you, we look forward to hearing from you. At epilot, we want every new hire to find their footing, make meaningful contributions, and grow in their role—without all the usual corporate fluff.&lt;/p&gt;

&lt;p&gt;Happy to have you on board ✨.&lt;/p&gt;

</description>
      <category>onboarding</category>
      <category>culture</category>
      <category>workplace</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Building a Scalable Audit Log System with AWS and ClickHouse</title>
      <dc:creator>Sebastian</dc:creator>
      <pubDate>Tue, 26 Nov 2024 09:00:05 +0000</pubDate>
      <link>https://dev.to/epilot/building-a-scalable-audit-log-system-with-aws-and-clickhouse-jn5</link>
      <guid>https://dev.to/epilot/building-a-scalable-audit-log-system-with-aws-and-clickhouse-jn5</guid>
      <description>&lt;p&gt;Audit logs might seem like a backend feature that only a few people care about, but they play a crucial role in keeping things running smoothly and securely in any SaaS or tech company. Let me take you through our journey of building a robust and scalable audit log system. Along the way, I’ll share why we needed it, what exactly audit logs are, and how we combined tools like AWS, ClickHouse, and OpenAPI to craft a solution that works like a charm.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Case of the Disappearing Configuration
&lt;/h2&gt;

&lt;p&gt;At epilot, we’ve encountered a frustratingly familiar scenario. A customer reaches out, upset that one of their workflow configurations has mysteriously vanished. Their immediate question? “Who deleted it?”—and the assumption is that someone on our team is responsible.&lt;/p&gt;

&lt;p&gt;Now here’s the tricky part: how do we, as engineers, figure out who did what and when?&lt;/p&gt;

&lt;p&gt;One obvious approach is to dive into the application logs. But here’s the catch: most of the production logs aren’t enabled by default. Even when they are, they’re often sampled, capturing only about 10% of the actual traffic. Additionally, those logs often seem to lack the required information. This means we’re left piecing together incomplete data, like trying to solve a puzzle with half the pieces missing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are Audit Logs Anyway?
&lt;/h2&gt;

&lt;p&gt;Audit logs provide clear visibility into system changes, aiding teams in investigations, diagnosing incidents, and tracing unauthorized actions. They empower admins by reducing support reliance and ensuring clarity on actions like role or workflow updates. For enterprise customers, audit logs are a critical, expected feature that supports compliance with standards like ISO 27001. Additionally, they lay the groundwork for enhanced threat detection capabilities in the future. In simple terms they try to help to answer the following questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHO&lt;/strong&gt; is doing something. Typically a user or a system (api call)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT&lt;/strong&gt; is that user/system doing?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHERE&lt;/strong&gt; is that occurring from? (e.g. an IP address)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHEN&lt;/strong&gt; did it occur?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY?&lt;/strong&gt; (optional) Why did the user log in? → we don’t know, Why is its IP blocked? → User logged in 5 times with the wrong password&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Considerations for a Successful Audit Log System
&lt;/h2&gt;

&lt;p&gt;Before diving into the technical details, it’s crucial to define what makes an audit log system effective. While the exact requirements depend on your company’s domain, there are some universal points worth considering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance&lt;/strong&gt;: Ensure the system adheres to regulations like GDPR. For example, customers may request the deletion of personal data, so you’ll need a straightforward way to erase all logs tied to a specific customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sustainability&lt;/strong&gt;: Audit logs grow rapidly, especially in high-traffic systems. Storing them indefinitely may not be feasible. Decide on strategies for archiving or purging logs over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions&lt;/strong&gt;: Define who is allowed to access audit logs to maintain security and privacy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format&lt;/strong&gt;: Standardize the structure of your logs to ensure they’re easy to interpret and query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Selection&lt;/strong&gt;: Carefully determine what actions and events are worth logging to answer critical questions effectively, without unnecessary noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making It Happen: How We Built Our Audit Logs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsp6ixm91x3oeb3ywahek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsp6ixm91x3oeb3ywahek.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At epilot, our APIs are built around serverless components provided by AWS. From the outset, we recognized that AWS API Gateway events provided a rich source of information for building audit logs. These events capture critical details such as user identities, actions performed (through the request payload), IP addresses, headers, and more.&lt;/p&gt;

&lt;p&gt;Given our microservices architecture, where services are organized by domain and accessed through an API Gateway (&lt;a href="https://docs.epilot.io/docs/architecture/overview/#system-architecture-diagram" rel="noopener noreferrer"&gt;see our system architecture&lt;/a&gt;), we needed a solution that seamlessly integrated with this structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-Level Overview&lt;/strong&gt;&lt;br&gt;
Our approach to audit logging can be summarized as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Capturing events asynchronously.&lt;/li&gt;
&lt;li&gt;Validating and transforming raw events into a standard format.&lt;/li&gt;
&lt;li&gt;Persisting the data in a read-only, scalable, and query-friendly storage system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This design adheres to several key technical principles:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Asynchronous Event Capture&lt;/u&gt;&lt;br&gt;
We use Amazon SQS to decouple event capture from the main HTTP request flow. For example, when a user creates a new workflow configuration, the relevant API Gateway event is pushed to an SQS queue by middleware wrapping the API. This ensures that audit logging does not introduce latency or affect the performance of the core application logic.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;From Raw to Standardized Events&lt;/u&gt;&lt;br&gt;
Our focus is on capturing system modifications, specifically HTTP methods like POST, PUT, PATCH, and DELETE. These provide meaningful insights into changes occurring within the system. GET requests, on the other hand, generate excessive noise and are generally excluded—though we offer an opt-in mechanism for services where logging GET requests adds value.&lt;/p&gt;

&lt;p&gt;A Lambda function processes raw API Gateway events from the SQS queue, transforming them into a structured and validated format. This includes filtering relevant data, enhancing it using metadata like OpenAPI specifications, and ensuring consistency across all logged events.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Data Persistence&lt;/u&gt;&lt;br&gt;
For storing audit logs, we chose &lt;a href="https://clickhouse.com/" rel="noopener noreferrer"&gt;ClickHouse&lt;/a&gt;, a highly scalable, SQL-based database that aligns with our requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only access: Supports immutability to preserve data integrity.
Scalability: Proven in our data lake setup to handle large volumes of data efficiently.&lt;/li&gt;
&lt;li&gt;Querying: SQL capabilities allow for precise filtering and analysis, which is more complex with alternatives like DynamoDB.
By leveraging ClickHouse, we ensure a robust and scalable foundation for our audit logs, simplifying future integrations and analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;Integration&lt;/u&gt;&lt;br&gt;
To make audit logging effortless for our microservices, we focused on seamless integration. At epilot, we rely heavily on &lt;a href="https://middy.js.org/" rel="noopener noreferrer"&gt;middy&lt;/a&gt;, a middleware engine used across all our services. Building on this, we introduced a new middleware: &lt;strong&gt;withAuditLog&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { withAuditLog } from '@epilot/audit-log'
import middy from '@middy/core'
import type { Handler } from 'aws-lambda'


export const withMiddlewares = (handler: lambda.Handler) =&amp;gt; {
  return middy(handler)
    .use(enableCorrelationIds())
    .use(...)
    .use(
      withAuditLog({
        ignorePaths: ['/v1/webhooks/configs/{configId}/trigger']
      })
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This middleware integrates directly into existing services and simplifies the audit logging process by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capturing API Gateway Events&lt;/strong&gt;: It hooks into the request lifecycle to extract the API Gateway event details.&lt;br&gt;
Omitting GET Requests by Default: To reduce noise, it filters out GET requests, with an option to opt them in for specific services where needed.&lt;br&gt;
&lt;strong&gt;Forwarding to SQS&lt;/strong&gt;: Its primary role is to forward the event to an SQS queue for asynchronous processing.&lt;/p&gt;

&lt;p&gt;With this middleware, adding audit logging to any microservice is as simple as including withAuditLogs in the service's middleware stack and giving the SQS:SendMessage permission. This ensures consistency, reduces implementation effort, and keeps the integration process dead simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Considerations&lt;/strong&gt;&lt;br&gt;
This article focuses on our high-level approach to building audit logs, as there are numerous ways to tackle the problem, each with its trade-offs. During our research, we explored alternatives like EventBridge for emitting events at the end of each request or Kinesis for streaming data. Ultimately, we chose a solution that met our key requirements: decoupling log emission from the main flow while offering flexibility in managing throughput and batching.&lt;/p&gt;

&lt;p&gt;Here’s why we chose SQS:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Decoupling from the Main Flow&lt;/u&gt;&lt;br&gt;
SQS allows us to process audit logs asynchronously, ensuring that the main HTTP request flow remains unaffected. This means audit log processing won’t slow down user-facing operations.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Flexibility with Throughput and Batching&lt;/u&gt;&lt;br&gt;
With SQS, we can fine-tune parameters like long-polling and batch windows to optimize throughput without compromising efficiency. This ensures scalable and reliable processing regardless of traffic spikes.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Scalability for POST/PUT/PATCH/DELETE Events&lt;/u&gt;&lt;br&gt;
Since we exclude GET requests by default, the system can handle fewer, more meaningful events. Capturing GET requests would require supporting a higher volume of events, potentially leading to Lambda concurrency issues, as multiple Lambda environments subscribing to the same queue could interfere with other services also using Lambda.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exposing Audit Logs to Users
&lt;/h2&gt;

&lt;p&gt;To make audit logs accessible and actionable, we introduced a new &lt;a href="https://sst.dev/" rel="noopener noreferrer"&gt;SST&lt;/a&gt;-based microservice that acts as a bridge to query data from ClickHouse. This microservice provides a simple and intuitive interface for users to explore their audit logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Search and Filtering: A user-friendly search bar allows users to combine filters effortlessly, enabling them to pinpoint specific events or patterns within the logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Activity Messages: Each audit log entry includes an activity message, a concise summary of what occurred. This message is dynamically constructed on the API side, tailored to the specific service name, making it customizable and relevant.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By customizing the activity messages for each service, users can quickly understand what happened in their systems without wading through raw data. This tailored approach ensures that the audit logs deliver immediate value and clarity to the end users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgat6dm8vy7qv7tt4sr1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgat6dm8vy7qv7tt4sr1c.png" alt=" " width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;In this article, we detailed the design and implementation of our audit log system at epilot, highlighting the key decisions and considerations that shaped its architecture. Our approach leverages AWS serverless components to seamlessly integrate audit logging into our microservices, ensuring scalability, efficiency, and ease of use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capturing Events:&lt;/strong&gt; Using a custom middleware, withAuditLogs, we extract API Gateway events asynchronously and forward them to an SQS queue, ensuring the logging process does not block the main application flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processing and Storing Logs:&lt;/strong&gt; A Lambda function transforms raw events into a standardized format, focusing on meaningful system modifications (POST, PUT, PATCH, DELETE) and stores them in a scalable, SQL-based ClickHouse database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Accessibility:&lt;/strong&gt; A new SST-based microservice provides a simple interface for querying and filtering logs. Tailored activity messages enhance usability, helping users quickly understand what occurred.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Considerations:&lt;/strong&gt; SQS was chosen for its ability to decouple the logging process, optimize throughput, and handle scalability challenges. While other solutions like EventBridge or Kinesis were viable, SQS met our specific requirements effectively.&lt;/p&gt;

&lt;p&gt;This high-level overview provides a flexible, scalable, and user-friendly solution for audit logging while ensuring system integrity and maintaining performance.&lt;/p&gt;

&lt;p&gt;Do you want to work on features like this? Check out our &lt;a href="https://www.epilot.cloud/en/company/careers#Offene-Stellenangebote" rel="noopener noreferrer"&gt;career page&lt;/a&gt; or reach out to &lt;a href="https://x.com/boingCntributor" rel="noopener noreferrer"&gt;my Twitter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>auditlog</category>
      <category>clickhouse</category>
      <category>sqs</category>
    </item>
    <item>
      <title>The Sauna Epiphany: How I Got Product Engineering Wrong</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Sun, 13 Oct 2024 12:23:43 +0000</pubDate>
      <link>https://dev.to/epilot/the-sauna-epiphany-how-i-got-product-engineering-wrong-35jg</link>
      <guid>https://dev.to/epilot/the-sauna-epiphany-how-i-got-product-engineering-wrong-35jg</guid>
      <description>&lt;p&gt;If you've seen my posts lately you've probably seen a lot of talk about &lt;em&gt;Product Engineers&lt;/em&gt;: software engineers who talk in customer problems and take pride in the products they build, not only the code and technologies they use.&lt;/p&gt;

&lt;p&gt;My core thesis is that with the rise of AI, the expectations for software engineers are being redefined, moving away from narrow programming roles: backend, frontend, C#, Java, React, etc. fast becoming obsolete due to AI tools like &lt;a href="https://www.cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; lowering the barrier of entry and unlocking productivity with any technology.&lt;/p&gt;

&lt;p&gt;The days when you could coast on your ability to churn out code are over. If you're still clinging to the idea that you're safe just because you know how to write code in a widely used language or framework you're in for a rude awakening.&lt;/p&gt;

&lt;p&gt;I believe this trend is likely to be the biggest industry shift in my lifetime, even surpassing the agile movement of the last 20 years. And I'm not the only one talking about it. (&lt;a href="https://blog.pragmaticengineer.com/the-product-minded-engineer/" rel="noopener noreferrer"&gt;#1&lt;/a&gt;, &lt;a href="https://hybridhacker.email/p/how-to-become-a-product-engineer" rel="noopener noreferrer"&gt;#2&lt;/a&gt;, &lt;a href="https://thesoftwareengineeringtimes.substack.com/p/are-product-engineers-replacing-software" rel="noopener noreferrer"&gt;#3&lt;/a&gt;, &lt;a href="https://saranga.dev/from-code-monkey-to-product-engineer-the-evolution-of-software-engineering-in-the-age-of-llms-3c79a508464d" rel="noopener noreferrer"&gt;#4&lt;/a&gt;, &lt;a href="https://thesoftwareengineeringtimes.substack.com/p/are-product-engineers-replacing-software" rel="noopener noreferrer"&gt;#5&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;But I got it wrong.&lt;/p&gt;

&lt;p&gt;Not entirely wrong, but I made it more complicated than it needed to be. In my quest to define what it means to be a product engineer, I overlooked the power of simplicity and ironically did not think enough about the customer: the ambitious software engineer thinking strategically about their careers.&lt;/p&gt;

&lt;h2&gt;
  
  
  I overengineered it
&lt;/h2&gt;

&lt;p&gt;In my earlier attempts to help engineers break out from purely technical roles, I published an extensive checklist to help engineers think and ask questions as product engineers. It covered everything from understanding the user and the market to measuring success and staying ahead of industry trends.&lt;/p&gt;

&lt;p&gt;Here's a taste of that checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#1-understand"&gt;1 Understand&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#11-whos-the-user"&gt;1.1 Who's the user?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#12-whos-the-customer"&gt;1.2 Who's the customer?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#13-whats-the-market"&gt;1.3 What's the market?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#14-ask-why"&gt;1.4 Ask Why&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#15-what-do-we-already-know"&gt;1.5 What do we already know?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#2-craft"&gt;2 Craft&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#21-am-i-proud-of-what-im-building"&gt;2.1 Am I proud of what I'm building?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#22-does-the-product-feel-good"&gt;2.2 Does the product feel good?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#23-how-do-i-get-there-faster"&gt;2.3 How do I get there faster?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#24-teamwork"&gt;2.4 Teamwork&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#3-growth"&gt;3 Growth&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#31-how-do-i-measure-the-success-of-my-work"&gt;3.1 How do I measure the success of my work?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#32-how-do-i-maximise-the-impact-of-my-work"&gt;3.2 How do I maximise the impact of my work?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#33-how-do-i-stay-ahead-of-the-curve"&gt;3.3 How do I stay ahead of the curve?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#4-product-vision"&gt;4 Product Vision&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#41-whats-our-north-star"&gt;4.1 What’s our North Star?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#42-how-does-my-work-impact-the-overall-design-of-the-product"&gt;4.2 How does my work impact the overall design of the product?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#43-whats-the-ambition-level"&gt;4.3 What's the ambition level?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Great stuff, but let's be honest — no engineer is going to run through a 15-point checklist to make a product decision. Just seeing this long list of questions can be totally overwhelming, especially if you're used to being in a technical role.&lt;/p&gt;

&lt;p&gt;If we expect engineers to actually start doing this stuff, the core idea needs to be simple and easy to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wake-Up Call in the Sauna
&lt;/h2&gt;

&lt;p&gt;As I'm writing this post, I'm enjoying a company working vacation in a nice hotel in Mallorca with my epilot colleagues. I was having a conversation in the hotel sauna with a principal engineer where something he said in passing suddenly clicked for me.&lt;/p&gt;

&lt;p&gt;He was venting about some engineers on his team who seemed to be diving head first into coding without bothering to understand the problem. &lt;em&gt;"It's so easy. Every engineer should be able to answer what problem they're solving, who the customer is, and what the impact of the work they're doing is."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That hit me like a splash of cold water in a 100°C Finnish sauna.&lt;/p&gt;

&lt;p&gt;He was totally right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boiling It Down to 3 Essential Questions
&lt;/h2&gt;

&lt;p&gt;To think like a product engineer, it's already enough to start with just three simple questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What's the problem?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For who?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why is this important?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive a bit deeper into each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What's the problem?
&lt;/h3&gt;

&lt;p&gt;Stop coding for a second. Do you really know what you're trying to solve? Not the ticket description in Jira, but the real-world issue. If you can't articulate the problem in simple plain English, you have no business writing a single line of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. For who?
&lt;/h3&gt;

&lt;p&gt;Identify your user and customer. Sometimes they're the same person; other times, they're not. Understanding who will use your product (and who will pay for it) is crucial. It shapes the way you approach the solution and helps you tailor the experience to meet their needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Why is this important?
&lt;/h3&gt;

&lt;p&gt;As engineers there's never a shortage of things we could improve or implement. If solving the problem doesn't make a meaningful difference, why are you wasting your valuable time? We're not here to build features that nobody uses or cares about. Connect your work to something that actually matters and helps build your track record.&lt;/p&gt;

&lt;p&gt;By anchoring your work in these three questions, you immediately move from code monkey to a high value product-minded engineer. Still a rare breed. You're not just implementing features someone else decided to build; you're elevating yourself to a position to influence product decisions and build smarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leverage Your Team—They're There for a Reason
&lt;/h2&gt;

&lt;p&gt;The biggest mistake engineers make is thinking they have to find all the answers themselves.&lt;/p&gt;

&lt;p&gt;Most of us are lucky to work in a product team with other disciplines like UX researchers, designers, PMs and business stakeholders whose job is to help answer these questions.&lt;/p&gt;

&lt;p&gt;By leaning on your team, you not only find better answers but also foster a more collaborative and innovative environment.&lt;/p&gt;

&lt;p&gt;Product engineering is a team sport.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking Action: Start Asking the Right Questions Today
&lt;/h2&gt;

&lt;p&gt;So, the next time you tackle a new topic, pause for a moment before diving into code. Ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What's the problem?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For who?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why is this important?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write down your answers. If you don't know, reach out to your team and find out. This simple practice can transform your approach to work, leading to better decisions, more impactful solutions, and a greater sense of ownership.&lt;/p&gt;

&lt;p&gt;Congratulations, you just became a Product Engineer! &lt;/p&gt;

&lt;h2&gt;
  
  
  Join the Conversation
&lt;/h2&gt;

&lt;p&gt;I'd love to hear your thoughts on this simplified approach to product engineering. Have you tried asking these three questions in your work? What impact did it have? Share your experiences in the comments below.&lt;/p&gt;

&lt;p&gt;Consider giving the &lt;a href="https://github.com/anttiviljami/product-engineer-manifesto" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; a star if the product engineer role resonates with you!&lt;/p&gt;

</description>
      <category>product</category>
      <category>ai</category>
      <category>sauna</category>
    </item>
    <item>
      <title>How We Integrate AI in epilot - Chapter 1: AWS Bedrock &amp; Prompt Engineering</title>
      <dc:creator>Kerem Nalbant</dc:creator>
      <pubDate>Thu, 18 Jul 2024 14:58:31 +0000</pubDate>
      <link>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-1-aws-bedrock-prompt-engineering-17jh</link>
      <guid>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-1-aws-bedrock-prompt-engineering-17jh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When we decided to bring AI to epilot, we had so many potential use cases that we first needed to do user research and identify the most repetitive and time consuming tasks. After that, we had to find out what our customers needed the most from these.&lt;/p&gt;

&lt;p&gt;Our research showed that users heavily utilized the messaging feature and for some cases when long email threads come into the play, we noticed that some customers were spending an average time of 16 minutes replying to an email, and we knew we could make it better by providing them with a shorter and clearer thread summary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh1sgyve0o6z77zujykx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh1sgyve0o6z77zujykx.png" alt="Enterprise AI Playbook" width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://platforms.substack.com/p/how-to-win-at-enterprise-ai-a-playbook" rel="noopener noreferrer"&gt;Enterprise AI Playbook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Work is a bundle of tasks, which are performed towards specific goals.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tasks are the ‘atomic unit’ of any work done in the enterprise. Tasks may be performed as a human service, or may be performed by software, towards achieving a goal.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our goal was to reduce that time down to less than a minute, and there were two different tasks being performed by users which we need to perform with AI to achieve our goal. This article addresses the &lt;strong&gt;Task: Send emails&lt;/strong&gt; and the steps to complete this task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read and understand the email thread:&lt;/strong&gt; Help users understand long email threads faster by providing AI-generated summary, next steps and topics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write an answer:&lt;/strong&gt; Provide AI-generated suggested answers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Our customers often deal with long email threads, requiring a long time to read and answer. We needed a solution that could summarize email threads and provide recommendations for next actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;While some problems could be solved with prompt engineering alone, some could be solved with Retrieval-Augmented Generation (RAG).&lt;/p&gt;

&lt;p&gt;For generating summaries, we didn't need any external contextual data  other than email thread to feed the prompt, so we decided to just go ahead with prompt engineering.&lt;/p&gt;

&lt;p&gt;The next task is suggesting AI-generated replies to email threads, where RAG would be really useful. For that, we will use a Vector DB and an embedding model, which I'll write about in the next chapter.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Bedrock
&lt;/h3&gt;

&lt;p&gt;We decided to use AWS Bedrock, as epilot we already make use of AWS in almost every area of our platform. It provides state-of-the-art LLMs from multiple providers and out of the box solutions such as Knowledge Base to achieve RAG easily and Model Evaluation to compare models and prompts with a fancy UI.&lt;/p&gt;

&lt;p&gt;AWS Bedrock also ensures the processed data is protected. One of our concerns was where the data would be stored, how it would be processed and whether 3rd parties would be involved.&lt;/p&gt;

&lt;p&gt;By default, AWS Bedrock offers zero-retention policy, ensuring that logs, prompts, LLM output and any personal data are not shared with any third parties or model providers. Bedrock also ensures that all processed data remains within the EU region.&lt;/p&gt;

&lt;h3&gt;
  
  
  GenAI Foundation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rgp42hg4fvmrszhczrp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rgp42hg4fvmrszhczrp.png" alt="Architecture" width="800" height="1432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GenAI Foundation has a central SQS queue and a handler function, ensures exactly one, concurrent and batch processing while staying within the rate limits of Bedrock.&lt;/p&gt;

&lt;p&gt;Integrating GenAI related code and logic to our existing APIs was not a good idea. So, I've decided to create a new monorepo. Here's the reasons:&lt;/p&gt;

&lt;h4&gt;
  
  
  Separation of Concerns
&lt;/h4&gt;

&lt;p&gt;By creating a separate repository, we maintain a clear separation of concerns. This ensures that the core functionalities of existing APIs remain clean and focused.&lt;/p&gt;

&lt;h4&gt;
  
  
  Language Flexibility
&lt;/h4&gt;

&lt;p&gt;Since GenAI-related code requires Python, having a separate repository allows us to leverage Python's capabilities without interfering with the TypeScript-based APIs. This separation ensures that each project can use the best-suited language for its specific tasks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Encapsulation
&lt;/h4&gt;

&lt;p&gt;Encapsulating the GenAI logic within a dedicated repository makes it easier to manage, update, and scale. This also allows engineers with specific expertise in GenAI to work independently of the rest of the system.&lt;/p&gt;

&lt;h4&gt;
  
  
  Modularity
&lt;/h4&gt;

&lt;p&gt;A modular approach allows for easier testing, maintenance, and deployment of the GenAI features. Updates and bug fixes in the GenAI module can be rolled out independently of the core APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model Choice &amp;amp; Prompt Engineering
&lt;/h3&gt;

&lt;p&gt;We decided to go with Claude 3 Sonnet because it was the best option for us at the time, given the costs and the complexity of the task. &lt;/p&gt;

&lt;p&gt;We are planning to switch to &lt;a href="https://www.anthropic.com/news/claude-3-5-sonnet" rel="noopener noreferrer"&gt;Claude 3.5 Sonnet&lt;/a&gt; once it's available in AWS Bedrock, which we're really excited about!&lt;/p&gt;

&lt;p&gt;To choose the best model for the task, AWS Bedrock offers &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation.html" rel="noopener noreferrer"&gt;Model Evaluation&lt;/a&gt;, which lets you easily compare models with each other. &lt;br&gt;
You can also do Prompt Evaluation by creating a dataset and executing it against a single model to compare the results of different prompts.&lt;/p&gt;



&lt;p&gt;There are lots of great resources online for prompt engineering, but I want to mention that Anthropic provides really helpful tips in their documentation. I strongly suggest you to take a look, if you haven't already. For me, the most important point was &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags" rel="noopener noreferrer"&gt;using XML tags&lt;/a&gt;. Anthropic mentions that their models produce much better results when used with XML tags.&lt;/p&gt;

&lt;p&gt;Prompt engineering is all about experimenting, so we spent some time on optimizing our prompt. I'd love to share an example of prompts with you!&lt;/p&gt;

&lt;p&gt;Below you can see some example usages of common prompt engineering techniques such as "Giving a Role to LLM", "Using XML Tags", "Using Examples (Multishot Prompting)", "Being clear and direct".&lt;/p&gt;
&lt;h4&gt;
  
  
  System Prompt
&lt;/h4&gt;

&lt;p&gt;System prompt is where we &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/system-prompts" rel="noopener noreferrer"&gt;give Claude a role&lt;/a&gt;, and some clear instructions about the task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an intelligent assistant specialized in assisting customer service agents in energy industry.

Your task is summarizing email conversations between customer service agents and customers, providing a clear and concise overview of the key points by following the instructions provided.

Your summaries will help your colleagues quickly understand the main aspects of each conversation without having to read through the entire email thread.

Your goal is to ensure that the agent can grasp the key points and next steps from your summary alone, making their workflow more efficient and effective.

You are a third person observer and must not provide any personal opinions or make any assumptions about the conversation.

You must use the third-person objective narration. You must report the events that take place without knowing the motivations or thoughts of any of the characters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  User Prompt
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here is the conversation between the customer and the agent:

&amp;lt;Email Thread&amp;gt;
&amp;lt;Subject&amp;gt;
{subject}
&amp;lt;/Subject&amp;gt;
&amp;lt;Messages&amp;gt;
{conversation}
&amp;lt;/Messages&amp;gt;
&amp;lt;/Email Thread&amp;gt;

&amp;lt;General Instructions&amp;gt;
- You must use &amp;lt;Email Thread&amp;gt; tags to identify the email conversation.
...
- You must use only the knowledge provided in the &amp;lt;Email Thread&amp;gt; tags and do not access any other external information or knowledge you already possess.
&amp;lt;/General Instructions&amp;gt;

&amp;lt;Language Instructions&amp;gt;
- You must optimize the language for making the summary easy and fast for humans to read.
...
- You must use a professional, respectful and informative tone.
&amp;lt;/Language Instructions&amp;gt;

&amp;lt;Reference Instructions&amp;gt;
- You must always refer to the Customer and Agent by their names.
...
&amp;lt;/Reference Instructions&amp;gt;

&amp;lt;Summary Instructions&amp;gt;
- You must get relevant quotes to complete the task from the conversation.
...
- You must write the summary in reverse chronological order.
&amp;lt;/Summary Instructions&amp;gt;

&amp;lt;Output Instructions&amp;gt;
- You must give your output in JSON format. The JSON object should be valid.
...
- If you will provide quotes or emphasize any part of the conversation, you must use single quotes. e.g. 'quote'.
&amp;lt;/Output Instructions&amp;gt;

# Few Shot Prompting
&amp;lt;Example Outputs&amp;gt;
{
  "summary": [
    "John Doe has processed the reimbursement request and informed Jane Doe.",
    "Jane Doe has provided the requested information and is awaiting further instructions from the team member.",
    "John Doe has acknowledged the Jane Doe refund request and has requested additional information to process the refund.",
    "Jane Doe is requesting a refund for a defective PV inverter."
  ],
  "topics": [
    "Refund request for defective product"
  ],
  "next_steps": [
    "If the information is sufficient, John Doe should process the refund and inform Jane Doe of the completion.",
    "John Doe should document the refund process for record-keeping."
  ]
}
...
&amp;lt;/Example Outputs&amp;gt;

You must follow the instructions listed in the following tags:
- &amp;lt;General Instructions&amp;gt; for general guide and rules,
- &amp;lt;Language Instructions&amp;gt; for lingual instructions that declares your tone, grammar, and output language,
...
- &amp;lt;Example Output&amp;gt; for example of the output as a reference.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Human-in-the-Loop (HITL)
&lt;/h3&gt;

&lt;p&gt;It's challenging to ship AI features, especially when it's the first one in the company which has thousands of users. We need to build trust, and keep it. To do that, we need to collect as much as feedback we can. Then, evaluate these feedbacks and take actions.&lt;/p&gt;

&lt;p&gt;HITL can be applied in different ways in different solutions. In our use case, we utilize it to evaluate feedback and detect hallucinations. Also, our team evaluating the feedback can change the result generated by the AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration Strategy
&lt;/h3&gt;

&lt;p&gt;Our migration strategy involves both runtime and one-time migration to ensure a smooth transition and boost the adoption for our users.&lt;br&gt;
With this way, we ensure we don't unnecessarily make use of LLMs, and we save costs while ensuring seamless integration.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rate Limits
&lt;/h3&gt;

&lt;p&gt;Since AWS Bedrock imposes rate limiting, we had to reflect this to our users. In order to offer a fair usage, we set limits according to the pricing tier of our users.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Examples
&lt;/h3&gt;

&lt;p&gt;Pay attention to the way I &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/prefill-claudes-response#example-maintaining-character-with-role-prompting" rel="noopener noreferrer"&gt;prefill Claude's response&lt;/a&gt; to force it to answer only with JSON object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 1000,
    "system": system_prompt,
    "messages": [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
            ],
        },
        {"role": "assistant", "content": "{"}, # Prefill Claude's response
    ],
    "temperature": temperature,
    "top_p": top_p,
    "top_k": top_k,
}
res = await client.invoke_model(
    modelId=MODEL_ID,
    contentType="application/json",
    accept="application/json",
    body=json.dumps(settings)
)

model_response = json.loads(await res["body"].read())
response_text = model_response["content"][0]["text"]

res_model = LLMResponseModel.model_validate_json("{" + response_text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By integrating AI into epilot, we have significantly enhanced the capabilities of our platform. This integration not only improves the efficiency of daily tasks, but also accelerates customer support. Furthermore, it's the first step in positioning epilot as a leader in the adoption of advanced AI technologies in the energy sector.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>bedrock</category>
      <category>anthropic</category>
      <category>promptengineering</category>
    </item>
  </channel>
</rss>
