<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: r emrah gökkaya</title>
    <description>The latest articles on DEV Community by r emrah gökkaya (@remrah).</description>
    <link>https://dev.to/remrah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4005491%2F1581662f-f1c5-48dd-9059-92012e0a16dd.jpg</url>
      <title>DEV Community: r emrah gökkaya</title>
      <link>https://dev.to/remrah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/remrah"/>
    <language>en</language>
    <item>
      <title>Offline-First, Encrypted, Audit-Ready: Building CalibKeep with .NET 8 + Avalonia</title>
      <dc:creator>r emrah gökkaya</dc:creator>
      <pubDate>Fri, 07 Aug 2026 10:18:04 +0000</pubDate>
      <link>https://dev.to/remrah/offline-first-encrypted-audit-ready-building-calibkeep-with-net-8-avalonia-9ka</link>
      <guid>https://dev.to/remrah/offline-first-encrypted-audit-ready-building-calibkeep-with-net-8-avalonia-9ka</guid>
      <description>&lt;p&gt;Walk into any small factory and ask how calibration is tracked. Nine times out of ten, someone points at a spreadsheet. It lives on one person's desktop, gets updated when somebody remembers, and when the ISO 9001 auditor asks for proof, that's what gets waved at them. It doesn't hold up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CalibKeep&lt;/strong&gt; is my attempt to fix that: a fully offline, encrypted desktop app that manages calibration schedules, depreciation, and audit trails. I learned a few painful lessons building it. This post is the stack, the traps, and the fixes — in the order I wish I'd hit them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack at a Glance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.NET 8&lt;/strong&gt; + &lt;strong&gt;Avalonia UI 11.2&lt;/strong&gt; (MVVM, CommunityToolkit.Mvvm)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EF Core 8 + SQLCipher&lt;/strong&gt; for an AES-256 encrypted local database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QuestPDF&lt;/strong&gt; for calibration/asset/depreciation reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;xUnit + Moq&lt;/strong&gt;, currently &lt;strong&gt;105 tests&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DPAPI&lt;/strong&gt; for local credential storage (auto-unlock hint only — never the master key)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rule #0: No DbContext Singletons
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Never this&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Always this&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_contextFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDbContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every repository and service opens its own short-lived context via &lt;code&gt;IDbContextFactory&lt;/code&gt;. This keeps shared state out of the picture. No cross-thread context leaks, no weird collisions when the UI thread and a background export hit the database at the same time.&lt;/p&gt;
&lt;h2&gt;
  
  
  The SQLCipher Trap That Cost Me a Day
&lt;/h2&gt;

&lt;p&gt;The scariest bug in this project: &lt;strong&gt;opening a SQLCipher database with the wrong passphrase permanently poisons the connection handle for the process lifetime.&lt;/strong&gt; Not &lt;code&gt;Close()&lt;/code&gt;. Not &lt;code&gt;ClearAllPools()&lt;/code&gt;. Not even GC. The handle stays broken.&lt;/p&gt;

&lt;p&gt;The fix was to &lt;em&gt;never open the file to validate the password&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// metadata.json holds an HMAC marker computed from the passphrase.&lt;/span&gt;
&lt;span class="c1"&gt;// We verify the HMAC before ever touching the SQLite file.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Restore went through the same kind of ordeal. Instead of swapping files around, we moved to SQLite's &lt;strong&gt;online backup API&lt;/strong&gt; (&lt;code&gt;BackupDatabase&lt;/code&gt;) and write into the live DB without locking:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;temp file  ──►  BackupDatabase  ──►  live SQLCipher DB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That eliminated both "file is not a database" and "file is being used" errors for good.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Audit Trail: Write It in the Same Transaction
&lt;/h2&gt;

&lt;p&gt;ISO 9001 wants proof of every change. So every service that does Create/Update/Delete writes an &lt;code&gt;AuditLog&lt;/code&gt; row &lt;strong&gt;in the same transaction&lt;/strong&gt; — no exceptions.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Table&lt;/span&gt;            &lt;span class="k"&gt;FieldName&lt;/span&gt;      &lt;span class="k"&gt;OldValue&lt;/span&gt;        &lt;span class="k"&gt;NewValue&lt;/span&gt;
&lt;span class="k"&gt;Calibration&lt;/span&gt;      &lt;span class="k"&gt;NextDueDate&lt;/span&gt;    &lt;span class="ld"&gt;2026-08-01&lt;/span&gt;      &lt;span class="ld"&gt;2026-11-01&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It's boring code, but it's the difference between "a nice app" and "a compliance tool."&lt;/p&gt;
&lt;h2&gt;
  
  
  Calibration Impact: The Feature That Adds Real Value
&lt;/h2&gt;

&lt;p&gt;Changing a calibration interval isn't a single field update — it can invalidate dependent equipment. So when a calibration-critical field changes (manufacturer, model, measurement range, accuracy, reference standard...), a small rule engine flags the impact as &lt;strong&gt;HIGH&lt;/strong&gt;, counts the open calibration tasks affected, and writes the reasoning into the audit trail. Interval changes on a calibration task get the same treatment. It's a hard-coded table of critical fields and two careful checks — no AI, no magic — but it's what makes the app feel like it understands the domain instead of just storing dates.&lt;/p&gt;
&lt;h2&gt;
  
  
  MSIX Packaging: Three Traps, One Script
&lt;/h2&gt;

&lt;p&gt;Shipping to the Microsoft Store surfaced three gotchas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;makeappx&lt;/code&gt; demands &lt;code&gt;AppxManifest.xml&lt;/code&gt;&lt;/strong&gt; — not &lt;code&gt;Package.appxmanifest&lt;/code&gt; — and the schema &lt;em&gt;requires&lt;/em&gt; a &lt;code&gt;TargetDeviceFamily&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing fails with &lt;code&gt;0x8007000B&lt;/code&gt;&lt;/strong&gt; when the manifest &lt;code&gt;Publisher&lt;/code&gt; (&lt;code&gt;CN=RuffStack&lt;/code&gt;) doesn't match the signing certificate subject. Event log ID 150 told us exactly this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store licenses are real, local trials are fake.&lt;/strong&gt; We now read the actual &lt;code&gt;StoreContext&lt;/code&gt; license in packaged builds and fall back to a local 15-day trial in dev. One interface:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IStoreLicenseContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LicenseInfo&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetLicenseAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// null = not a Store build&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Everything is now one command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;powershell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CalibKeep.Packaging\build-msix.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sign&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Encrypted local storage in .NET is doable, but budget time for SQLCipher handle poisoning and backup/restore edge cases.&lt;/li&gt;
&lt;li&gt;If you need compliance, bake the audit trail into the &lt;em&gt;transaction boundary&lt;/em&gt;, not as a side effect.&lt;/li&gt;
&lt;li&gt;Test the packaging/licensing story early — it's the "works on my machine" trap with extra steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building an offline-first desktop tool with .NET 8 + Avalonia, I hope this saves you a couple of rabbit holes.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://ruffstack.dev/calibkeep" rel="noopener noreferrer" class="c-link"&gt;
            RuffStack | Software Developer | SaaS &amp;amp; AI Tools
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Independent developer shipping EU compliance SaaS and AI-powered desktop tools.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fruffstack.dev%2Fruff6.jpg" width="415" height="314"&gt;
          ruffstack.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://apps.microsoft.com/detail/9NPK1LRD76FL" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;apps.microsoft.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>dotnet</category>
      <category>avalonia</category>
      <category>csharp</category>
      <category>desktop</category>
    </item>
    <item>
      <title># How We Built a Real-Time ETS Price Tracker for CBAM Compliance</title>
      <dc:creator>r emrah gökkaya</dc:creator>
      <pubDate>Wed, 01 Jul 2026 18:26:32 +0000</pubDate>
      <link>https://dev.to/remrah/-how-we-built-a-real-time-ets-price-tracker-for-cbam-compliance-4mpa</link>
      <guid>https://dev.to/remrah/-how-we-built-a-real-time-ets-price-tracker-for-cbam-compliance-4mpa</guid>
      <description>&lt;p&gt;CBAM certificate costs are directly tied to EU ETS allowance prices — for the 2026 compliance year it's a quarterly average, from 2027 it switches to a weekly average of EU ETS auction closing prices. That sounds like a simple "call an API, store a number" problem. It isn't, and the reasons why taught me more about building reliable data pipelines than anything else in this project.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://cbamtrack.com" rel="noopener noreferrer"&gt;CBAMTrack&lt;/a&gt;, a CBAM compliance platform for EU importers and non-EU exporters, solo, on Next.js App Router + TypeScript + Prisma/PostgreSQL, hosted on Northflank behind Cloudflare. Here's how the ETS price tracker actually works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just call an API" doesn't hold up
&lt;/h2&gt;

&lt;p&gt;A compliance tool can't have an "unlucky day." If our price feed goes down and a customer generates a CBAM cost estimate off a stale or missing value, that's not a cosmetic bug — it's a number someone might actually budget against. So the design constraint from day one was: &lt;strong&gt;the system must never silently serve a wrong or missing price.&lt;/strong&gt; It either serves a verified price or it visibly tells you it couldn't.&lt;/p&gt;

&lt;p&gt;That single constraint is what turned this from a fetch-and-cache script into a fallback chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback chain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Primary source:   [YOUR PRIMARY EU ETS PRICE SOURCE]
2. Secondary source: [YOUR FALLBACK SOURCE, if primary fails or is stale]
3. Last verified value: cached in Postgres, used with an explicit
   "stale as of [date]" flag surfaced in the UI
4. Manual override: an internal admin path to enter a verified price
   by hand if both automated sources fail for more than N hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tier only activates if the one above it fails or returns data outside a sanity-check range (a &amp;gt;15% single-day swing gets flagged for review rather than trusted blindly — carbon markets are volatile, but not usually &lt;em&gt;that&lt;/em&gt; volatile).&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing prices the way CBAM actually needs them
&lt;/h2&gt;

&lt;p&gt;The tricky part isn't fetching a price — it's storing it in a shape that supports both compliance-year rules at once, since 2026 uses a quarterly average and 2027 onward uses a weekly average.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified schema&lt;/span&gt;
&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="nx"&gt;EtsPrice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt;          &lt;span class="nb"&gt;String&lt;/span&gt;   &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;id&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cuid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;        &lt;span class="nx"&gt;DateTime&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;unique&lt;/span&gt;
  &lt;span class="nx"&gt;priceEurPerTonne&lt;/span&gt;  &lt;span class="nx"&gt;Decimal&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;      &lt;span class="nb"&gt;String&lt;/span&gt;   &lt;span class="c1"&gt;// which tier of the fallback chain produced this&lt;/span&gt;
  &lt;span class="nx"&gt;isStale&lt;/span&gt;     &lt;span class="nb"&gt;Boolean&lt;/span&gt;  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;fetchedAt&lt;/span&gt;   &lt;span class="nx"&gt;DateTime&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storing raw daily prices — not pre-computed averages — turned out to be the right call. It meant when the 2026→2027 rule change landed, I didn't have to backfill anything. The averaging logic just reads a different window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Postgres, not Redis
&lt;/h2&gt;

&lt;p&gt;The obvious instinct for a "fetch a price, cache it, serve it fast" problem is Redis. I didn't use it, and I don't think I will.&lt;/p&gt;

&lt;p&gt;Here's the reasoning: the read pattern for ETS prices isn't "give me the latest value as fast as possible" — it's "give me the volume-weighted average over this specific window, where the window definition itself depends on which compliance year we're calculating for." That's a query with a &lt;code&gt;WHERE date BETWEEN&lt;/code&gt; and a &lt;code&gt;GROUP BY&lt;/code&gt;, not a key lookup. Redis is excellent at the thing I don't need (sub-millisecond single-key reads) and clumsy at the thing I do need (windowed aggregation over a time series with a variable boundary rule).&lt;/p&gt;

&lt;p&gt;Postgres gives me that aggregation for free, in SQL, against data that's already sitting next to every other table the calculation engine touches — no cross-service joins, no second system to keep in sync, no cache-invalidation bugs when a price gets corrected after the fact (which does happen; auction data occasionally gets revised after publication).&lt;/p&gt;

&lt;p&gt;There's also a boring but real reason: I'm a solo founder. Every extra piece of infrastructure is something &lt;em&gt;I&lt;/em&gt; get paged for at 2am. A Redis instance earns its keep when you have a genuine hot-path latency problem. A handful of daily price rows, queried a few hundred times a day, does not have that problem. Postgres with an index on &lt;code&gt;date&lt;/code&gt; handles it without me thinking about it again.&lt;/p&gt;

&lt;p&gt;If CBAMTrack ever needs sub-second price lookups at real scale, I'll revisit. I'd rather add that complexity when the data forces my hand than because a blog post told me Redis is what "real" caching looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  normalizePeriod() and displayPeriod()
&lt;/h2&gt;

&lt;p&gt;CBAM reporting periods don't map cleanly onto calendar quarters once you factor in the transition between the quarterly-average rule (2026) and weekly-average rule (2027+). I ended up writing two small helpers that get used everywhere in the reporting engine, not just the price tracker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// normalizePeriod: takes a raw date range and returns the&lt;/span&gt;
&lt;span class="c1"&gt;// canonical CBAM reporting period it belongs to, accounting&lt;/span&gt;
&lt;span class="c1"&gt;// for the 2026/2027 averaging-rule boundary&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizePeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CbamPeriod&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// displayPeriod: takes a normalized period and returns the&lt;/span&gt;
&lt;span class="c1"&gt;// human-readable label used across the UI and PDF reports&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;displayPeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CbamPeriod&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having a single source of truth for "what period does this date belong to, and what do we call it" saved me from a category of bug I hit early on: a report and its underlying price data disagreeing about which quarter they were describing because one code path used raw dates and another used the period label. Small helper, disproportionate bug reduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;If I were starting over, I'd build the sanity-check/anomaly-flagging logic &lt;em&gt;before&lt;/em&gt; the happy-path fetch-and-store logic, not after. I added it reactively once I saw what a bad data day actually looked like in staging — a source returning a cached value from three weeks earlier without any indication it was stale. In a compliance product, "silently wrong" is a worse failure mode than "loudly broken," and I'd design for that assumption from the start next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond CBAM
&lt;/h2&gt;

&lt;p&gt;If you're building anything that depends on an external price or regulatory feed — carbon markets, FX rates, commodity indices — the same three questions are worth asking before you write the fetch logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What happens the day your source is down?&lt;/li&gt;
&lt;li&gt;What happens the day your source returns a number that's &lt;em&gt;technically valid but wrong&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;Does your storage layer assume today's aggregation rule will still be true next year?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Getting those three answers right up front is cheaper than retrofitting them after a customer asks why their number changed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building CBAMTrack as a solo founder — EU CBAM compliance reporting for SME importers and Gulf/MENA exporters. Live ETS prices and the certificate cost calculator this pipeline feeds are at &lt;a href="https://cbamtrack.com/calculator" rel="noopener noreferrer"&gt;cbamtrack.com/calculator&lt;/a&gt;. If you're dealing with CBAM data pipelines yourself, I'd genuinely like to compare notes in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>nextjs</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
