<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Compile Care]]></title><description><![CDATA[Compile Care]]></description><link>https://compile.care</link><generator>RSS for Node</generator><lastBuildDate>Thu, 07 May 2026 10:02:12 GMT</lastBuildDate><atom:link href="https://compile.care/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building Trust: Authentication, Authorization, and Audit Logging]]></title><description><![CDATA[← Part 3: Dynamic Mapping Engine
Trust, But Verify (Then Log Everything)
In healthcare IT, "it works" isn't enough. You need to prove:

Who accessed data

What they accessed

When they accessed it

Why (in some cases)

That you can reconstruct what h...]]></description><link>https://compile.care/building-a-fhir-adapter-from-scratch-part-4</link><guid isPermaLink="true">https://compile.care/building-a-fhir-adapter-from-scratch-part-4</guid><category><![CDATA[fhir]]></category><category><![CDATA[hl7]]></category><category><![CDATA[Rust]]></category><category><![CDATA[rust lang]]></category><category><![CDATA[healthcare]]></category><dc:creator><![CDATA[Alberto Lagos]]></dc:creator><pubDate>Mon, 19 Jan 2026 03:00:46 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="./03-dynamic-mapping-engine.md">← Part 3: Dynamic Mapping Engine</a></p>
<h2 id="heading-trust-but-verify-then-log-everything">Trust, But Verify (Then Log Everything)</h2>
<p>In healthcare IT, "it works" isn't enough. You need to prove:</p>
<ul>
<li><p><strong>Who</strong> accessed data</p>
</li>
<li><p><strong>What</strong> they accessed</p>
</li>
<li><p><strong>When</strong> they accessed it</p>
</li>
<li><p><strong>Why</strong> (in some cases)</p>
</li>
<li><p><strong>That you can reconstruct</strong> what happened 6 months ago</p>
</li>
</ul>
<p>This isn't paranoia—it's HIPAA compliance. And it's the difference between a minor incident and a career-ending breach.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768323788499/65a0b194-e011-414e-bc70-cd88d0d82e67.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-authentication-keycloak-integration">Authentication: Keycloak Integration</h2>
<p>I needed enterprise-grade authentication without building it myself. Enter <strong>Keycloak</strong>.</p>
<h3 id="heading-why-keycloak">Why Keycloak?</h3>
<ul>
<li><p>✅ <strong>Battle-tested:</strong> Used by Red Hat and many others</p>
</li>
<li><p>✅ <strong>SSO/SAML/OIDC:</strong> Hospitals/healthcare companies love their SAML</p>
</li>
<li><p>✅ <strong>Realm isolation:</strong> Perfect for multi-tenancy</p>
</li>
<li><p>✅ <strong>Open source:</strong> No vendor lock-in</p>
</li>
</ul>
<p>Each tenant gets their own <strong>Keycloak realm</strong>. Hospital A's users can't log into Hospital B's realm. Period.</p>
<h3 id="heading-the-jwt-flow">The JWT Flow</h3>
<pre><code class="lang-plaintext">1. User → Keycloak: "username + password"
2. Keycloak → User: JWT token
3. User → FHIR Adapter: "GET /fhir/Patient/123" + JWT header
4. Adapter → Keycloak: "Is this token valid?"
5. Keycloak → Adapter: "Yes, user=john, tenant=42, roles=[fhir-read]"
6. Adapter: Process request (if authorized)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768323875891/9136e80b-4863-4010-ab9d-b1bedc6e09ce.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-jwt-validation-in-rust">JWT Validation in Rust</h3>
<p>I built an async middleware that validates every request:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">auth_middleware</span></span>(
    State(state): State&lt;AppState&gt;,
    <span class="hljs-keyword">mut</span> req: Request,
    next: Next,
) -&gt; <span class="hljs-built_in">Result</span>&lt;Response, AppError&gt; {
    <span class="hljs-comment">// 1. Extract JWT from Authorization header</span>
    <span class="hljs-keyword">let</span> token = extract_bearer_token(&amp;req)
        .ok_or_else(|| AppError::Unauthorized(<span class="hljs-string">"Missing token"</span>))?;

    <span class="hljs-comment">// 2. Validate signature and expiration with Keycloak</span>
    <span class="hljs-keyword">let</span> claims = state.keycloak_client
        .validate_token(&amp;token)
        .<span class="hljs-keyword">await</span>
        .map_err(|e| {
            tracing::warn!(<span class="hljs-string">"Token validation failed: {}"</span>, e);
            AppError::Unauthorized(<span class="hljs-string">"Invalid token"</span>)
        })?;

    <span class="hljs-comment">// 3. Extract critical claims</span>
    <span class="hljs-keyword">let</span> auth_context = AuthContext {
        user_id: claims.sub,
        client_id: claims.client_id
            .ok_or_else(|| AppError::Unauthorized(<span class="hljs-string">"Missing client_id"</span>))?,
        roles: claims.realm_access.roles,
        email: claims.email,
    };

    <span class="hljs-comment">// 4. Inject into request for downstream handlers</span>
    req.extensions_mut().insert(auth_context);

    <span class="hljs-literal">Ok</span>(next.run(req).<span class="hljs-keyword">await</span>)
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">extract_bearer_token</span></span>(req: &amp;Request) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt; {
    req.headers()
        .get(<span class="hljs-string">"Authorization"</span>)?
        .to_str()
        .ok()?
        .strip_prefix(<span class="hljs-string">"Bearer "</span>)
        .map(|s| s.to_string())
}
</code></pre>
<h3 id="heading-jwks-caching">JWKS Caching</h3>
<p>Validating JWTs requires Keycloak's public keys (JWKS). Fetching them on every request would be slow.</p>
<p>I implemented a <strong>TTL-based JWKS cache</strong>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">KeycloakClient</span></span> {
    jwks_cache: Arc&lt;Mutex&lt;<span class="hljs-built_in">Option</span>&lt;(JwkSet, Instant)&gt;&gt;&gt;,
    jwks_cache_ttl: Duration,
    server_url: <span class="hljs-built_in">String</span>,
    realm: <span class="hljs-built_in">String</span>,
}

<span class="hljs-keyword">impl</span> KeycloakClient {
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_jwks</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;JwkSet&gt; {
        <span class="hljs-comment">// Check cache</span>
        {
            <span class="hljs-keyword">let</span> cache = <span class="hljs-keyword">self</span>.jwks_cache.lock().unwrap();
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>((jwks, cached_at)) = cache.as_ref() {
                <span class="hljs-keyword">if</span> cached_at.elapsed() &lt; <span class="hljs-keyword">self</span>.jwks_cache_ttl {
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(jwks.clone());
                }
            }
        }

        <span class="hljs-comment">// Fetch from Keycloak</span>
        <span class="hljs-keyword">let</span> url = <span class="hljs-built_in">format!</span>(
            <span class="hljs-string">"{}/realms/{}/protocol/openid-connect/certs"</span>,
            <span class="hljs-keyword">self</span>.server_url, <span class="hljs-keyword">self</span>.realm
        );

        <span class="hljs-keyword">let</span> jwks: JwkSet = reqwest::get(&amp;url)
            .<span class="hljs-keyword">await</span>?
            .json()
            .<span class="hljs-keyword">await</span>?;

        <span class="hljs-comment">// Update cache</span>
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> cache = <span class="hljs-keyword">self</span>.jwks_cache.lock().unwrap();
        *cache = <span class="hljs-literal">Some</span>((jwks.clone(), Instant::now()));

        <span class="hljs-literal">Ok</span>(jwks)
    }
}
</code></pre>
<p><strong>Performance:</strong> 1st request ~50ms (fetches JWKS), subsequent requests ~2ms (cached).</p>
<h2 id="heading-authorization-role-based-access-control">Authorization: Role-Based Access Control</h2>
<p>Authentication answers "who are you?" Authorization answers "what can you do?"</p>
<p>I defined three roles:</p>
<ul>
<li><p><code>fhir-read</code>: Read resources</p>
</li>
<li><p><code>fhir-write</code>: Create/update resources</p>
</li>
<li><p><code>fhir-admin</code>: Admin panel access, configuration changes</p>
</li>
</ul>
<pre><code class="lang-rust"><span class="hljs-keyword">impl</span> AuthContext {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">has_role</span></span>(&amp;<span class="hljs-keyword">self</span>, role: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">bool</span> {
        <span class="hljs-keyword">self</span>.roles.iter().any(|r| r == role)
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">require_role</span></span>(&amp;<span class="hljs-keyword">self</span>, role: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;(), AppError&gt; {
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">self</span>.has_role(role) {
            <span class="hljs-literal">Ok</span>(())
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-literal">Err</span>(AppError::Forbidden(
                <span class="hljs-built_in">format!</span>(<span class="hljs-string">"Role '{}' required"</span>, role)
            ))
        }
    }
}
</code></pre>
<p>Usage in handlers:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">patient_read</span></span>(
    State(state): State&lt;AppState&gt;,
    auth: AuthContext,  <span class="hljs-comment">// Injected by middleware</span>
    Path(id): Path&lt;<span class="hljs-built_in">String</span>&gt;,
) -&gt; <span class="hljs-built_in">Result</span>&lt;Json&lt;Patient&gt;, AppError&gt; {
    <span class="hljs-comment">// Check authorization</span>
    auth.require_role(<span class="hljs-string">"fhir-read"</span>)?;

    <span class="hljs-comment">// Fetch patient (automatically scoped to auth.client_id)</span>
    <span class="hljs-keyword">let</span> patient = state.fetch_patient(&amp;auth.client_id, &amp;id).<span class="hljs-keyword">await</span>?;

    <span class="hljs-literal">Ok</span>(Json(patient))
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768323974834/6f09f5d8-eae2-43d3-85ae-eae0554d7072.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-audit-logging-the-paper-trail">Audit Logging: The Paper Trail</h2>
<p>Every request gets logged. Not just errors—<strong>everything</strong>.</p>
<h3 id="heading-what-we-log">What We Log</h3>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AuditLogParams</span></span> {
    <span class="hljs-keyword">pub</span> resource_type: <span class="hljs-built_in">String</span>,     <span class="hljs-comment">// "Patient"</span>
    <span class="hljs-keyword">pub</span> resource_id: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;,  <span class="hljs-comment">// "12345"</span>
    <span class="hljs-keyword">pub</span> operation: AuditOperation, <span class="hljs-comment">// Read, Create, Update, Delete, Search</span>
    <span class="hljs-keyword">pub</span> user_id: <span class="hljs-built_in">String</span>,           <span class="hljs-comment">// From JWT</span>
    <span class="hljs-keyword">pub</span> client_id: <span class="hljs-built_in">String</span>,         <span class="hljs-comment">// Tenant ID</span>
    <span class="hljs-keyword">pub</span> ip_address: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;,
    <span class="hljs-keyword">pub</span> user_agent: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;,
    <span class="hljs-keyword">pub</span> request_id: <span class="hljs-built_in">String</span>,        <span class="hljs-comment">// UUID for correlation</span>
    <span class="hljs-keyword">pub</span> success: <span class="hljs-built_in">bool</span>,
    <span class="hljs-keyword">pub</span> error_message: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;,
    <span class="hljs-keyword">pub</span> request_body: <span class="hljs-built_in">Option</span>&lt;JsonValue&gt;,   <span class="hljs-comment">// Sanitized!</span>
    <span class="hljs-keyword">pub</span> response_body: <span class="hljs-built_in">Option</span>&lt;JsonValue&gt;,  <span class="hljs-comment">// Sanitized!</span>
}

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">AuditOperation</span></span> {
    Read,
    Create,
    Update,
    Delete,
    Search,
}
</code></pre>
<h3 id="heading-the-audit-log-flow">The Audit Log Flow</h3>
<p>Every handler follows this pattern:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">patient_create</span></span>(
    State(state): State&lt;AppState&gt;,
    auth: AuthContext,
    Json(patient): Json&lt;Patient&gt;,
) -&gt; <span class="hljs-built_in">Result</span>&lt;Response, AppError&gt; {
    <span class="hljs-keyword">let</span> request_id = uuid::Uuid::new_v4().to_string();

    <span class="hljs-comment">// Prepare audit log params</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> audit_params = AuditLogParams {
        resource_type: <span class="hljs-string">"Patient"</span>.to_string(),
        resource_id: <span class="hljs-literal">None</span>,
        operation: AuditOperation::Create,
        user_id: auth.user_id.clone(),
        client_id: auth.client_id.clone(),
        request_id: request_id.clone(),
        success: <span class="hljs-literal">false</span>,  <span class="hljs-comment">// Will be updated</span>
        error_message: <span class="hljs-literal">None</span>,
        request_body: serde_json::to_value(&amp;patient).ok(),
        ..<span class="hljs-built_in">Default</span>::default()
    };

    <span class="hljs-comment">// Execute operation</span>
    <span class="hljs-keyword">let</span> result = <span class="hljs-keyword">async</span> {
        <span class="hljs-keyword">let</span> id = state.create_patient(&amp;auth.client_id, &amp;patient).<span class="hljs-keyword">await</span>?;
        Ok::&lt;_, AppError&gt;(id)
    }.<span class="hljs-keyword">await</span>;

    <span class="hljs-comment">// Update audit params based on result</span>
    <span class="hljs-keyword">match</span> result {
        <span class="hljs-literal">Ok</span>(id) =&gt; {
            audit_params.success = <span class="hljs-literal">true</span>;
            audit_params.resource_id = <span class="hljs-literal">Some</span>(id.clone());

            <span class="hljs-comment">// Log success</span>
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Err</span>(e) = state.audit_logger.log(audit_params).<span class="hljs-keyword">await</span> {
                tracing::error!(<span class="hljs-string">"Failed to log audit: {}"</span>, e);
            }

            <span class="hljs-literal">Ok</span>((StatusCode::CREATED, Json(json!({<span class="hljs-string">"id"</span>: id}))).into_response())
        }
        <span class="hljs-literal">Err</span>(e) =&gt; {
            audit_params.error_message = <span class="hljs-literal">Some</span>(e.to_string());

            <span class="hljs-comment">// Log failure</span>
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Err</span>(log_err) = state.audit_logger.log(audit_params).<span class="hljs-keyword">await</span> {
                tracing::error!(<span class="hljs-string">"Failed to log audit: {}"</span>, log_err);
            }

            <span class="hljs-literal">Err</span>(e)
        }
    }
}
</code></pre>
<p><strong>Critical:</strong> Even if the request fails, we log it. Especially if it fails.</p>
<h3 id="heading-sanitizing-sensitive-data">Sanitizing Sensitive Data</h3>
<p>We log request/response bodies, but <strong>we sanitize PHI</strong> (Protected Health Information):</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">sanitize_for_audit</span></span>(value: &amp;JsonValue) -&gt; JsonValue {
    <span class="hljs-keyword">match</span> value {
        JsonValue::Object(map) =&gt; {
            <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> sanitized = serde_json::Map::new();
            <span class="hljs-keyword">for</span> (key, val) <span class="hljs-keyword">in</span> map {
                <span class="hljs-keyword">let</span> sanitized_val = <span class="hljs-keyword">if</span> SENSITIVE_FIELDS.contains(&amp;key.as_str()) {
                    json!(<span class="hljs-string">"[REDACTED]"</span>)
                } <span class="hljs-keyword">else</span> {
                    sanitize_for_audit(val)
                };
                sanitized.insert(key.clone(), sanitized_val);
            }
            JsonValue::Object(sanitized)
        }
        JsonValue::Array(arr) =&gt; {
            JsonValue::Array(arr.iter().map(sanitize_for_audit).collect())
        }
        _ =&gt; value.clone(),
    }
}

<span class="hljs-keyword">const</span> SENSITIVE_FIELDS: &amp;[&amp;<span class="hljs-built_in">str</span>] = &amp;[
    <span class="hljs-string">"ssn"</span>, <span class="hljs-string">"social_security"</span>, <span class="hljs-string">"password"</span>, <span class="hljs-string">"token"</span>,
    <span class="hljs-string">"birthDate"</span>, <span class="hljs-string">"deceased"</span>, <span class="hljs-string">"multipleBirth"</span>  <span class="hljs-comment">// Some PHI</span>
];
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768324089274/82d6b7a5-f533-4c64-85b5-0856c996b14b.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-audit-log-storage">Audit Log Storage</h3>
<p>Audit logs go to a <strong>separate database</strong> from application data:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> audit_logs (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">BIGINT</span> AUTO_INCREMENT PRIMARY <span class="hljs-keyword">KEY</span>,
    resource_type <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>),
    resource_id <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>),
    operation ENUM(<span class="hljs-string">'read'</span>, <span class="hljs-string">'create'</span>, <span class="hljs-string">'update'</span>, <span class="hljs-string">'delete'</span>, <span class="hljs-string">'search'</span>),
    user_id <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    client_id <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    ip_address <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">45</span>),
    user_agent <span class="hljs-built_in">TEXT</span>,
    request_id <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">36</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    <span class="hljs-keyword">success</span> <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    error_message <span class="hljs-built_in">TEXT</span>,
    request_body <span class="hljs-keyword">JSON</span>,
    response_body <span class="hljs-keyword">JSON</span>,
    created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>,

    <span class="hljs-keyword">INDEX</span> idx_client_id (client_id),
    <span class="hljs-keyword">INDEX</span> idx_user_id (user_id),
    <span class="hljs-keyword">INDEX</span> idx_request_id (request_id),
    <span class="hljs-keyword">INDEX</span> idx_created_at (created_at)
);
</code></pre>
<p><strong>Retention:</strong> We keep logs for <strong>7 years</strong> (HIPAA requirement).</p>
<h2 id="heading-rate-limiting-defense-against-abuse">Rate Limiting: Defense Against Abuse</h2>
<p>Without rate limiting, one malicious (or buggy) client could DOS the entire system.</p>
<p>I implemented <strong>per-tenant rate limiting</strong> using a token bucket algorithm:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">RateLimiter</span></span> {
    buckets: Arc&lt;Mutex&lt;HashMap&lt;<span class="hljs-built_in">String</span>, TokenBucket&gt;&gt;&gt;,
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TokenBucket</span></span> {
    tokens: <span class="hljs-built_in">f64</span>,
    last_refill: Instant,
    capacity: <span class="hljs-built_in">f64</span>,
    refill_rate: <span class="hljs-built_in">f64</span>,  <span class="hljs-comment">// tokens per second</span>
}

<span class="hljs-keyword">impl</span> RateLimiter {
    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">check_rate_limit</span></span>(
        &amp;<span class="hljs-keyword">self</span>,
        client_id: &amp;<span class="hljs-built_in">str</span>,
        tokens_needed: <span class="hljs-built_in">f64</span>,
    ) -&gt; <span class="hljs-built_in">Result</span>&lt;RateLimitInfo, AppError&gt; {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> buckets = <span class="hljs-keyword">self</span>.buckets.lock().unwrap();

        <span class="hljs-keyword">let</span> bucket = buckets.entry(client_id.to_string())
            .or_insert_with(|| TokenBucket {
                tokens: <span class="hljs-number">100.0</span>,  <span class="hljs-comment">// Initial capacity</span>
                last_refill: Instant::now(),
                capacity: <span class="hljs-number">100.0</span>,
                refill_rate: <span class="hljs-number">10.0</span>,  <span class="hljs-comment">// 10 req/s sustained</span>
            });

        <span class="hljs-comment">// Refill tokens based on elapsed time</span>
        <span class="hljs-keyword">let</span> elapsed = bucket.last_refill.elapsed().as_secs_f64();
        bucket.tokens = (bucket.tokens + elapsed * bucket.refill_rate)
            .min(bucket.capacity);
        bucket.last_refill = Instant::now();

        <span class="hljs-comment">// Check if we have enough tokens</span>
        <span class="hljs-keyword">if</span> bucket.tokens &gt;= tokens_needed {
            bucket.tokens -= tokens_needed;

            <span class="hljs-literal">Ok</span>(RateLimitInfo {
                limit: bucket.capacity <span class="hljs-keyword">as</span> <span class="hljs-built_in">i32</span>,
                remaining: bucket.tokens <span class="hljs-keyword">as</span> <span class="hljs-built_in">i32</span>,
                reset: (bucket.capacity - bucket.tokens) / bucket.refill_rate,
            })
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-literal">Err</span>(AppError::RateLimitExceeded)
        }
    }
}
</code></pre>
<p><strong>Limits:</strong></p>
<ul>
<li><p><strong>Burst:</strong> 100 requests</p>
</li>
<li><p><strong>Sustained:</strong> 10 req/s</p>
</li>
<li><p><strong>Scope:</strong> Per tenant</p>
</li>
</ul>
<p>Headers returned:</p>
<pre><code class="lang-plaintext">X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 2.7
</code></pre>
<h2 id="heading-security-checklist">Security Checklist</h2>
<p>Before going to production, I verified:</p>
<ul>
<li><p>✅ <strong>All endpoints require authentication</strong> (except health check)</p>
</li>
<li><p>✅ <strong>JWT signatures validated</strong> against Keycloak JWKS</p>
</li>
<li><p>✅ <strong>Tenant isolation enforced</strong> (client_id in every query)</p>
</li>
<li><p>✅ <strong>Audit logging on all operations</strong></p>
</li>
<li><p>✅ <strong>Sensitive data sanitized</strong> in logs</p>
</li>
<li><p>✅ <strong>Rate limiting per tenant</strong></p>
</li>
<li><p>✅ <strong>Database URLs encrypted at rest</strong></p>
</li>
<li><p>✅ <strong>No secrets in logs</strong> (URL sanitization)</p>
</li>
<li><p>✅ <strong>HTTPS enforced</strong> (TLS 1.3)</p>
</li>
<li><p>✅ <strong>CORS configured</strong> (whitelist only)</p>
</li>
</ul>
<h2 id="heading-what-i-learned">What I Learned</h2>
<p><strong>✅ What Worked:</strong></p>
<ul>
<li><p>Keycloak handles the hard stuff (MFA, SAML, etc.)</p>
</li>
<li><p>Audit logs helps security review in the future</p>
</li>
<li><p>Rate limiting prevent buggy client or attacs from taking us down</p>
</li>
</ul>
<p><strong>❌ What Didn't:</strong></p>
<ul>
<li><p>Initial design logged full JWT tokens (oops!)</p>
</li>
<li><p>Forgot to sanitize database URLs in error messages</p>
</li>
</ul>
<p><strong>🔧 Improvements Made:</strong></p>
<ul>
<li><p>Added comprehensive sanitization</p>
</li>
<li><p>Implemented circuit breakers per tenant</p>
</li>
</ul>
<h2 id="heading-up-next-the-admin-panel">Up Next: The Admin Panel</h2>
<p>Security is invisible to users. The <strong>admin panel</strong> is where they actually interact with the system.</p>
<p>In Part 5, we'll build a Next.js admin interface that lets tenants configure mappings without writing code (or calling me at 2 AM).</p>
<hr />
<p><strong>Security Resources:</strong></p>
<ul>
<li><p><a target="_blank" href="https://owasp.org/www-project-api-security/">OWASP API Security Top 10</a></p>
</li>
<li><p><a target="_blank" href="https://www.keycloak.org/documentation">Keycloak Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://www.hhs.gov/hipaa/for-professionals/security/index.html">HIPAA Compliance Checklist</a></p>
</li>
</ul>
<p><strong>Discussion:</strong> How do you handle audit logging in your systems? What's your retention policy? Let's discuss!</p>
]]></content:encoded></item><item><title><![CDATA[Dynamic Mapping Engine: Bridging Legacy and Modern Healthcare]]></title><description><![CDATA[← Part 2: Multi-Tenant Architecture
The Legacy Database Problem
Here's a real example from one of our tenants:
Hospital A stores patients like this:
CREATE TABLE pacientes (
    id_paciente INT PRIMARY KEY,
    rut_pac VARCHAR(12),
    nom_pac VARCHA...]]></description><link>https://compile.care/building-a-fhir-adapter-from-scratch-part-3</link><guid isPermaLink="true">https://compile.care/building-a-fhir-adapter-from-scratch-part-3</guid><category><![CDATA[fhir]]></category><category><![CDATA[hl7]]></category><category><![CDATA[Rust]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Alberto Lagos]]></dc:creator><pubDate>Tue, 13 Jan 2026 16:51:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768323113062/d9af59bc-bab8-4c90-9b89-c6f960721618.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p><a target="_blank" href="./02-multi-tenant-architecture.md">← Part 2: Multi-Tenant Architecture</a></p>
<h2 id="heading-the-legacy-database-problem">The Legacy Database Problem</h2>
<p>Here's a real example from one of our tenants:</p>
<p><strong>Hospital A</strong> stores patients like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> pacientes (
    id_paciente <span class="hljs-built_in">INT</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    rut_pac <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">12</span>),
    nom_pac <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>),
    ap_pat_pac <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>),
    ap_mat_pac <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>),
    fec_nac_pac <span class="hljs-built_in">DATE</span>,
    sexo_pac <span class="hljs-built_in">CHAR</span>(<span class="hljs-number">1</span>)  <span class="hljs-comment">-- 'M' or 'F'</span>
);
</code></pre>
<p><strong>Hospital B</strong> stores patients like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> usuarios (
    id_usr <span class="hljs-built_in">INT</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    rut_usr <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">15</span>),
    nombre_usr <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">150</span>),
    fecha_nacimiento <span class="hljs-built_in">DATE</span>,
    usr_activo <span class="hljs-built_in">TINYINT</span>  <span class="hljs-comment">-- 0 or 1</span>
);
</code></pre>
<p>Both need to map to the <strong>same FHIR Patient resource</strong>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"resourceType"</span>: <span class="hljs-string">"Patient"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"12345"</span>,
  <span class="hljs-attr">"identifier"</span>: [{<span class="hljs-attr">"value"</span>: <span class="hljs-string">"12345678-9"</span>}],
  <span class="hljs-attr">"name"</span>: [{<span class="hljs-attr">"family"</span>: <span class="hljs-string">"Garcia"</span>, <span class="hljs-attr">"given"</span>: [<span class="hljs-string">"Juan"</span>]}],
  <span class="hljs-attr">"birthDate"</span>: <span class="hljs-string">"1985-03-15"</span>,
  <span class="hljs-attr">"gender"</span>: <span class="hljs-string">"male"</span>,
  <span class="hljs-attr">"active"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<p>The challenge: <strong>How do we map arbitrary database schemas to FHIR without writing custom code for each tenant?</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768322800244/4a27b149-e8f6-4561-a0a2-0cfd5ad7a88b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-solution-dynamic-mapping-configuration">Solution: Dynamic Mapping Configuration</h2>
<p>Instead of code generation or hardcoded mappings, I built a <strong>configuration-driven mapping engine</strong> that translates between databases and FHIR at runtime.</p>
<h3 id="heading-the-mapping-model">The Mapping Model</h3>
<p>Each tenant defines <strong>table mappings</strong> and <strong>field mappings</strong>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TableMapping</span></span> {
    <span class="hljs-keyword">pub</span> fhir_resource_type: <span class="hljs-built_in">String</span>,      <span class="hljs-comment">// "Patient"</span>
    <span class="hljs-keyword">pub</span> database_table_name: <span class="hljs-built_in">String</span>,     <span class="hljs-comment">// "pacientes" or "usuarios"</span>
    <span class="hljs-keyword">pub</span> database_schema: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;, <span class="hljs-comment">// Some tenants use schemas</span>
}

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">FieldMapping</span></span> {
    <span class="hljs-keyword">pub</span> fhir_path: <span class="hljs-built_in">String</span>,          <span class="hljs-comment">// "name[0].family"</span>
    <span class="hljs-keyword">pub</span> database_column: <span class="hljs-built_in">String</span>,    <span class="hljs-comment">// "ap_pat_pac"</span>
    <span class="hljs-keyword">pub</span> data_type: DataType,        <span class="hljs-comment">// String, Integer, Boolean, Date</span>
    <span class="hljs-keyword">pub</span> transformation_id: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">i32</span>&gt;,  <span class="hljs-comment">// Optional data transformation</span>
    <span class="hljs-keyword">pub</span> is_required: <span class="hljs-built_in">bool</span>,
    <span class="hljs-keyword">pub</span> is_primary_key: <span class="hljs-built_in">bool</span>,
}
</code></pre>
<p>Tenants configure these via an admin panel (more on that in Part 5).</p>
<h2 id="heading-fhir-path-parser-navigating-nested-structures">FHIR Path Parser: Navigating Nested Structures</h2>
<p>FHIR is deeply nested. A patient's family name isn't just <code>name</code>—it's <code>name[0].family</code>.</p>
<p>I built a <strong>FHIR path parser</strong> that handles:</p>
<ul>
<li><p><strong>Simple fields:</strong> <code>id</code>, <code>gender</code>, <code>birthDate</code></p>
</li>
<li><p><strong>Array access:</strong> <code>name[0]</code>, <code>identifier[1]</code></p>
</li>
<li><p><strong>Nested fields:</strong> <code>name[0].family</code>, <code>identifier[0].value</code></p>
</li>
<li><p><strong>Array filters:</strong> <code>identifier[use='official'].value</code> (future)</p>
</li>
</ul>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">PathSegment</span></span> {
    Field(<span class="hljs-built_in">String</span>),           <span class="hljs-comment">// "name"</span>
    ArrayIndex(<span class="hljs-built_in">usize</span>),       <span class="hljs-comment">// [0]</span>
    ArrayFilter {            <span class="hljs-comment">// [use='official']</span>
        key: <span class="hljs-built_in">String</span>,
        value: <span class="hljs-built_in">String</span>,
    },
}

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">FhirPath</span></span> {
    segments: <span class="hljs-built_in">Vec</span>&lt;PathSegment&gt;,
}

<span class="hljs-keyword">impl</span> FhirPath {
    <span class="hljs-comment">// Parse "name[0].family" into segments</span>
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">parse</span></span>(path: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-keyword">Self</span>&gt; {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> segments = <span class="hljs-built_in">Vec</span>::new();
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> current = <span class="hljs-built_in">String</span>::new();

        <span class="hljs-keyword">for</span> ch <span class="hljs-keyword">in</span> path.chars() {
            <span class="hljs-keyword">match</span> ch {
                <span class="hljs-string">'.'</span> =&gt; {
                    <span class="hljs-keyword">if</span> !current.is_empty() {
                        segments.push(PathSegment::Field(current.clone()));
                        current.clear();
                    }
                }
                <span class="hljs-string">'['</span> =&gt; {
                    <span class="hljs-keyword">if</span> !current.is_empty() {
                        segments.push(PathSegment::Field(current.clone()));
                        current.clear();
                    }
                    <span class="hljs-comment">// Parse array index or filter...</span>
                }
                _ =&gt; current.push(ch),
            }
        }

        <span class="hljs-keyword">if</span> !current.is_empty() {
            segments.push(PathSegment::Field(current));
        }

        <span class="hljs-literal">Ok</span>(FhirPath { segments })
    }

    <span class="hljs-comment">// Set a value in FHIR JSON using this path</span>
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">set</span></span>(&amp;<span class="hljs-keyword">self</span>, resource: &amp;<span class="hljs-keyword">mut</span> JsonValue, value: JsonValue) -&gt; <span class="hljs-built_in">Result</span>&lt;()&gt; {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> current = resource;

        <span class="hljs-keyword">for</span> (i, segment) <span class="hljs-keyword">in</span> <span class="hljs-keyword">self</span>.segments.iter().enumerate() {
            <span class="hljs-keyword">let</span> is_last = i == <span class="hljs-keyword">self</span>.segments.len() - <span class="hljs-number">1</span>;

            <span class="hljs-keyword">match</span> segment {
                PathSegment::Field(name) =&gt; {
                    <span class="hljs-keyword">if</span> is_last {
                        current[name] = value.clone();
                        <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(());
                    } <span class="hljs-keyword">else</span> {
                        <span class="hljs-comment">// Navigate deeper, creating structure if needed</span>
                        <span class="hljs-keyword">if</span> !current.get(name).is_some() {
                            <span class="hljs-comment">// Check what the next segment needs</span>
                            <span class="hljs-keyword">if</span> matches!(<span class="hljs-keyword">self</span>.segments.get(i + <span class="hljs-number">1</span>), <span class="hljs-literal">Some</span>(PathSegment::ArrayIndex(_))) {
                                current[name] = json!([]);  <span class="hljs-comment">// Create array</span>
                            } <span class="hljs-keyword">else</span> {
                                current[name] = json!({});  <span class="hljs-comment">// Create object</span>
                            }
                        }
                        current = &amp;<span class="hljs-keyword">mut</span> current[name];
                    }
                }
                PathSegment::ArrayIndex(idx) =&gt; {
                    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> JsonValue::Array(arr) = current {
                        <span class="hljs-comment">// Ensure array is large enough</span>
                        <span class="hljs-keyword">while</span> arr.len() &lt;= *idx {
                            arr.push(json!({}));
                        }

                        <span class="hljs-keyword">if</span> is_last {
                            arr[*idx] = value.clone();
                            <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(());
                        } <span class="hljs-keyword">else</span> {
                            current = &amp;<span class="hljs-keyword">mut</span> arr[*idx];
                        }
                    }
                }
                _ =&gt; {}
            }
        }

        <span class="hljs-literal">Ok</span>(())
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768322856010/c37ed5ac-42ad-4a9c-8d61-fb9838223694.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-type-inference-when-the-database-lies">Type Inference: When the Database Lies</h2>
<p>Here's a nasty surprise: MySQL stores booleans as <code>TINYINT</code> (0 or 1). But FHIR expects <code>true</code> or <code>false</code>.</p>
<p>Similarly, many legacy systems store IDs as <code>INT</code>, but FHIR requires them as strings.</p>
<p>I built a <strong>type inference system</strong> that knows the FHIR spec:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">infer_fhir_type_from_path</span></span>(fhir_path: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;DataType&gt; {
    <span class="hljs-keyword">let</span> path_lower = fhir_path.to_lowercase();

    <span class="hljs-comment">// Boolean fields</span>
    <span class="hljs-keyword">if</span> path_lower == <span class="hljs-string">"active"</span>
        || path_lower.ends_with(<span class="hljs-string">".active"</span>)
        || path_lower == <span class="hljs-string">"deceased"</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Some</span>(DataType::Boolean);
    }

    <span class="hljs-comment">// String fields (even if DB stores as INT)</span>
    <span class="hljs-keyword">if</span> fhir_path == <span class="hljs-string">"id"</span>
        || path_lower.ends_with(<span class="hljs-string">".id"</span>)
        || path_lower.contains(<span class="hljs-string">"identifier"</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Some</span>(DataType::<span class="hljs-built_in">String</span>);
    }

    <span class="hljs-comment">// Date fields</span>
    <span class="hljs-keyword">if</span> path_lower == <span class="hljs-string">"birthdate"</span>
        || path_lower.ends_with(<span class="hljs-string">".date"</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Some</span>(DataType::Date);
    }

    <span class="hljs-literal">None</span>  <span class="hljs-comment">// Use DB type</span>
}
</code></pre>
<p>During mapping, the engine:</p>
<ol>
<li><p>Reads value from database (e.g., <code>TINYINT 1</code>)</p>
</li>
<li><p>Checks if FHIR expects a specific type</p>
</li>
<li><p>Converts: <code>1</code> → <code>true</code>, <code>42</code> → <code>"42"</code>, etc.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768322904859/06e87a5e-3578-4465-96f7-8c87ba14565b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-transformations-beyond-simple-type-conversion">Transformations: Beyond Simple Type Conversion</h2>
<p>Sometimes you need more than type conversion. Examples:</p>
<ul>
<li><p><strong>Enum mapping:</strong> <code>'M'</code> → <code>"male"</code>, <code>'F'</code> → <code>"female"</code></p>
</li>
<li><p><strong>String combination:</strong> Join <code>first_name + last_name</code> into <code>name[0].given</code></p>
</li>
<li><p><strong>Date formatting:</strong> <code>DD/MM/YYYY</code> → <code>YYYY-MM-DD</code></p>
</li>
</ul>
<p>I built a <strong>transformation system</strong>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">TransformationType</span></span> {
    EnumMapping,    <span class="hljs-comment">// Map values (M → male)</span>
    Combine,        <span class="hljs-comment">// Join array elements</span>
    Split,          <span class="hljs-comment">// Split string into array</span>
    Format,         <span class="hljs-comment">// Date/time formatting</span>
    Conditional,    <span class="hljs-comment">// If-then-else logic</span>
}

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Transformation</span></span> {
    <span class="hljs-keyword">pub</span> name: <span class="hljs-built_in">String</span>,
    <span class="hljs-keyword">pub</span> transformation_type: TransformationType,
    <span class="hljs-keyword">pub</span> configuration: JsonValue,  <span class="hljs-comment">// Type-specific config</span>
}
</code></pre>
<p>Example transformation for gender:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Gender M/F to FHIR"</span>,
  <span class="hljs-attr">"transformation_type"</span>: <span class="hljs-string">"enum_mapping"</span>,
  <span class="hljs-attr">"configuration"</span>: {
    <span class="hljs-attr">"M"</span>: <span class="hljs-string">"male"</span>,
    <span class="hljs-attr">"F"</span>: <span class="hljs-string">"female"</span>,
    <span class="hljs-attr">"I"</span>: <span class="hljs-string">"other"</span>
  }
}
</code></pre>
<p>The transformation engine applies these <strong>before</strong> setting the FHIR value:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">transform_to_fhir</span></span>(
    &amp;<span class="hljs-keyword">self</span>,
    value: &amp;DbValue,
    transformation_id: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">i32</span>&gt;,
    target_type: <span class="hljs-built_in">Option</span>&lt;DataType&gt;,
) -&gt; <span class="hljs-built_in">Result</span>&lt;JsonValue&gt; {
    <span class="hljs-comment">// 1. Convert DB value to JSON with type awareness</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> json_value = dbvalue_to_json_with_type(value, target_type);

    <span class="hljs-comment">// 2. Apply transformation if specified</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(id) = transformation_id {
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(transformation) = <span class="hljs-keyword">self</span>.transformations.get(&amp;id) {
            json_value = <span class="hljs-keyword">self</span>.apply_transformation(&amp;json_value, transformation)?;
        }
    }

    <span class="hljs-literal">Ok</span>(json_value)
}
</code></pre>
<h2 id="heading-the-mapping-process-putting-it-all-together">The Mapping Process: Putting It All Together</h2>
<p>When a request comes in for <code>GET /fhir/Patient/123</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">to_fhir</span></span>(
    &amp;<span class="hljs-keyword">self</span>,
    resource_type: &amp;<span class="hljs-built_in">str</span>,
    db_row: &amp;HashMap&lt;<span class="hljs-built_in">String</span>, DbValue&gt;,
) -&gt; <span class="hljs-built_in">Result</span>&lt;JsonValue&gt; {
    <span class="hljs-comment">// 1. Get field mappings for this resource type</span>
    <span class="hljs-keyword">let</span> table_mapping = <span class="hljs-keyword">self</span>.config.get_table_mapping(resource_type)?;
    <span class="hljs-keyword">let</span> field_mappings = &amp;table_mapping.field_mappings;

    <span class="hljs-comment">// 2. Start with base FHIR resource</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> fhir_resource = json!({
        <span class="hljs-string">"resourceType"</span>: resource_type
    });

    <span class="hljs-comment">// 3. Map each field</span>
    <span class="hljs-keyword">for</span> field_mapping <span class="hljs-keyword">in</span> field_mappings {
        <span class="hljs-comment">// Get database value</span>
        <span class="hljs-keyword">let</span> db_value = <span class="hljs-keyword">match</span> db_row.get(&amp;field_mapping.database_column) {
            <span class="hljs-literal">Some</span>(val) <span class="hljs-keyword">if</span> !matches!(val, DbValue::Null) =&gt; val,
            _ =&gt; <span class="hljs-keyword">continue</span>,  <span class="hljs-comment">// Skip missing/null values</span>
        };

        <span class="hljs-comment">// Infer expected FHIR type</span>
        <span class="hljs-keyword">let</span> expected_type = infer_fhir_type_from_path(&amp;field_mapping.fhir_path);

        <span class="hljs-comment">// Transform value</span>
        <span class="hljs-keyword">let</span> fhir_value = <span class="hljs-keyword">self</span>.transform_engine.transform_to_fhir(
            db_value,
            field_mapping.transformation_id,
            expected_type,
        )?;

        <span class="hljs-comment">// Set value using FHIR path</span>
        <span class="hljs-keyword">let</span> fhir_path = FhirPath::parse(&amp;field_mapping.fhir_path)?;
        fhir_path.set(&amp;<span class="hljs-keyword">mut</span> fhir_resource, fhir_value)?;
    }

    <span class="hljs-comment">// 4. Clean up empty objects in arrays</span>
    clean_empty_objects(&amp;<span class="hljs-keyword">mut</span> fhir_resource);

    <span class="hljs-literal">Ok</span>(fhir_resource)
}
</code></pre>
<p><strong>Result:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"resourceType"</span>: <span class="hljs-string">"Patient"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"123"</span>,
  <span class="hljs-attr">"active"</span>: <span class="hljs-literal">true</span>,              <span class="hljs-comment">// Converted from TINYINT 1</span>
  <span class="hljs-attr">"identifier"</span>: [
    {<span class="hljs-attr">"value"</span>: <span class="hljs-string">"12345678-9"</span>}    <span class="hljs-comment">// From rut_pac column</span>
  ],
  <span class="hljs-attr">"name"</span>: [{
    <span class="hljs-attr">"family"</span>: <span class="hljs-string">"Garcia"</span>,        <span class="hljs-comment">// From ap_pat_pac</span>
    <span class="hljs-attr">"given"</span>: [<span class="hljs-string">"Juan"</span>]          <span class="hljs-comment">// From nom_pac</span>
  }],
  <span class="hljs-attr">"gender"</span>: <span class="hljs-string">"male"</span>,            <span class="hljs-comment">// Transformed from 'M'</span>
  <span class="hljs-attr">"birthDate"</span>: <span class="hljs-string">"1985-03-15"</span>    <span class="hljs-comment">// From fec_nac_pac</span>
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768322958443/0442d118-9d3d-4e48-a380-a1b48f14e684.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-edge-cases-and-gotchas">Edge Cases and Gotchas</h2>
<h3 id="heading-1-sparse-array-indices">1. Sparse Array Indices</h3>
<p>When a mapping uses <code>identifier[1].value</code>, the path parser creates empty objects at <code>[0]</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"identifier"</span>: [{}, {<span class="hljs-attr">"value"</span>: <span class="hljs-string">"X"</span>}]  <span class="hljs-comment">// ❌ Breaks deserialization</span>
}
</code></pre>
<p><strong>Solution:</strong> Clean empty objects after mapping:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">clean_empty_objects</span></span>(value: &amp;<span class="hljs-keyword">mut</span> JsonValue) {
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> JsonValue::Array(arr) = value {
        arr.retain(|item| {
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> JsonValue::Object(map) = item {
                !map.is_empty()  <span class="hljs-comment">// Keep only non-empty objects</span>
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-literal">true</span>
            }
        });
    }
    <span class="hljs-comment">// Recurse into nested structures...</span>
}
</code></pre>
<h3 id="heading-2-null-values">2. NULL Values</h3>
<p>Database <code>NULL</code> ≠ JSON <code>null</code>. Omit the field entirely:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Skip NULL values unless required</span>
<span class="hljs-keyword">if</span> matches!(db_value, DbValue::Null) {
    <span class="hljs-keyword">if</span> field_mapping.is_required {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(<span class="hljs-string">"Required field is NULL"</span>);
    }
    <span class="hljs-keyword">continue</span>;  <span class="hljs-comment">// Skip this field</span>
}
</code></pre>
<h3 id="heading-3-bidirectional-mapping">3. Bidirectional Mapping</h3>
<p>Creating a Patient means going <strong>FHIR → DB</strong>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">to_db</span></span>(
    &amp;<span class="hljs-keyword">self</span>,
    resource_type: &amp;<span class="hljs-built_in">str</span>,
    fhir_resource: &amp;JsonValue,
) -&gt; <span class="hljs-built_in">Result</span>&lt;HashMap&lt;<span class="hljs-built_in">String</span>, DbValue&gt;&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> db_row = HashMap::new();

    <span class="hljs-keyword">for</span> field_mapping <span class="hljs-keyword">in</span> field_mappings {
        <span class="hljs-keyword">let</span> fhir_path = FhirPath::parse(&amp;field_mapping.fhir_path)?;
        <span class="hljs-keyword">let</span> fhir_value = fhir_path.get(fhir_resource)?;

        <span class="hljs-comment">// Reverse transformation</span>
        <span class="hljs-keyword">let</span> db_value = <span class="hljs-keyword">self</span>.transform_engine.transform_to_db(
            &amp;fhir_value,
            field_mapping.transformation_id,
        )?;

        db_row.insert(field_mapping.database_column.clone(), db_value);
    }

    <span class="hljs-literal">Ok</span>(db_row)
}
</code></pre>
<h2 id="heading-performance-considerations">Performance Considerations</h2>
<p><strong>The Good:</strong></p>
<ul>
<li><p>Type inference is <code>O(1)</code> (string comparison)</p>
</li>
<li><p>Path parsing is cached per-mapping</p>
</li>
<li><p>Transformations are pure functions (no I/O)</p>
</li>
</ul>
<p><strong>The Tradeoff:</strong></p>
<ul>
<li><p>Dynamic mapping is ~2x slower than hand-coded (15ms vs 8ms)</p>
</li>
<li><p>But it means zero code for new tenants</p>
</li>
<li><p>Worth it for our use case</p>
</li>
</ul>
<h2 id="heading-coming-up-security-and-observability">Coming Up: Security and Observability</h2>
<p>The mapping engine handles <em>what</em> data gets transformed. But we also need to know <em>who</em> accessed <em>what</em> data and <em>when</em>.</p>
<p>In Part 4, we'll cover:</p>
<ul>
<li><p>Authentication and authorization with Keycloak</p>
</li>
<li><p>Audit logging for HIPAA compliance</p>
</li>
<li><p>Rate limiting and circuit breakers</p>
</li>
<li><p>Monitoring what matters</p>
</li>
</ul>
<hr />
<p><strong>Discussion:</strong> How do you handle schema evolution in your systems? Code generation vs runtime configuration? Let me know your approach!</p>
]]></content:encoded></item><item><title><![CDATA[Multi-Tenant Architecture: Isolating Healthcare Data at Scale]]></title><description><![CDATA[The Multi-Tenancy Challenge
Imagine you're running a SaaS application. Now imagine that application handles the most sensitive data possible: medical records. Now imagine that a single bug could expose Patient A's cancer diagnosis to Hospital B.
That...]]></description><link>https://compile.care/building-a-fhir-adapter-from-scratch-part-2</link><guid isPermaLink="true">https://compile.care/building-a-fhir-adapter-from-scratch-part-2</guid><category><![CDATA[Rust]]></category><category><![CDATA[rust lang]]></category><category><![CDATA[fhir]]></category><dc:creator><![CDATA[Alberto Lagos]]></dc:creator><pubDate>Mon, 05 Jan 2026 14:16:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622941587/20b99714-981b-4647-9970-582269617554.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-multi-tenancy-challenge">The Multi-Tenancy Challenge</h2>
<p>Imagine you're running a SaaS application. Now imagine that application handles the most sensitive data possible: medical records. Now imagine that a single bug could expose Patient A's cancer diagnosis to Hospital B.</p>
<p>That's the stakes we're playing with in a multi-tenant healthcare system.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622362440/485ecc9a-6055-4fa9-8f34-c00ae7da3ec2.png" alt class="image--center mx-auto" /></p>
<p>Most SaaS apps solve multi-tenancy at the application layer—same database, different <code>tenant_id</code> columns. That works for Slack or Notion. It doesn't work for healthcare.</p>
<p>Here's why:</p>
<ul>
<li><p><strong>Regulatory compliance:</strong> HIPAA requires strong data isolation</p>
</li>
<li><p><strong>Different schemas:</strong> Hospital A stores patient names as <code>patient_name</code>, Hospital B uses <code>full_name</code></p>
</li>
<li><p><strong>Legacy systems:</strong> Each tenant runs their own ancient MySQL database on-premise</p>
</li>
<li><p><strong>Zero trust:</strong> One tenant's data must be <em>physically impossible</em> to access from another tenant's queries</p>
</li>
</ul>
<h2 id="heading-the-architecture-database-level-isolation">The Architecture: Database-Level Isolation</h2>
<p>Instead of logical isolation (same DB, different rows), I went with <strong>physical isolation</strong>—each tenant gets their own database connection to their own database.</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Each tenant has their own encrypted database URL</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TenantDatabaseConfig</span></span> {
    <span class="hljs-keyword">pub</span> id: <span class="hljs-built_in">i32</span>,
    <span class="hljs-keyword">pub</span> client_id: <span class="hljs-built_in">String</span>,
    <span class="hljs-keyword">pub</span> database_url: <span class="hljs-built_in">String</span>,  <span class="hljs-comment">// Encrypted</span>
    <span class="hljs-keyword">pub</span> database_type: DatabaseType,
}
</code></pre>
<h3 id="heading-the-connection-pool-dance">The Connection Pool Dance</h3>
<p>Here's the tricky part: you can't create a new database connection for every request. Connections are expensive (100ms+ handshake). But you also can't keep 1000 connection pools open if you have 1000 tenants.</p>
<p>I built a <code>TenantPoolManager</code> that:</p>
<ol>
<li><p><strong>Lazily creates</strong> connection pools (only when needed)</p>
</li>
<li><p><strong>Caches them</strong> in memory for fast access</p>
</li>
<li><p><strong>Evicts idle pools</strong> after 30 minutes of inactivity</p>
</li>
<li><p><strong>Validates connections</strong> before returning them</p>
</li>
</ol>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TenantPoolManager</span></span> {
    pools: Arc&lt;Mutex&lt;HashMap&lt;<span class="hljs-built_in">String</span>, DatabasePool&gt;&gt;&gt;,
    encryption_key: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">u8</span>&gt;,
}

<span class="hljs-keyword">impl</span> TenantPoolManager {
    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_pool</span></span>(&amp;<span class="hljs-keyword">self</span>, client_id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;DatabasePool&gt; {
        <span class="hljs-comment">// Check cache first</span>
        {
            <span class="hljs-keyword">let</span> pools = <span class="hljs-keyword">self</span>.pools.lock().unwrap();
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(pool) = pools.get(client_id) {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(pool.clone());
            }
        }

        <span class="hljs-comment">// Not cached - fetch config, decrypt URL, create pool</span>
        <span class="hljs-keyword">let</span> config = <span class="hljs-keyword">self</span>.fetch_tenant_config(client_id).<span class="hljs-keyword">await</span>?;
        <span class="hljs-keyword">let</span> decrypted_url = <span class="hljs-keyword">self</span>.decrypt_database_url(&amp;config.database_url)?;
        <span class="hljs-keyword">let</span> pool = <span class="hljs-keyword">self</span>.create_pool(&amp;decrypted_url).<span class="hljs-keyword">await</span>?;

        <span class="hljs-comment">// Cache it</span>
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> pools = <span class="hljs-keyword">self</span>.pools.lock().unwrap();
        pools.insert(client_id.to_string(), pool.clone());

        <span class="hljs-literal">Ok</span>(pool)
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622392052/ffc64773-6887-44d8-9177-dc2225f6e9f8.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-encryption">Encryption</h2>
<p>Database URLs contain credentials. Storing them in plaintext would be... unwise.</p>
<p>Every tenant's database URL is encrypted using AES-256-GCM before hitting the database:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">encrypt_database_url</span></span>(&amp;<span class="hljs-keyword">self</span>, url: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">String</span>&gt; {
    <span class="hljs-keyword">let</span> cipher = Aes256Gcm::new(Key::&lt;Aes256Gcm&gt;::from_slice(&amp;<span class="hljs-keyword">self</span>.encryption_key));
    <span class="hljs-keyword">let</span> nonce = Aes256Gcm::generate_nonce(&amp;<span class="hljs-keyword">mut</span> OsRng);

    <span class="hljs-keyword">let</span> ciphertext = cipher.encrypt(&amp;nonce, url.as_bytes())
        .map_err(|e| AdapterError::Encryption(e.to_string()))?;

    <span class="hljs-comment">// Encode as base64 for storage</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> combined = nonce.to_vec();
    combined.extend_from_slice(&amp;ciphertext);
    <span class="hljs-literal">Ok</span>(base64::encode(&amp;combined))
}
</code></pre>
<p>The encryption key lives in environment variables, never in the database.</p>
<h2 id="heading-configuration-caching-speed-vs-consistency">Configuration Caching: Speed vs Consistency</h2>
<p>Each tenant has a configuration:</p>
<ul>
<li><p>Which FHIR resources they support</p>
</li>
<li><p>How to map FHIR paths to database columns</p>
</li>
<li><p>Custom transformations (e.g., "1"/"0" → true/false)</p>
</li>
<li><p>Nested array handling strategies</p>
</li>
</ul>
<p>Loading this from MySQL on every request would be slow (5-10ms per query). But caching it forever means configuration changes don't take effect.</p>
<p>I built a <code>ConfigResolver</code> with a TTL-based cache:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ConfigResolver</span></span> {
    cache: Arc&lt;Mutex&lt;HashMap&lt;<span class="hljs-built_in">String</span>, CachedConfig&gt;&gt;&gt;,
    cache_ttl: Duration,
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">CachedConfig</span></span> {
    config: TenantConfig,
    loaded_at: Instant,
}

<span class="hljs-keyword">impl</span> ConfigResolver {
    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_config</span></span>(&amp;<span class="hljs-keyword">self</span>, client_id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;TenantConfig&gt; {
        <span class="hljs-comment">// Check cache</span>
        {
            <span class="hljs-keyword">let</span> cache = <span class="hljs-keyword">self</span>.cache.lock().unwrap();
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(cached) = cache.get(client_id) {
                <span class="hljs-keyword">if</span> cached.loaded_at.elapsed() &lt; <span class="hljs-keyword">self</span>.cache_ttl {
                    <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(cached.config.clone());
                }
            }
        }

        <span class="hljs-comment">// Cache miss or expired - reload</span>
        <span class="hljs-keyword">let</span> config = <span class="hljs-keyword">self</span>.load_from_database(client_id).<span class="hljs-keyword">await</span>?;

        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> cache = <span class="hljs-keyword">self</span>.cache.lock().unwrap();
        cache.insert(client_id.to_string(), CachedConfig {
            config: config.clone(),
            loaded_at: Instant::now(),
        });

        <span class="hljs-literal">Ok</span>(config)
    }

    <span class="hljs-comment">// Admin panel calls this after configuration changes</span>
    <span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">invalidate</span></span>(&amp;<span class="hljs-keyword">self</span>, client_id: &amp;<span class="hljs-built_in">str</span>) {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> cache = <span class="hljs-keyword">self</span>.cache.lock().unwrap();
        cache.remove(client_id);
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622413017/4a629de8-82e7-4a84-ab63-0468b349d41b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-authentication-keycloak-integration">Authentication: Keycloak Integration</h2>
<p>Multi-tenancy isn't just about data—it's about <em>who can access</em> that data.</p>
<p>I integrated Keycloak for enterprise SSO:</p>
<ul>
<li><p><strong>Client ID embedded in JWT:</strong> Every request includes <code>client_id</code> claim</p>
</li>
<li><p><strong>Role-based access:</strong> <code>fhir-read</code>, <code>fhir-write</code>, <code>fhir-admin</code></p>
</li>
</ul>
<p>The authentication flow:</p>
<ol>
<li><p>User logs in via Keycloak</p>
</li>
<li><p>Every request to the adapter includes this JWT</p>
</li>
<li><p>Middleware validates JWT and extracts <code>client_id</code></p>
</li>
<li><p>All database queries are scoped to that <code>client_id</code></p>
</li>
</ol>
<p>Keycloak issues JWT with claims:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"sub"</span>: <span class="hljs-string">"user-123"</span>,
  <span class="hljs-attr">"client_id"</span>: <span class="hljs-string">"tenant-42"</span>,
  <span class="hljs-attr">"realm_access"</span>: {
    <span class="hljs-attr">"roles"</span>: [<span class="hljs-string">"fhir-read"</span>, <span class="hljs-string">"fhir-write"</span>]
  }
}
</code></pre>
<p><strong>No way to query across tenants. The</strong> <code>client_id</code> is the security boundary.</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Middleware that extracts and validates tenant context</span>
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">auth_middleware</span></span>(
    State(state): State&lt;AppState&gt;,
    req: Request,
    next: Next,
) -&gt; <span class="hljs-built_in">Result</span>&lt;Response, AppError&gt; {
    <span class="hljs-keyword">let</span> token = extract_bearer_token(&amp;req)?;

    <span class="hljs-comment">// Validate JWT signature and expiration</span>
    <span class="hljs-keyword">let</span> claims = state.keycloak.validate_token(&amp;token).<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Extract client_id - this determines data isolation</span>
    <span class="hljs-keyword">let</span> client_id = claims.client_id
        .ok_or_else(|| AppError::Unauthorized(<span class="hljs-string">"Missing client_id"</span>))?;

    <span class="hljs-comment">// Inject into request extensions for downstream handlers</span>
    req.extensions_mut().insert(AuthContext {
        user_id: claims.sub,
        client_id,
        roles: claims.realm_access.roles,
    });

    <span class="hljs-literal">Ok</span>(next.run(req).<span class="hljs-keyword">await</span>)
}
</code></pre>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<h3 id="heading-what-worked">✅ What Worked</h3>
<ol>
<li><p><strong>Database-level isolation:</strong> No worrying about leaked <code>WHERE</code> clauses</p>
</li>
<li><p><strong>Lazy pool creation:</strong> Most tenants are idle most of the time</p>
</li>
<li><p><strong>TTL-based config cache:</strong> 5-minute TTL is a sweet spot</p>
</li>
</ol>
<h3 id="heading-what-didnt">❌ What Didn't</h3>
<ol>
<li><p><strong>Initial design had no pool eviction:</strong> Memory grew unbounded</p>
</li>
<li><p><strong>Encrypted URLs in logs:</strong> Accidentally logged encrypted URLs (look like gibberish, but still bad)</p>
</li>
<li><p><strong>No circuit breakers:</strong> One tenant's broken DB took down the whole adapter</p>
</li>
</ol>
<h3 id="heading-improvements-made">🔧 Improvements Made</h3>
<ol>
<li><p><strong>Added pool eviction</strong> after 30 min idle</p>
</li>
<li><p><strong>Sanitized all logging</strong> (URLs, credentials, PHI)</p>
</li>
<li><p><strong>Per-tenant circuit breakers</strong> (now one tenant can fail without affecting others)</p>
</li>
</ol>
<h2 id="heading-up-next-the-mapping-engine">Up Next: The Mapping Engine</h2>
<p>Physical isolation solves <em>security</em>. But how do we handle the fact that Hospital A stores patient names as <code>patient_name</code> while Hospital B uses <code>nombre_paciente</code>?</p>
<p>That's where the <strong>dynamic mapping engine</strong> comes in.</p>
<hr />
<p><em>Part 3 will dive deep into how the adapter translates arbitrary database schemas into standard FHIR resources at runtime—without code generation.</em></p>
<hr />
<p><strong>Discussion:</strong> How do you handle multi-tenancy in your systems? Physical vs logical isolation? Let me know your thoughts.</p>
]]></content:encoded></item><item><title><![CDATA[Building a FHIR Adapter from Scratch: Why Rust, Why Now]]></title><description><![CDATA[The Problem: Healthcare Data in Silos
Healthcare systems are a mess. I don't mean that lightly—after years of working with legacy hospital databases, I've seen it all: patient data stored in decades-old MySQL schemas (or even Microsoft Access), custo...]]></description><link>https://compile.care/building-a-fhir-adapter-from-scratch-part-1</link><guid isPermaLink="true">https://compile.care/building-a-fhir-adapter-from-scratch-part-1</guid><category><![CDATA[Rust]]></category><category><![CDATA[rust lang]]></category><category><![CDATA[fhir]]></category><category><![CDATA[healthcare]]></category><dc:creator><![CDATA[Alberto Lagos]]></dc:creator><pubDate>Mon, 05 Jan 2026 14:11:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622906514/9d0b5c3a-b518-4055-bf2d-9818b8ce9c2a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-the-problem-healthcare-data-in-silos">The Problem: Healthcare Data in Silos</h1>
<p>Healthcare systems are a mess. I don't mean that lightly—after years of working with legacy hospital databases, I've seen it all: patient data stored in decades-old MySQL schemas (or even Microsoft Access), custom field names that made sense to someone in 1998, and zero interoperability between systems.</p>
<p>When a doctor needs to access a patient's complete medical history, they often can't. The data exists, scattered across multiple incompatible systems, each speaking its own dialect.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622141000/9ef3db8d-c3ef-4c72-a086-47ec29518967.png" alt class="image--center mx-auto" /></p>
<p>FHIR (Fast Healthcare Interoperability Resources) was supposed to solve this. It's a modern standard for exchanging healthcare information electronically. But there's a catch: most healthcare providers run on legacy systems that predate FHIR by decades.</p>
<h2 id="heading-the-solution-a-bridge-between-worlds">The Solution: A Bridge Between Worlds</h2>
<p>I decided to build a <strong>FHIR adapter</strong>—a system that sits between legacy databases and modern FHIR-based applications. It translates old-school database schemas into standard FHIR resources on the fly.</p>
<p>But here's where it gets interesting: this isn't just for one hospital. It's <strong>multi-tenant</strong>, meaning hundreds of healthcare organizations can use the same adapter instance, each with their own completely isolated data and custom database schemas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622167322/0ff0eb3d-42a3-4ffa-9d16-cc0554401101.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-rust">Why Rust?</h2>
<p>I'll be honest: I started from zero with Rust. No production experience, just curiosity and a belief that Rust was the right tool for this job.</p>
<p>Here's why I chose it:</p>
<h3 id="heading-1-performance-matters-in-healthcare">1. <strong>Performance Matters in Healthcare</strong></h3>
<p>When a doctor requests a patient's records, they need them <em>now</em>. Not in 5 seconds, not after a loading spinner—now. Rust's zero-cost abstractions and performance characteristics meant I could build a system that handles thousands of requests per second without breaking a sweat.</p>
<h3 id="heading-2-memory-safety-without-garbage-collection">2. <strong>Memory Safety Without Garbage Collection</strong></h3>
<p>Healthcare data is sensitive. A memory leak or buffer overflow isn't just a bug—it's a potential HIPAA violation. Rust's borrow checker guarantees memory safety at compile time, eliminating entire classes of vulnerabilities that plague C/C++ systems.</p>
<h3 id="heading-3-fearless-concurrency">3. <strong>Fearless Concurrency</strong></h3>
<p>A multi-tenant system means handling hundreds of database connections simultaneously, each to different legacy databases. Rust's ownership model makes concurrent programming not just possible, but safe and predictable.</p>
<pre><code class="lang-rust"><span class="hljs-comment">// This is legal Rust - the compiler guarantees safety</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">handle_request</span></span>(tenant_id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;Patient&gt; {
    <span class="hljs-keyword">let</span> pool = tenant_pool_manager.get_pool(tenant_id).<span class="hljs-keyword">await</span>?;
    <span class="hljs-keyword">let</span> config = config_resolver.get_config(tenant_id).<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Multiple async operations, all safe</span>
    <span class="hljs-keyword">let</span> patient = fetch_patient(&amp;pool, &amp;config).<span class="hljs-keyword">await</span>?;
    <span class="hljs-literal">Ok</span>(patient)
}
</code></pre>
<h3 id="heading-4-learning-curve-as-a-feature">4. <strong>Learning Curve as a Feature</strong></h3>
<p>Yes, Rust is hard. The borrow checker will fight you. You'll spend hours debugging lifetime errors. But here's the thing: <strong>every error you fix at compile time is a bug that won't crash in production</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622211398/f11d9a7b-66a0-4d04-80a2-812bd89792a6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-tech-stack">The Tech Stack</h2>
<p>After evaluating options, here's what I landed on:</p>
<ul>
<li><p><strong>Backend:</strong> Rust + Axum (web framework)</p>
</li>
<li><p><strong>Database:</strong> MySQL (for adapter config) + dynamic connections to tenant databases</p>
</li>
<li><p><strong>Authentication:</strong> Keycloak (enterprise SSO)</p>
</li>
<li><p><strong>Admin Panel:</strong> Next.js 14 + TypeScript</p>
</li>
<li><p><strong>Deployment:</strong> Docker + systemd</p>
</li>
</ul>
<h2 id="heading-early-decisions-that-mattered">Early Decisions That Mattered</h2>
<h3 id="heading-decision-1-dynamic-configuration-over-code-generation">Decision 1: Dynamic Configuration Over Code Generation</h3>
<p>Instead of generating code for each tenant's schema, I built a <strong>dynamic mapping engine</strong> that reads configuration at runtime. This means:</p>
<ul>
<li><p>✅ No recompilation needed when onboarding a new tenant</p>
</li>
<li><p>✅ Tenant admins can configure mappings via UI</p>
</li>
<li><p>❌ More complex runtime logic</p>
</li>
</ul>
<h3 id="heading-decision-2-compile-time-safety-where-possible-runtime-flexibility-where-needed">Decision 2: Compile-Time Safety Where Possible, Runtime Flexibility Where Needed</h3>
<p>I used Rust's type system aggressively for the core adapter logic, but accepted <code>serde_json::Value</code> for the dynamic mapping layer. The tradeoff was worth it.</p>
<h3 id="heading-decision-3-admin-panel-in-typescript-not-rust">Decision 3: Admin Panel in TypeScript, Not Rust</h3>
<p>I could have built the admin UI in Rust (Yew, Leptos, etc.), but Next.js was the pragmatic choice. Development velocity matters, and React's ecosystem is unmatched for building complex forms.</p>
<h2 id="heading-whats-coming-next">What's Coming Next</h2>
<p>In the next posts, I'll dive deeper into:</p>
<ul>
<li><p><strong>Part 2:</strong> Multi-tenant architecture and database isolation</p>
</li>
<li><p><strong>Part 3:</strong> The dynamic mapping engine that powers it all</p>
</li>
<li><p><strong>Part 4:</strong> Security, authentication, and audit logging</p>
</li>
<li><p><strong>Part 5:</strong> Building an admin panel that doesn't suck</p>
</li>
</ul>
<h2 id="heading-starting-from-zero">Starting from Zero</h2>
<p>When I started this project, I had:</p>
<ul>
<li><p>Zero production Rust experience</p>
</li>
<li><p>A vague understanding of FHIR</p>
</li>
<li><p>A belief that healthcare IT could be better</p>
</li>
</ul>
<p>Six months later, I have:</p>
<ul>
<li><p>A working multi-tenant FHIR adapter</p>
</li>
<li><p>20,000+ lines of Rust</p>
</li>
<li><p>Battle scars from fighting the borrow checker (and winning)</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767622246117/968558eb-bb1c-4a50-b5f9-9167502070d0.png" alt class="image--center mx-auto" /></p>
<p>If you're considering Rust for a production project, here's my advice: do it. The learning curve is steep, but the payoff is real. You'll write better code, catch bugs earlier, and sleep better knowing your production system won't randomly segfault at 3 AM.</p>
<hr />
<p><em>This is Part 1 of a 5-part series documenting my journey building a multi-tenant FHIR adapter from scratch. Follow along at</em> <a target="_blank" href="https://compile-care.ghost.io/"><em>compile.care</em></a> <em>for updates.</em></p>
]]></content:encoded></item></channel></rss>