<?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>Forem: MD RASHEDUL ISLAM</title>
    <description>The latest articles on Forem by MD RASHEDUL ISLAM (@rhsumon).</description>
    <link>https://forem.com/rhsumon</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3947049%2F5a47d575-d4af-4722-b1cf-3846bc38a9cf.jpg</url>
      <title>Forem: MD RASHEDUL ISLAM</title>
      <link>https://forem.com/rhsumon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rhsumon"/>
    <language>en</language>
    <item>
      <title>We almost spent £180/month on Redis. We spent £40 instead.</title>
      <dc:creator>MD RASHEDUL ISLAM</dc:creator>
      <pubDate>Sat, 23 May 2026 23:52:41 +0000</pubDate>
      <link>https://forem.com/rhsumon/we-almost-spent-ps180month-on-redis-we-spent-ps40-instead-3ebm</link>
      <guid>https://forem.com/rhsumon/we-almost-spent-ps180month-on-redis-we-spent-ps40-instead-3ebm</guid>
      <description>&lt;p&gt;&lt;em&gt;by Rashed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We had a number in our heads: managed Redis, smallest HA tier, maybe $70 a month. We needed a Redis-class store for coordination primitives — JWT denylist, rate-limit counters, idempotency keys, distributed locks — and it had to survive a node dying, because some of those primitives sit on the money path. So we went to provision it.&lt;/p&gt;

&lt;p&gt;The console had a different number. Memorystore for Redis Standard — the tier that actually gives you a high-availability replica — starts at 5GB. Not 1GB. The smallest HA config you can buy is $234/mo. We needed about 1.25GB. We were looking at paying 4.5× for capacity we'd never touch, purely to get the replica.&lt;/p&gt;

&lt;p&gt;That gap between the number in our heads and the number on the screen is the whole story. We ended up on Memorystore for Valkey at $51/mo, with HA, and the migration cost us zero lines of code. Here's how we got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the store is actually for
&lt;/h2&gt;

&lt;p&gt;None of this matters unless the store earns its keep, so: what runs on it.&lt;/p&gt;

&lt;p&gt;Four things, all of them coordination rather than data-of-record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency keys.&lt;/strong&gt; Money-path endpoints (orders, payments, refunds, captures) dedupe retries against keys held here. If TanStack Query retries a payment POST on a network blip, the second request finds the key and replays the first response instead of charging twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT denylist.&lt;/strong&gt; Revoked tokens get checked against this on the way in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting.&lt;/strong&gt; Per-business counters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locks.&lt;/strong&gt; Short-lived coordination for workers that mustn't double-act.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three of those four are either security or money. That's why "no HA" was never really on the table for go-live — if the store vanishes mid-incident, the idempotency guarantee vanishes with it, and the failure mode is a double charge, not a slow page. We do fail open on lookups to stay available, bounded by the access-token lifetime, but the store being &lt;em&gt;up&lt;/em&gt; is the thing we're paying for.&lt;/p&gt;

&lt;p&gt;So we needed HA. That's the constraint that made the pricing matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number we had wrong
&lt;/h2&gt;

&lt;p&gt;Here's the thing worth saying plainly, because it's the actual lesson: we quoted the price from memory and the memory was wrong.&lt;/p&gt;

&lt;p&gt;We'd assumed Memorystore for Redis "Standard 1GB" was a thing that existed at roughly $70/mo. It isn't. On GCP, the Redis &lt;strong&gt;Basic&lt;/strong&gt; tier gives you 1GB but no replica — no HA at all. The moment you want a standby replica, you're on &lt;strong&gt;Standard&lt;/strong&gt;, and Standard has a 5GB floor. There is no 1GB HA Redis. The cheapest HA Redis you can provision is 5GB at $234/mo.&lt;/p&gt;

&lt;p&gt;We only found this by opening the console for our actual region (europe-west2) and trying to build the thing. Every back-of-envelope number we'd written in the planning doc was wrong, because we'd been pricing a configuration that doesn't exist.&lt;/p&gt;

&lt;p&gt;These are the numbers we actually saw, verified in the console on the day we provisioned:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;HA&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Redis Basic, 1GB&lt;/td&gt;
&lt;td&gt;$39 (£30)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Cheapest, no replica at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis Standard, 5GB (min)&lt;/td&gt;
&lt;td&gt;$234 (£180)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;4.5× our actual need&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Valkey, pico + 1 replica&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$51 (£40)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What we shipped for go-live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valkey, pico, 0 replicas&lt;/td&gt;
&lt;td&gt;~$26 (£20)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;What we run in the test phase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shape of the table is the argument. Redis Basic is cheap but gives us nothing we can launch payments on. Redis Standard gives us HA but bundles it with 4GB of RAM we will never address. Valkey gives us HA at a size that matches the workload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpc78pqkpvs98dvcugbz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpc78pqkpvs98dvcugbz8.png" alt=" " width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0of4wfr0l9zjn7e8few.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0of4wfr0l9zjn7e8few.png" alt=" " width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Valkey, specifically
&lt;/h2&gt;

&lt;p&gt;Valkey is the open-source fork of Redis that the Linux Foundation picked up after Redis changed its license in 2024. On GCP it shows up as Memorystore for Valkey, and the reason it was an easy call comes down to four things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The migration was free.&lt;/strong&gt; Valkey is wire-compatible with Redis 7.2. Our client is &lt;code&gt;ioredis&lt;/code&gt;, and &lt;code&gt;ioredis&lt;/code&gt; does not know or care whether it's talking to Redis or Valkey — same protocol, same commands, same connection code. We changed the host it points at. That was the entire code change. There's a version of this post where switching datastores is a two-week migration; this was not that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The license is actually open.&lt;/strong&gt; Valkey is BSD-3-licensed. Redis moved to a source-available license (RSALv2 / SSPLv1). For us this is a portability question, not an ideology one: if we ever leave GCP, an open-source-licensed store is one fewer thing tying us to a vendor's managed offering. It costs nothing today and buys optionality later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The networking is the modern path.&lt;/strong&gt; Memorystore for Valkey connects over Private Service Connect into the platform VPC, rather than the older VPC-peering model. PSC doesn't consume an IP range from our network and gives finer-grained access control. It's a one-time service-connection setup, about fifteen minutes, documented in our runbook. We'd rather be on the path Google is investing in than the one they're winding down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HA at a sane size.&lt;/strong&gt; This is the one that started the whole thing. Valkey lets us provision a small instance (a ~1.25GB pico) and &lt;em&gt;separately&lt;/em&gt; choose to add a replica. The HA decision is decoupled from the capacity decision. With Redis Standard those two are welded together at 5GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pre-launch trick
&lt;/h2&gt;

&lt;p&gt;One detail that saved us money without costing us anything: the replica is a toggle, so we don't pay for it before we need it.&lt;/p&gt;

&lt;p&gt;In the test phase, where there are no real payments and a node dying means a developer reruns something, we run Valkey with &lt;strong&gt;zero replicas&lt;/strong&gt; at about $26/mo. The day before we start processing live payments, we bump it to &lt;strong&gt;one replica&lt;/strong&gt; and it becomes $51/mo HA. Same instance, same data model, same connection string — one setting.&lt;/p&gt;

&lt;p&gt;We're not paying for a standby to protect test traffic that doesn't need protecting. The HA cost lands exactly when the thing it protects goes live, and not a month earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we picked, what we rejected, why
&lt;/h2&gt;

&lt;p&gt;We picked &lt;strong&gt;Memorystore for Valkey, pico size, one replica, europe-west2, over Private Service Connect&lt;/strong&gt; — $51/mo, HA, wire-compatible with our existing Redis client.&lt;/p&gt;

&lt;p&gt;We rejected &lt;strong&gt;Redis Basic 1GB ($39/mo)&lt;/strong&gt;. It's the cheapest line on the table and it was never viable, because it has no replica. Saving $12/mo to run our idempotency and denylist on a single node with no failover is a false economy the first time that node restarts during a payment.&lt;/p&gt;

&lt;p&gt;We rejected &lt;strong&gt;Redis Standard 5GB ($234/mo)&lt;/strong&gt;. This is the option we'd have sleepwalked into if we hadn't opened the console. It's a perfectly good product; it's just sized for a workload four and a half times ours, and you can't buy it smaller. Paying $183/mo more than Valkey for 4GB of RAM we'll never address is the definition of overprovisioning.&lt;/p&gt;

&lt;p&gt;What we gave up by choosing Valkey: Redis is the older, more battle-tested managed product with a deeper operational track record on GCP. Valkey-on-Memorystore is newer. We're betting that wire-compatibility plus an open license plus a managed SLA covers the maturity gap for a coordination store — and that bet is much easier to make when the thing speaks the exact same protocol, so reverting would also be a one-line change.&lt;/p&gt;

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

&lt;p&gt;The lesson isn't "use Valkey." The lesson is the thing that nearly cost us $183 a month: &lt;strong&gt;don't quote cloud pricing from memory — open the console for your actual region and try to build the actual thing.&lt;/strong&gt; Our planning number was off by a factor that mattered, not because we were careless but because the tier we assumed existed simply doesn't. Pricing pages have floors, minimums, and tier boundaries that don't show up until you're in the provisioning flow.&lt;/p&gt;

&lt;p&gt;Two questions worth asking before you provision any managed datastore. First: do you actually need HA, or are you about to pay for a replica to protect traffic that can tolerate a restart? For us the answer was yes, but only on the money path, and only at go-live. Second: is your capacity floor set by your workload or by the vendor's smallest HA tier? If it's the vendor's, there's usually a wire-compatible alternative that lets you size HA and capacity independently.&lt;/p&gt;

&lt;p&gt;We'll come back to the things this store actually powers in later posts — the idempotency middleware on the money paths is its own story, and so is the JWT denylist. This one was about the bill. We almost paid £180 a month. We pay £40.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>gcp</category>
      <category>saas</category>
      <category>backend</category>
    </item>
    <item>
      <title>One backend, four products: why we bet on platform-per-brand</title>
      <dc:creator>MD RASHEDUL ISLAM</dc:creator>
      <pubDate>Sat, 23 May 2026 04:06:24 +0000</pubDate>
      <link>https://forem.com/rhsumon/one-backend-four-products-why-we-bet-on-platform-per-brand-3j2d</link>
      <guid>https://forem.com/rhsumon/one-backend-four-products-why-we-bet-on-platform-per-brand-3j2d</guid>
      <description>&lt;p&gt;&lt;em&gt;by Rashed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We shipped an auth bandaid at 2am. Cookies wouldn't flow between &lt;code&gt;platform.ginilab.com&lt;/code&gt; and our gateway, which was running under a different registrable domain. Browsers blocked them, correctly. The bandaid that unblocked the demo was a 5-minute bearer token held in a Zustand store on the frontend, attached by hand to every request. It worked.&lt;/p&gt;

&lt;p&gt;Within 24 hours we'd shipped four PRs of cookie-domain workarounds. Then someone asked the obvious question: &lt;em&gt;"why isn't &lt;code&gt;api.ginilab.com&lt;/code&gt; just another hostname on the same gateway?"&lt;/em&gt; It was. We were deep into a problem we'd solved in a single DNS record.&lt;/p&gt;

&lt;p&gt;That bug — cookie-domain mismatch in a multi-brand platform — is the one-paragraph version of why this post exists.&lt;/p&gt;

&lt;p&gt;One caveat before we go further. v3 is in staging. It's not yet processing live payments. 300+ restaurants run on our legacy PHP/MySQL stack today, and the cohort migration hasn't started. What follows is the architecture we bet on and the pain we hit getting here, not a victory lap. If you want a "we scaled to a billion requests" story, this isn't it. If you want an honest mid-migration account from a small team, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "platform-per-brand" actually means
&lt;/h2&gt;

&lt;p&gt;Ginilab is one backend platform that runs four products. Tomafood is restaurant ordering, with 300+ restaurants live on the legacy stack and the full rewrite to v3 in progress. CloudPOS is a POS for non-food retail. iSchool is school management. Ecommerce is generic ecommerce. Tomafood is the full rewrite. The other three consume shared services via REST or SDK. They aren't separate codebases. They aren't separate backends. They're different products mounted on the same platform.&lt;/p&gt;

&lt;p&gt;This is unusual. Most SaaS teams either build one product and stay there, or build separate platforms per product when product two arrives. The shared-platform-across-products shape is the third path, and it has a tax: every architectural decision has to assume more than one consumer, more than one brand, more than one domain. The tax shows up early and never goes away.&lt;/p&gt;

&lt;p&gt;We took the tax on purpose. We knew CloudPOS and iSchool were coming. Without that, this would have been overengineering.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[INSERT DIAGRAM 1 HERE — architecture sketch: four products on top, shared multi-hostname gateway in the middle, shared services below keyed by business_id + app_id, Tomafood-only restaurant-service off to the side.]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  We rejected the obvious answer twice
&lt;/h2&gt;

&lt;p&gt;The obvious answer when a second product appears is to fork. Take the codebase that works for product one, copy it, change the domain, run a second backend. Engineers know how to do this. It feels safe.&lt;/p&gt;

&lt;p&gt;We rejected it twice. The first time was when CloudPOS came online and the temptation was to fork Tomafood's auth service and run it as a second backend behind the POS product. The second time was when iSchool was scoped and the temptation flipped: extract microservices per product, one stack per vertical. Both options were wrong for the same underlying reason. A customer who orders on Tomafood and later signs up for CloudPOS should be the same identity. Forking the auth service means reconciling those identities later. Per-product microservices means reconciling them four times.&lt;/p&gt;

&lt;p&gt;The version that doesn't require reconciliation is one auth service, multi-tenant by design, with every shared service carrying both &lt;em&gt;who the business is&lt;/em&gt; and &lt;em&gt;which product they're using&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design rule that makes it work
&lt;/h2&gt;

&lt;p&gt;Every shared service in the platform — auth, addresses, payments, notifications, gateway — carries two identifiers on every query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;business_id&lt;/code&gt; is the specific business (UUID).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_id&lt;/code&gt; is which product they're using: &lt;code&gt;tomafood&lt;/code&gt;, &lt;code&gt;cloudpos&lt;/code&gt;, &lt;code&gt;ischool&lt;/code&gt;, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not &lt;code&gt;restaurant_id&lt;/code&gt;. There is no &lt;code&gt;restaurant_id&lt;/code&gt; column anywhere in a shared service. &lt;code&gt;restaurant_id&lt;/code&gt; is a Tomafood-only concept that lives only in the Tomafood product service.&lt;/p&gt;

&lt;p&gt;The pair flows through JWT claims and is enforced at the repository layer. We say this in the CLAUDE.md at the root of the repo about as bluntly as we can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shared services NEVER use restaurant_id — always business_id + app_id.
Repository enforces WHERE business_id = ? on every query.
JWT claims include businessId + appId.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice that means a row in &lt;code&gt;auth_db.users&lt;/code&gt; doesn't know what a restaurant is. It knows it belongs to a business, and the business runs on an app. A row in &lt;code&gt;restaurant_db.recipes&lt;/code&gt; does know what a restaurant is, because &lt;code&gt;restaurant_db&lt;/code&gt; belongs to Tomafood and &lt;code&gt;restaurant_id&lt;/code&gt; is meaningful there.&lt;/p&gt;

&lt;p&gt;The boundary is consistent. Shared services see businesses. The Tomafood product service sees restaurants. That sentence took us a long time to write down, and longer to enforce.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[INSERT DIAGRAM 2 HERE — decision tree: shared service? then business_id + app_id. restaurant-service? then restaurant_id is fine. Neither? then you're in the wrong file.]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The multi-brand pain, made concrete
&lt;/h2&gt;

&lt;p&gt;The cookie story from the opener is what happens when "multi-brand" stops being an abstract design rule and becomes a Tuesday. Each restaurant on Tomafood can run on its own white-label domain — their brand, their registrable domain. The platform has its own brand. The gateway has to accept cookies from all of them.&lt;/p&gt;

&lt;p&gt;The first version of the cookie-domain helper was a security hole. It checked &lt;code&gt;host.includes('ginilab.com')&lt;/code&gt; to decide whether to set the cookie's domain attribute. A lookalike host like &lt;code&gt;ginilab.com.evil.example&lt;/code&gt; would have passed that check. The second version checks suffix-with-leading-dot:&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;// packages/shared/src/cookies/pick-domain.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pickCookieDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&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="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.ginilab.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.ginilab.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ... plus one branch per brand registrable domain&lt;/span&gt;
  &lt;span class="c1"&gt;// Lookalike-domain defence: must end with the LEADING dot,&lt;/span&gt;
  &lt;span class="c1"&gt;// not just contain the string.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;A handful of lines. They exist because we have more than one brand on one platform. If we'd had one brand, this would have been a hardcoded constant. If we'd had four separate backends, each one would have hardcoded its own constant, and the bug would live in four places.&lt;/p&gt;

&lt;p&gt;This is the smallest, ugliest example of the platform-per-brand tax. There are larger ones. They all have the same shape: a thing that would be a constant in a single-brand world becomes a function in a multi-brand one. The function is the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we picked, what we rejected, why
&lt;/h2&gt;

&lt;p&gt;We picked one backend platform, multi-product, multi-brand. Shared services keyed by &lt;code&gt;business_id&lt;/code&gt; + &lt;code&gt;app_id&lt;/code&gt;. The Tomafood-only product service keeps &lt;code&gt;restaurant_id&lt;/code&gt;. JWT carries both identifiers. The same gateway is exposed under per-brand hostnames so cookies flow.&lt;/p&gt;

&lt;p&gt;We rejected one codebase per product — four backends, four auth services, four databases. This is the standard SaaS path and most teams' default. We rejected it because the products share customers and the reconciliation cost compounds. A user who shops on Ecommerce and orders on Tomafood and whose kid is on iSchool is one human. Four backends would turn that human into four accounts with four passwords and four address books, held together by sync code. We would be writing and maintaining that sync code for years.&lt;/p&gt;

&lt;p&gt;We rejected microservices-per-product from day one. Per-vertical stacks, one platform-org per product. We rejected this because we're a small team and the operational surface scales with services rather than users. Splitting before a second consumer exists for any given surface is premature. Our restaurant-service today is a deliberate monolith — it contains menu, orders, kitchen, tables, drivers, reservations, and reviews in one deployable. We will split a surface out the moment a second consumer (CloudPOS, iSchool) needs that surface, and not before.&lt;/p&gt;

&lt;p&gt;We gave up the freedom to ship a product-specific schema change without thinking about other products. Every shared schema change has to consider all current and plausible future consumers. That slows down week-to-week work. The bet is that it speeds us up over the lifespan of the platform.&lt;/p&gt;

&lt;p&gt;What we got is more boring than it sounds: one auth service, one identity model, one set of secrets to rotate, and a single place to fix every helper.&lt;/p&gt;

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

&lt;p&gt;Platform-per-brand is a bet on product-multiplication. We made it because we knew CloudPOS and iSchool were coming. If you only ever ship one product, this is pure overhead. Every shared-service decision costs more than it would in a single-product codebase, and you get none of the payoff. If you'll ship two, the difference is one team versus four. If you'll ship four, there is no version of this where fork-and-clone stays survivable.&lt;/p&gt;

&lt;p&gt;Two questions worth sitting with before betting the same way. Do you know what product two looks like? Does it share customers with product one? If both answers are yes, the platform shape pays off. If either is no, it's overhead.&lt;/p&gt;

&lt;p&gt;We'll come back to specific pieces of this in later posts — the idempotency middleware on money paths, the multi-zone CDN purge, the Valkey vs Redis pricing fight, the strategic monolith. Each is its own story. This post is the foundation. Every later decision in the series only makes sense because the platform shape was already chosen.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>saas</category>
      <category>backend</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
