<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Engineering on VividMap Blog</title><link>https://blog.vividmap.io/categories/engineering/</link><description>Recent content in Engineering on VividMap Blog</description><image><title>VividMap Blog</title><url>https://blog.vividmap.io/og-image.png</url><link>https://blog.vividmap.io/og-image.png</link></image><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 17 May 2026 12:37:43 -0600</lastBuildDate><atom:link href="https://blog.vividmap.io/categories/engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>I Built a Career Intelligence Tool for Staff+ Engineers — Here's the Technical Architecture</title><link>https://blog.vividmap.io/posts/vividmap-technical-architecture/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://blog.vividmap.io/posts/vividmap-technical-architecture/</guid><description>VividMap is a personal career tool for senior ICs: shadow org charts, skill cost estimation, and an AI assistant that has actual context about your career situation. Built with Vue.js 3 + .NET 10 + PostgreSQL.</description><content:encoded><![CDATA[<p>The standard advice for senior engineers is &ldquo;be more strategic.&rdquo; Nobody provides tooling for what that actually means in practice.</p>
<p>Most productivity and knowledge management tools are built for managers or PMs. The senior IC workflow — navigating informal power structures, tracking skill bets across years, building visibility without a title change — doesn&rsquo;t fit any standard category. Notion, Confluence, JIRA: these are collaboration tools optimized for teams, not personal career intelligence.</p>
<p>I built VividMap to fill that gap. Here&rsquo;s the technical breakdown.</p>
<h2 id="the-stack">The Stack</h2>
<pre tabindex="0"><code>Vue.js 3 (Composition API) + Pinia + Vue Router
.NET 10 Minimal API
PostgreSQL 17
Firebase Authentication (manual JWT verification)
Docker Compose deployment on VPS
</code></pre><p>Deliberately conventional. VividMap&rsquo;s value is in the domain modeling, not the infrastructure. C# is what I know deeply; the runtime doesn&rsquo;t affect the product.</p>
<h2 id="domain-model-what-career-intelligence-actually-means-in-data">Domain Model: What &ldquo;Career Intelligence&rdquo; Actually Means in Data</h2>
<p>The eight modules map to distinct data types:</p>
<table>
  <thead>
      <tr>
          <th>Module</th>
          <th>Core Data Type</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Shadow Org Chart</td>
          <td>Graph (nodes + weighted edges with influence type)</td>
      </tr>
      <tr>
          <td>Maps (You Are Here, Topographical, Treasure)</td>
          <td>Hierarchical position + goal paths</td>
      </tr>
      <tr>
          <td>Skill Learning Tracker</td>
          <td>Skills with multi-dimensional cost estimates</td>
      </tr>
      <tr>
          <td>Technology Roadmap</td>
          <td>Timeline items with dependency edges</td>
      </tr>
      <tr>
          <td>Second Brain</td>
          <td>Hierarchical folders + full-text searchable notes</td>
      </tr>
      <tr>
          <td>Pomodoro Timer</td>
          <td>Sessions linked to initiatives (not generic time)</td>
      </tr>
      <tr>
          <td>Engineering Loop</td>
          <td>Analytics integration (PostHog)</td>
      </tr>
      <tr>
          <td>AI Assistant</td>
          <td>Retrieval over all user data + LLM completion</td>
      </tr>
  </tbody>
</table>
<p>The most interesting data structure is the org chart.</p>
<h2 id="the-shadow-org-chart-as-a-graph">The Shadow Org Chart as a Graph</h2>
<p>The official hierarchy is a tree. The shadow org chart is a directed graph.</p>
<p>Each node is a person. Each edge has:</p>
<ul>
<li><code>influenceType</code>: <code>ALLY | BLOCKER | NEUTRAL | SPONSOR | MENTEE</code></li>
<li><code>weight</code>: float representing influence strength (0.0–1.0)</li>
<li><code>notes</code>: private text about why this person matters</li>
<li><code>isInformal</code>: whether the edge is in the official org chart or the shadow map</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrgNode</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Guid Id { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> Name { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string?</span> Title { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string?</span> Team { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IsInformal { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> ICollection&lt;OrgEdge&gt; OutboundEdges { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrgEdge</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Guid FromNodeId { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Guid ToNodeId { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> InfluenceType InfluenceType { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">float</span> Weight { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string?</span> Notes { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">bool</span> IsInformal { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>PostgreSQL handles the persistence. The graph algorithms (shortest path to a decision-maker, influence reachability) run in the API layer using a standard adjacency list walk — the graphs are small enough (personal org chart, rarely more than 200 nodes) that a graph database adds no value.</p>
<p>The Vue.js frontend renders the graph using a canvas-based layout with force simulation. Informal edges render as dashed lines. Influence type maps to edge color.</p>
<h2 id="firebase-auth-with-manual-jwt-verification">Firebase Auth With Manual JWT Verification</h2>
<p>This was the most surprising detour in the build.</p>
<p>Firebase Authentication normally works via service account key files — you download a JSON file from Google Cloud Console and pass it to the Firebase Admin SDK. The SDK verifies tokens server-side using the service account.</p>
<p>The problem: an org policy on the Google Cloud project blocked service account key generation. The policy that prevents it is a good security practice, but it meant the standard Firebase Admin SDK setup was unavailable.</p>
<p>The workaround: verify Firebase ID tokens manually using Google&rsquo;s public JWKS endpoint.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task&lt;ClaimsPrincipal?&gt; ValidateFirebaseTokenAsync(<span style="color:#66d9ef">string</span> idToken)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Google&#39;s public key endpoint for Firebase</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">string</span> jwksUri = <span style="color:#e6db74">&#34;https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> jwks = <span style="color:#66d9ef">await</span> _httpClient.GetFromJsonAsync&lt;JsonWebKeySet&gt;(jwksUri);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> tokenHandler = <span style="color:#66d9ef">new</span> JsonWebTokenHandler();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> validationParams = <span style="color:#66d9ef">new</span> TokenValidationParameters
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ValidateIssuerSigningKey = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        IssuerSigningKeys = jwks!.GetSigningKeys(),
</span></span><span style="display:flex;"><span>        ValidateIssuer = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        ValidIssuer = <span style="color:#e6db74">$&#34;https://securetoken.google.com/{_projectId}&#34;</span>,
</span></span><span style="display:flex;"><span>        ValidateAudience = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        ValidAudience = _projectId,
</span></span><span style="display:flex;"><span>        ValidateLifetime = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        ClockSkew = TimeSpan.FromMinutes(<span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> result = <span style="color:#66d9ef">await</span> tokenHandler.ValidateTokenAsync(idToken, validationParams);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result.IsValid ? <span style="color:#66d9ef">new</span> ClaimsPrincipal(result.ClaimsIdentity) : <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>JWKS keys rotate. The implementation caches them with the <code>Cache-Control: max-age</code> from the response headers and re-fetches when the cache expires — same behavior the Admin SDK produces, just without the service account dependency.</p>
<h2 id="the-ai-assistant-context-is-the-differentiator">The AI Assistant: Context Is the Differentiator</h2>
<p>The AI assistant is the feature that&rsquo;s hardest to replicate with a generic AI chat tool.</p>
<p>The assistant has access to:</p>
<ul>
<li>Your org chart (who you&rsquo;ve identified as allies, blockers, sponsors)</li>
<li>Your Second Brain notes (1:1 summaries, architecture decisions, political context)</li>
<li>Your skill state and cost estimates</li>
<li>Your roadmap and initiative status</li>
</ul>
<p>When you ask &ldquo;I&rsquo;m getting deprioritized in the upcoming reorg — what should I do?&rdquo;, it responds with context about your specific org map, not generic reorg advice. That&rsquo;s the differentiation: not a smarter model, but a model with actually relevant context.</p>
<p>The implementation is RAG over the user&rsquo;s own data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task&lt;<span style="color:#66d9ef">string</span>&gt; GetAssistantResponseAsync(<span style="color:#66d9ef">string</span> userId, <span style="color:#66d9ef">string</span> query)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Retrieve relevant context chunks from user&#39;s data</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> orgContext = <span style="color:#66d9ef">await</span> _orgChartService.GetRelevantContextAsync(userId, query);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> notesContext = <span style="color:#66d9ef">await</span> _secondBrainService.SearchAsync(userId, query, limit: <span style="color:#ae81ff">5</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> skillContext = <span style="color:#66d9ef">await</span> _skillService.GetCurrentStateAsync(userId);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> systemPrompt = BuildSystemPrompt(orgContext, notesContext, skillContext);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> _claudeClient.CompleteAsync(systemPrompt, query);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The system prompt includes a structured summary of the org chart, relevant note excerpts, and skill state. The model is Claude Sonnet — token costs are manageable at personal scale. The retrieval isn&rsquo;t doing dense vector search; it&rsquo;s keyword-based filtering over the user&rsquo;s own small dataset, which is fast and doesn&rsquo;t require an embedding store.</p>
<h2 id="what-id-do-differently">What I&rsquo;d Do Differently</h2>
<p><strong>Firebase auth decision earlier.</strong> The JWKS manual verification wasn&rsquo;t hard, but it ate a day and a half I didn&rsquo;t budget for. If the production environment has org policy restrictions, discover them in week one, not week twelve.</p>
<p><strong>Graph storage.</strong> I&rsquo;m storing the org chart in PostgreSQL as two standard tables (nodes + edges). For the current use case (personal org charts under 200 nodes), this is fine. If VividMap ever supports team-level org maps with thousands of nodes, a proper graph storage approach would matter. For now, the PostgreSQL representation is a complexity-appropriate choice.</p>
<p><strong>The Pomodoro timer should have been last.</strong> It was easy to build and satisfying to ship, but the timer isn&rsquo;t what differentiates the product. I spent time on it early when the graph visualization and AI assistant integration deserved that time more. Feature prioritization in solo products without a PM is harder than it looks.</p>
<h2 id="current-status">Current Status</h2>
<p>VividMap is live at <a href="https://app.vividmap.io">app.vividmap.io</a>. Free during beta.</p>
<p>If you&rsquo;re a senior IC or staff engineer who thinks about the org navigation layer of the job, this is what I built. Happy to answer questions about the shadow org chart design, the Firebase workaround, or the RAG architecture.</p>
<hr>
<p><em>VividMap is a career intelligence tool for Staff+ engineers. Live at <a href="https://app.vividmap.io">app.vividmap.io</a> — free during beta.</em></p>
]]></content:encoded></item></channel></rss>