<?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: Ihor Olkhovatyi</title>
    <description>The latest articles on DEV Community by Ihor Olkhovatyi (@sssselfmade).</description>
    <link>https://dev.to/sssselfmade</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%2F4042775%2Fec4d19b7-9374-415c-a354-007c89334f9e.png</url>
      <title>DEV Community: Ihor Olkhovatyi</title>
      <link>https://dev.to/sssselfmade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sssselfmade"/>
    <language>en</language>
    <item>
      <title>Your budgeting app is lying to you about last month</title>
      <dc:creator>Ihor Olkhovatyi</dc:creator>
      <pubDate>Wed, 22 Jul 2026 23:48:26 +0000</pubDate>
      <link>https://dev.to/sssselfmade/your-budgeting-app-is-lying-to-you-about-last-month-3kn8</link>
      <guid>https://dev.to/sssselfmade/your-budgeting-app-is-lying-to-you-about-last-month-3kn8</guid>
      <description>&lt;p&gt;&lt;em&gt;I build a multi-currency personal-finance app. This is one of the&lt;br&gt;
first bugs I had to design my way out of — and it's one almost every&lt;br&gt;
multi-currency app I've tried still has.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;You spent 200 zł on groceries in March. Your base currency is euros.&lt;br&gt;
In March the app showed that as, say, €46. You open the app in July and&lt;br&gt;
that same March transaction now reads €43 — even though nothing about&lt;br&gt;
March changed. Multiply that across every foreign transaction and your&lt;br&gt;
"total spent in March" quietly drifts every single day.&lt;/p&gt;

&lt;p&gt;That's not a rounding quirk. It means your financial history is being&lt;br&gt;
&lt;strong&gt;rewritten with today's exchange rate&lt;/strong&gt;, and you can't trust any&lt;br&gt;
number older than right now.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;The naive multi-currency data model looks reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;transaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PLN"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-14"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, to show a total in the user's base currency, you convert on read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;liveRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;liveRate&lt;/code&gt; returns &lt;em&gt;today's&lt;/em&gt; rate. So the March transaction is re-priced&lt;br&gt;
with the July rate every time it's rendered. Balances update live —&lt;br&gt;
which is correct for what you hold &lt;em&gt;right now&lt;/em&gt; — but the same logic&lt;br&gt;
leaks onto &lt;strong&gt;historical&lt;/strong&gt; transactions, where it's just wrong. A cost&lt;br&gt;
you actually incurred at March's rate should read at March's rate&lt;br&gt;
forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Convert live for balances. Snapshot for history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A transaction is an event that happened at a moment in time. The&lt;br&gt;
exchange rate that applied is a &lt;strong&gt;fact about that moment&lt;/strong&gt;, not a live&lt;br&gt;
value. So capture it once, at ingest, and never recompute it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;transaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s2"&gt;"PLN"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-14"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;rate_to_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="l"&gt;0.2312&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;// EUR per PLN on the value date, frozen&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;amount_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mf"&gt;46.24&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;// optional: precomputed, for fast sums&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now historical totals are a plain &lt;code&gt;SUM(amount_base)&lt;/code&gt; — deterministic,&lt;br&gt;
and March stays €46.24 in July, next year, forever. Only current&lt;br&gt;
account &lt;strong&gt;balances&lt;/strong&gt; get the live rate, because those really are&lt;br&gt;
"what is this worth right now."&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that bite
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Which date's rate?&lt;/strong&gt; Use the transaction's &lt;em&gt;value date&lt;/em&gt; (when the&lt;br&gt;
money actually moved), not the booking/import date. Banks can book days&lt;br&gt;
later; pricing at import time reintroduces drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rates aren't published every day.&lt;/strong&gt; The ECB (I use their Frankfurter&lt;br&gt;
feed) publishes on TARGET business days — no weekends, no holidays. A&lt;br&gt;
Saturday transaction has no same-day rate. Carry forward the last&lt;br&gt;
published rate rather than skipping or interpolating; that's what the&lt;br&gt;
banks do too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some currencies aren't in the feed.&lt;/strong&gt; The ECB doesn't publish UAH,&lt;br&gt;
for example. You need a secondary source for the long tail, normalized&lt;br&gt;
into the same base so your snapshot is consistent regardless of which&lt;br&gt;
feed a given currency came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store the rate, not just the converted amount.&lt;/strong&gt; If you only store&lt;br&gt;
&lt;code&gt;amount_base&lt;/code&gt;, you can never let the user change their base currency&lt;br&gt;
without lying again. Keep the original &lt;code&gt;amount&lt;/code&gt; + &lt;code&gt;currency&lt;/code&gt; + &lt;code&gt;date&lt;/code&gt;,&lt;br&gt;
and the snapshotted rate. To switch base currency you re-derive from&lt;br&gt;
the original at each transaction's &lt;em&gt;historical&lt;/em&gt; rate — which means you&lt;br&gt;
also need historical rates on hand, not just today's. (Storing the&lt;br&gt;
rate against a single pivot currency like EUR and deriving cross-rates&lt;br&gt;
keeps this cheap.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency.&lt;/strong&gt; Banks re-send transactions. Snapshot the rate keyed to&lt;br&gt;
the transaction's identity + value date so a re-sync never re-prices an&lt;br&gt;
existing row.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you're building anything that sums money across currencies over&lt;br&gt;
time — budgeting, accounting, portfolio, expenses — decide explicitly,&lt;br&gt;
per number, whether it's a &lt;strong&gt;live&lt;/strong&gt; value or a &lt;strong&gt;historical&lt;/strong&gt; one.&lt;br&gt;
Balances are live. Everything that already happened is history, and&lt;br&gt;
history needs its rate frozen at the moment it occurred. Get this wrong&lt;br&gt;
and every chart you draw is quietly fiction.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://everypennyapp.com" rel="noopener noreferrer"&gt;EveryPenny&lt;/a&gt;, a calm&lt;br&gt;
multi-currency money tracker for people whose money lives across more&lt;br&gt;
than one country — it connects EEA banks and Monobank in Ukraine and&lt;br&gt;
shows one honest total. It's what pushed me to get this right. Happy to&lt;br&gt;
go deeper on the rate-sourcing side in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>database</category>
      <category>fintech</category>
    </item>
    <item>
      <title>My app's anti-theft feature locked every user out</title>
      <dc:creator>Ihor Olkhovatyi</dc:creator>
      <pubDate>Wed, 22 Jul 2026 23:48:16 +0000</pubDate>
      <link>https://dev.to/sssselfmade/my-apps-anti-theft-feature-locked-every-user-out-48mf</link>
      <guid>https://dev.to/sssselfmade/my-apps-anti-theft-feature-locked-every-user-out-48mf</guid>
      <description>&lt;p&gt;&lt;em&gt;A real incident from the finance app I build solo. The security design&lt;br&gt;
was textbook. That's exactly why it bit me.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The textbook setup
&lt;/h2&gt;

&lt;p&gt;Short-lived access token, long-lived &lt;strong&gt;rotating&lt;/strong&gt; refresh token. Every&lt;br&gt;
time the client refreshes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It presents its refresh token.&lt;/li&gt;
&lt;li&gt;The server verifies it, &lt;strong&gt;revokes it&lt;/strong&gt;, and issues a brand-new
access + refresh pair.&lt;/li&gt;
&lt;li&gt;The old refresh token is now dead — single use.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the part everyone recommends (OWASP calls it refresh-token reuse&lt;br&gt;
detection): if a client ever presents a refresh token that's &lt;strong&gt;already&lt;br&gt;
been revoked&lt;/strong&gt;, treat it as theft. An attacker who stole a token and&lt;br&gt;
replayed it would trip this. The safe response is scorched earth —&lt;br&gt;
&lt;strong&gt;revoke every session for that user&lt;/strong&gt; so both the attacker and the&lt;br&gt;
victim get kicked and must sign in fresh.&lt;/p&gt;

&lt;p&gt;This is correct. It catches a real attack. Ship it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The incident
&lt;/h2&gt;

&lt;p&gt;One evening my own app showed me a auth error over a €0.00 balance and&lt;br&gt;
an "add your first account" screen — as if I'd never signed in. The&lt;br&gt;
data was all there on the backend. Only the login was dead.&lt;/p&gt;

&lt;p&gt;The audit log told the story. All day: healthy refreshes every ~15&lt;br&gt;
minutes. Then, one refresh, the server saw an &lt;strong&gt;already-revoked&lt;/strong&gt; token,&lt;br&gt;
declared theft, and revoked all sessions. The "attacker"?&lt;/p&gt;

&lt;p&gt;Every session in the chain carried &lt;strong&gt;the same device id&lt;/strong&gt;. It was my&lt;br&gt;
phone. My phone was the thief.&lt;/p&gt;
&lt;h2&gt;
  
  
  How a device robs itself
&lt;/h2&gt;

&lt;p&gt;A same-device replay of its own just-rotated token has completely&lt;br&gt;
benign causes, and they're all common:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lost response.&lt;/strong&gt; The server rotated the token and committed, but the
HTTP response never made it back (dropped connection, backgrounded
app, dead tunnel). The client still holds the &lt;em&gt;old&lt;/em&gt; token and retries
it. To the server: a revoked token, replayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failed local write.&lt;/strong&gt; The new token pair came back, but writing it
to the keychain failed (or raced with app suspension). Next launch,
the client presents the previous token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two processes racing.&lt;/strong&gt; My app &lt;em&gt;and&lt;/em&gt; its widget extension both hit a
401 and both refresh with the same token. One wins and rotates it; the
other arrives a beat later with what is now a revoked token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are theft. But "revoked token replayed" looked identical&lt;br&gt;
to theft, so the nuke fired and I logged myself out everywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  The insight
&lt;/h2&gt;

&lt;p&gt;Here's the thing I missed: for a &lt;strong&gt;same-device&lt;/strong&gt; replay, the revoke-all&lt;br&gt;
adds almost no security.&lt;/p&gt;

&lt;p&gt;Reuse-detection defends against an attacker who exfiltrated a refresh&lt;br&gt;
token and replays it &lt;strong&gt;from somewhere else&lt;/strong&gt;. That "somewhere else" is&lt;br&gt;
the signal. An attacker replaying from the &lt;em&gt;same device&lt;/em&gt;, with the same&lt;br&gt;
bound device id, already had full access to the device's storage — the&lt;br&gt;
nuke doesn't save you there; they could just present the live token.&lt;/p&gt;

&lt;p&gt;So the theft signal isn't "a revoked token was replayed." It's "a&lt;br&gt;
revoked token was replayed &lt;strong&gt;from a different device than the one the&lt;br&gt;
session is bound to&lt;/strong&gt;."&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Bind sessions to a device id, then scope the scorched-earth response to&lt;br&gt;
the cases that are actually suspicious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sameDeviceBenignReplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;UNBOUND&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;presentedDeviceId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokedReason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rotated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sameDeviceBenignReplay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;revokeAllSessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;theft_detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RefreshError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token_replayed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 401 either way&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same device, replaying its own &lt;em&gt;rotated&lt;/em&gt; token → plain 401.&lt;/strong&gt; The
client just re-authenticates its refresh normally or picks up the
winner's new token. No nuke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different device, or a non-rotation revocation (sign-out), or an
unbound session we can't attribute → keep the full revoke-all.&lt;/strong&gt; Real
theft signals still get scorched earth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three supporting fixes made it robust:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A cross-process refresh coordinator.&lt;/strong&gt; The app and widget now go
through a single-flight mutex (a file lock in a shared container +
a notification), so only one refresh is ever in flight per device.
The loser waits for the winner's token instead of racing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harden the token write.&lt;/strong&gt; Check the keychain write status, retry
once, and log loudly on failure — a silently failed write was one
of the replay causes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never turn a transient failure into a logout.&lt;/strong&gt; A dropped network
or a 5xx on the refresh endpoint must be retryable, not "your
session is dead." Only a genuine 401/403 from the auth server means
sign out.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Rotating refresh tokens + reuse detection is still the right design.
Keep it.&lt;/li&gt;
&lt;li&gt;But &lt;strong&gt;"reused token" is not the same as "stolen token."&lt;/strong&gt; The stolen
signal is &lt;em&gt;reuse from an unexpected device.&lt;/em&gt; Bind sessions to a
device and let that distinction gate your most destructive response.&lt;/li&gt;
&lt;li&gt;Make client refresh &lt;strong&gt;single-flight across every process&lt;/strong&gt; that shares
the credentials, or your own app becomes the attacker.&lt;/li&gt;
&lt;li&gt;Distinguish transient failures from auth rejections everywhere in the
refresh path. Logging users out on a network blip is its own outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The uncomfortable lesson: a security control that's too eager is&lt;br&gt;
indistinguishable from a bug to the person it locks out. Scope the&lt;br&gt;
blast radius to the actual threat.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://everypennyapp.com" rel="noopener noreferrer"&gt;EveryPenny&lt;/a&gt;, a private,&lt;br&gt;
multi-currency money tracker for iPhone. This one shipped a fix the&lt;br&gt;
same day — solo means the postmortem and the patch are the same&lt;br&gt;
afternoon. Ask me anything about the auth model in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>node</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
