<?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: Eyosias Negash</title>
    <description>The latest articles on DEV Community by Eyosias Negash (@eyosias_negash_8cbc103bb4).</description>
    <link>https://dev.to/eyosias_negash_8cbc103bb4</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%2F4213919%2F522e884c-a125-41ee-9f0f-634634762113.jpg</url>
      <title>DEV Community: Eyosias Negash</title>
      <link>https://dev.to/eyosias_negash_8cbc103bb4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eyosias_negash_8cbc103bb4"/>
    <language>en</language>
    <item>
      <title>Three services and nothing to store</title>
      <dc:creator>Eyosias Negash</dc:creator>
      <pubDate>Fri, 30 Oct 2026 12:18:44 +0000</pubDate>
      <link>https://dev.to/eyosias_negash_8cbc103bb4/three-services-and-nothing-to-store-58nk</link>
      <guid>https://dev.to/eyosias_negash_8cbc103bb4/three-services-and-nothing-to-store-58nk</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://eyosias.me/2026/10/25/system.html" rel="noopener noreferrer"&gt;eyosias.me&lt;/a&gt;. This is a chapter in the Ivyk8 series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last time: I cut a flagship-platform plan down to three stateless TypeScript services and five written decisions. This chapter is the result: what runs, how a request travels, and what keeps it safe.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The app reads an Android money manager’s backup from the user’s Google Drive and turns it into reports.&lt;/li&gt;
&lt;li&gt;Three services: a static React dashboard, a public back end (&lt;code&gt;drive-sync-service&lt;/code&gt;) and an internal calculator (&lt;code&gt;analytics-service&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;A report request is a pure function of &lt;code&gt;(backup, timezone, rate overrides)&lt;/code&gt;. Change the timezone and the same request runs again with new parameters. Nothing is stored.&lt;/li&gt;
&lt;li&gt;The security model is mostly subtraction: tokens never reach the browser, the calculator is unreachable from outside, and every container runs unprivileged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Browser ──► Ingress (Traefik)
              ├── /      ──► web-dashboard        React app, served by nginx, static only
              └── /api   ──► drive-sync-service   public API (a BFF)
                               │  Google sign-in, Drive read-only
                               │  encrypted cookie session
                               ▼
                             analytics-service    reachable inside the cluster only
                               │  validate → index → pure report functions
                               ▼
                             ECB daily exchange rates (+ fallback)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A BFF (backend for frontend) is a server that exists to serve one front end. Here it owns everything that touches Google, so the browser never does.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Owns&lt;/th&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Public?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web-dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the UI&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;yes, &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drive-sync-service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sign-in, token refresh, folder choice, listing backups, choosing which bytes to analyse&lt;/td&gt;
&lt;td&gt;an encrypted cookie&lt;/td&gt;
&lt;td&gt;yes, &lt;code&gt;/api&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;analytics-service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;validation, business rules, exchange rates, every number on screen&lt;/td&gt;
&lt;td&gt;an in-memory cache&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A shared &lt;code&gt;packages/contracts&lt;/code&gt; package holds the request and response types, so a change to a report’s shape fails the build in both services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening a report, one hop at a time
&lt;/h2&gt;

&lt;p&gt;Say you open the balances page.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The browser&lt;/strong&gt; calls &lt;code&gt;GET /api/backups/:fileId/balances&lt;/code&gt; with a timezone and optional rate overrides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;drive-sync decrypts the session cookie&lt;/strong&gt; and refreshes the Google access token if it has expired. If the refresh token is dead it answers &lt;code&gt;401 REAUTH_REQUIRED&lt;/code&gt; and the UI shows a “Reconnect” link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It checks the file is in the folder you chose.&lt;/strong&gt; A file ID from anywhere else gets a 404. That check is the line between “your backup folder” and “any file this token could see”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It builds the dataset key:&lt;/strong&gt; &lt;code&gt;sha256(user id, file id, Drive md5)&lt;/code&gt;. Two users never share a cache entry, and an edited file gets a new key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It asks analytics for the report.&lt;/strong&gt; If analytics answers 404, the dataset isn’t loaded: first open, evicted, or the pod restarted. drive-sync then downloads the backup (with a size limit enforced up front and while streaming), &lt;code&gt;PUT&lt;/code&gt;s it to analytics, and retries once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics computes the report&lt;/strong&gt; from the dataset, timezone and overrides, and returns the numbers with the rates it used and any warnings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 404-then-upload dance is the design in miniature. Analytics holds parsed backups in an in-memory LRU cache, and the cache is allowed to be wrong. When it’s empty, it gets refilled. That is why a pod can be killed at any time without anyone noticing, and why there is no database to back up.&lt;/p&gt;

&lt;p&gt;It is also why “change the timezone” or “override this exchange rate” needs no endpoint of its own. It is the same request with different query parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: mostly subtraction
&lt;/h2&gt;

&lt;p&gt;Most of the model is things the system doesn’t have.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tokens never reach the browser.&lt;/strong&gt; The session is an encrypted cookie that only drive-sync can open, and Google access is limited to the &lt;code&gt;drive.readonly&lt;/code&gt; scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only named reports pass through.&lt;/strong&gt; drive-sync forwards a report name to analytics only if it is in a fixed allowlist (&lt;code&gt;REPORTS&lt;/code&gt;). A new report is invisible to the outside world until someone adds it there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default deny.&lt;/strong&gt; Every namespace starts from a deny-all NetworkPolicy, and each allowed path (ingress to dashboard, ingress to drive-sync, drive-sync to analytics) is written explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restricted Pod Security.&lt;/strong&gt; The namespace enforces the &lt;code&gt;restricted&lt;/code&gt; profile: no root, no privilege escalation, dropped capabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small images.&lt;/strong&gt; The Node services run on distroless images (no shell, no package manager, non-root). The dashboard uses &lt;code&gt;nginx-unprivileged&lt;/code&gt;. The root filesystems are read-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A smoke test proves the claim.&lt;/strong&gt; The deploy check requests analytics from outside the cluster and expects it to fail. A policy nobody tests is a hope.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The calculator
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;analytics-service&lt;/code&gt; is where the rules live, so it is where bugs will live too. Its shape keeps them findable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate once.&lt;/strong&gt; A backup is checked against a schema with ajv on upload. Everything after that trusts the shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports are pure functions.&lt;/strong&gt; Input is the dataset, a timezone and rate overrides. Output is numbers. There is nothing to mock and nothing to reset between tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time is bucketed by the user’s timezone,&lt;/strong&gt; with weeks starting on Monday. A transaction near midnight lands in the day the user would call it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rates degrade gracefully.&lt;/strong&gt; ECB rates are cached and served stale if a refresh fails. A user override wins over any fetched rate. A currency with no rate at all is reported as “unavailable”, not guessed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Problems become warnings,&lt;/strong&gt; not failures. A report with one unconvertible transaction still renders, and says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it could do at launch
&lt;/h2&gt;

&lt;p&gt;Sign-in, a folder picker, a backup list, a transactions table, balances and net worth, income and expense, and the Trends charts. Later chapters add budgets, a projection, tags and custom reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it gets onto a cluster
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;make up&lt;/code&gt; creates a local k3d cluster (Kubernetes in Docker) and deploys everything with kustomize. Overlays for local, CI and a multi-node cluster differ in small ways, such as which &lt;code&gt;ingressClassName&lt;/code&gt; the Ingress objects use. There is also a CI pipeline that tests, scans and signs images. At this point it is written but has not run once.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small question worth recording
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubectl port-forward svc/traefik 30080:80&lt;/code&gt; reaches the container’s port 8000, not 80. The Service’s port 80 maps to a named &lt;code&gt;targetPort&lt;/code&gt;, and the Traefik chart binds a non-root port because it runs unprivileged. The “80” is the Service’s, not the container’s.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If a result is a function of its inputs, don’t store it.&lt;/strong&gt; Recomputing from &lt;code&gt;(backup, tz, rates)&lt;/code&gt; removed the database, the migrations and every “which copy is right” question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the cache be wrong.&lt;/strong&gt; A cache that can be empty at any time, with a defined refill path, turns pod restarts into non-events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove the negative.&lt;/strong&gt; “Analytics is internal” is only true if a test tries from outside and fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist what a public server forwards.&lt;/strong&gt; An allowlist fails closed; a blocklist fails whenever you add something.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;The next chapter moves this off my laptop’s local cluster onto three machines at home. It did not go the way the diagram suggests.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The story continues as the work does.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://github.com/eyosias-k-negash/ivk8" rel="noopener noreferrer"&gt;github.com/eyosias-k-negash/ivk8&lt;/a&gt;. First commits: &lt;a href="https://github.com/eyosias-k-negash/ivk8/commit/e1d849a" rel="noopener noreferrer"&gt;&lt;code&gt;e1d849a&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/eyosias-k-negash/ivk8/commit/65bac77" rel="noopener noreferrer"&gt;&lt;code&gt;65bac77&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Previous: &lt;a href="https://eyosias.me/2026/10/23/from-a-phone-app-to-a-cluster.html" rel="noopener noreferrer"&gt;Chapter 1, From a phone app to a cluster&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>typescript</category>
    </item>
    <item>
      <title>From a phone app to a cluster</title>
      <dc:creator>Eyosias Negash</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:21:27 +0000</pubDate>
      <link>https://dev.to/eyosias_negash_8cbc103bb4/from-a-phone-app-to-a-cluster-4j8i</link>
      <guid>https://dev.to/eyosias_negash_8cbc103bb4/from-a-phone-app-to-a-cluster-4j8i</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://eyosias.me/2026/10/23/from-a-phone-app-to-a-cluster.html" rel="noopener noreferrer"&gt;eyosias.me&lt;/a&gt;. This is a chapter in the Ivyk8 series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ivyk8&lt;/strong&gt; is Ivy + k8s: a read-only desktop dashboard for the backups my phone app writes to Google Drive.&lt;/li&gt;
&lt;li&gt;The first blueprint was a flagship platform: Postgres with replicas, Redis, a service mesh, GitOps, canaries. A critique of it said it was about 18 months of work for a team.&lt;/li&gt;
&lt;li&gt;On Oct 1 I cut it to three stateless TypeScript services, Kubernetes for delivery, and five written decisions (ADRs).&lt;/li&gt;
&lt;li&gt;This is part 1 of a series. Parts about each feature and each outage follow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it starts: a phone app I couldn’t leave alone
&lt;/h2&gt;

&lt;p&gt;Ivy Wallet is an open-source money manager for Android. Its owner archived it on Nov 5, 2024. I wanted to keep using it, so I forked it as &lt;a href="https://github.com/eyosias-k-negash/ivy-wallet-plus" rel="noopener noreferrer"&gt;Ivy Wallet+&lt;/a&gt; and added what I was missing.&lt;/p&gt;

&lt;p&gt;The additions, in the order I made them in late August:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Photo and audio attachments&lt;/strong&gt; on transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction location:&lt;/strong&gt; coordinates and a place name, a Room (Android’s SQLite layer) migration, and a button that opens the place in a maps app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balance at day:&lt;/strong&gt; tap a date in the history and see each account’s balance on that day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMS auto-fill.&lt;/strong&gt; The app listens for bank SMS on a chosen SIM, parses the amount, balance, date and description with per-bank regex rules, and raises a notification that opens a pre-filled transaction. Rules import as JSON. I wrote rule sets for four banks and a mobile-money wallet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A wrapped reports filter&lt;/strong&gt; in place of horizontal scrolling, and a rebrand to Ivy Wallet+ with its own package name and logo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is this blog’s subject. It matters because of what it did to the data. The phone now captured more than before, and a phone screen is a poor place to study years of transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Ivy Wallet+ already writes its backups to Google Drive. That file is the whole interface between the phone and anything else. So the idea was small: read the backup, show it on a desktop screen with charts the app can’t draw, and see trends, averages, and projections of expenses and incomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first blueprint was too big
&lt;/h2&gt;

&lt;p&gt;I started where an ambitious person starts, with a prompt for a “top 1% flagship project”. The first blueprint had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Kotlin sync client and a Go sync API;&lt;/li&gt;
&lt;li&gt;a currency service and a report worker;&lt;/li&gt;
&lt;li&gt;Postgres with read replicas, and Redis as both cache and queue;&lt;/li&gt;
&lt;li&gt;Istio, Argo CD, Argo Rollouts, KEDA, SOPS and a full observability stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I asked Claude Code to critique it before building anything. This is where the pair worked well: the critique was blunt and I kept it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Roughly 18 months of work for a team.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also found specific faults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis as cache and queue is a contradiction.&lt;/strong&gt; A cache wants &lt;code&gt;allkeys-lru&lt;/code&gt; (evict anything under memory pressure). A queue needs &lt;code&gt;noeviction&lt;/code&gt; (never lose a job). One instance can’t be both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read replicas break read-your-writes.&lt;/strong&gt; A sync client that uploads and then reads back can see old data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hand-rolled Postgres StatefulSet “signals junior K8s judgment”.&lt;/strong&gt; Use an operator, or don’t run a database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No identity layer.&lt;/strong&gt; Nothing said who the user was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Cosign signing without admission verification is security theater.”&lt;/strong&gt; Signed images mean little if the cluster never checks the signatures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last point is worth a dry aside. It’s still partly true of this project today: the release workflow signs images, and nothing in the cluster verifies them yet. The critique named the gap before I built it. Closing it is on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cut
&lt;/h2&gt;

&lt;p&gt;I cut in two stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MVP prompt&lt;/strong&gt; kept core Kubernetes and a basic CI pipeline with static scanning. GitOps, canaries, observability, KEDA and signing went on hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The final cut, Oct 1,&lt;/strong&gt; went further. I wrote it down as five architecture decision records (ADRs), short files that say what was decided and what it replaced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ADR&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0001&lt;/td&gt;
&lt;td&gt;Three stateless TypeScript services: &lt;code&gt;web-dashboard&lt;/code&gt;, &lt;code&gt;drive-sync-service&lt;/code&gt; (the public back end for the dashboard) and &lt;code&gt;analytics-service&lt;/code&gt; (internal). No Postgres, no Redis. It supersedes the earlier scaffold.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0002&lt;/td&gt;
&lt;td&gt;Sessions live in an encrypted cookie, so the services hold no session store.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0003&lt;/td&gt;
&lt;td&gt;Google Drive access is limited to the &lt;code&gt;drive.readonly&lt;/code&gt; scope. The app can’t change a backup even if it were compromised.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0004&lt;/td&gt;
&lt;td&gt;Kubernetes is the only delivery path: k3d (Kubernetes in Docker) locally and in CI, no docker-compose.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0005&lt;/td&gt;
&lt;td&gt;Exchange rates come from the ECB, with a fallback source for currencies the ECB doesn’t publish.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reasoning behind 0001 is the part to keep. The analytics are computed in memory from one file. Timezone and rate overrides are request parameters. Nothing needs to persist between requests, so nothing gets a database. On a cache miss, drive-sync re-uploads the backup to analytics. That is a feature, not a shortcut: stateless services can be killed, replicated and rolled without ceremony. I did not know yet how much I would lean on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;One sentence in the repo says what I was after: &lt;strong&gt;built, secured, tested and delivered the way a production product would be.&lt;/strong&gt; The product is a personal dashboard for one user. The practice around it is the point: tests that gate merges, image scanning, signed releases, rollouts on a real cluster.&lt;/p&gt;

&lt;p&gt;That decides how the series is told. Features are chapters. The cluster misbehaving is its own kind of story, and those get shorter posts of their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it taught me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask for the critique before the code.&lt;/strong&gt; An hour of being told the plan is theater was cheaper than 18 months of finding out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statelessness is a decision you pay for once.&lt;/strong&gt; Dropping the database removed whole classes of work: migrations, backups, replicas, failover.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the decision down while it is fresh.&lt;/strong&gt; The ADRs are why I can still explain, weeks later, why there is no Redis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;The next parts cover the dashboard’s first real features and the move from a local k3d cluster onto three machines at home. That second move did not go smoothly, and it is the source of the stranger posts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The story continues as the work does.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Source: &lt;a href="https://github.com/eyosias-k-negash/ivk8" rel="noopener noreferrer"&gt;github.com/eyosias-k-negash/ivk8&lt;/a&gt;. Decisions: &lt;code&gt;docs/adr/0001&lt;/code&gt;–&lt;code&gt;0005&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;Next: &lt;a href="https://eyosias.me/2026/10/25/system.html" rel="noopener noreferrer"&gt;Chapter 2, Three services and nothing to store&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
