惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
D
Docker
博客园 - 聂微东
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 叶小钗
李成银的技术随笔
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
Jina AI
Jina AI
罗磊的独立博客
小众软件
小众软件
月光博客
月光博客
量子位
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - Franky
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Last Week in AI
Last Week in AI
J
Java Code Geeks
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
宝玉的分享
宝玉的分享
H
Help Net Security
腾讯CDC
T
ThreatConnect
Cyberwarzone
Cyberwarzone
S
Securelist
A
Arctic Wolf
B
Blog
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
A
About on SuperTechFans
F
Fox-IT International blog
P
Proofpoint News Feed
The Register - Security
The Register - Security
G
GRAHAM CLULEY
C
CXSECURITY Database RSS Feed - CXSecurity.com
阮一峰的网络日志
阮一峰的网络日志
P
Privacy & Cybersecurity Law Blog
美团技术团队
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
Security Latest
Security Latest
F
Full Disclosure
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity

DEV Community

Why Country/State/City Pickers Are Weirdly Hard Node.js 22 LTS — EOL Date, Support Timeline, and What Comes Next The 7-Layer Memory Architecture Behind Modern AI Agents I Imagined Hermes Agent Running an Entire Smart City — And It Changed How I See AI AI's tech debt is invisible — even to AI. I solved it at the architecture layer. Why ROAS 300% Can Still Mean Losses — Gross Margin in 5 Ecommerce Verticals You Don’t Need to Try Every AI Tool to Keep Up NovelPilot: A Novel Writing Agent Powered by Gemma 4 BoxAgnts is an Out-Of-The-Box Secure AI Agent ToolBox in a WASM SandBox Gemma 4 deep dive: why a 1.5 GB model scores 37.5% on competition mathematics, how the MoE routing actually works, and which model fits your hardware. Full breakdown inside. BeeLlama v0.2.0: 164 tok/s on a 27B model, one RTX 3090 Google Just Declared the Chat-Log Interface Dead. Here's What Neural Expressive Actually Signals for Developers. ARCHITECTURE SPECIFICATION & FORMAL SYSTEM REPORT: k501-AIONARC Notes from a Hammock What's Google Antigravity 2.0 ? Here's What the Agent Harness Actually Changes for Developers. Building an E2EE Chat App in Flask - Part 3: Keeping File Uploads Safe Google's Gemini Spark. Here's What It Actually Does for Developers. Microsoft Just Shipped MCP Governance for .NET. Here's What It Actually Enforces. How I Built a Pakistan Internet Speed Test Platform at 16 How to Build a Supervisor Agent Architecture Without Frameworks I Built My Own Corner of the Internet — Here's What It Looks Like How does VuReact compile Vue 3's defineExpose() to React? Neo-VECTR's Rift Ascent Idempotency Keys: The API Safety Net You Probably Aren't Using Building E-Commerce Sites for Niche Products: Technical Lessons from Specialty Outdoor Retailers Audit Logs: The Silent Guardian of Every Serious System Open-source SDS tooling for Japanese MHLW compliance: the gap nobody filled BetAGracevI I Built a Post-Quantum Cryptographic Identity SDK for AI Agents — Here's Why It Needs to Exist Running Claude Code across multiple repos without losing context There Are Cameras in Every Room of My House. I Put Them There. Why your AI agent loops forever (and how to break the cycle) How does VuReact compile Vue 3's defineSlots() to React? Building a Privacy-First Resume Editor with Typst WASM and React One Soul, Any Model: Portable Memory for Open-Source Agents with .klickd From Pixels to Prescriptions: Building an Autonomous Healthcare Booking Agent with LangGraph MonoGame - A Game Engine for Those Who Love Reinventing the Wheel # Day 24: In Solana, Everything is an Account Mastering Node.js HTTP Module: Build Servers, REST APIs, and Handle Requests Mastering Node.js HTTP Module: Build Servers, REST APIs, and Handle Requests RP2040 Wristwatch Tells Time With a Vintage VU Meter Needle observations about models / 2026, may From Video Transcripts to Source-Grounded AI Notes: A Practical Look at Notesnip AI Agent Dev Environment Guide — Real Experience from an AI Living Inside a Server How I Run 7 AI Models 24/7: Multi-Agent Architecture in Practice What exactly changes with the Claude Max plan? I Revived a Broken MLOps Platform — Now It's Self-Service, Policy-Guarded, and Operationally Credible OpenAI's $2M-tokens-for-equity YC deal, decoded Why DMX Infrastructure is Still Stuck in the 90s Agent Series (2): ReAct — The Most Important Agent Reasoning Paradigm Open Source Project (No.73): Sub2API - All-in-One Claude/OpenAI/Gemini Subscription-to-API Relay I Made the Wrong Bet on Event Streaming in Our Treasure Hunt Engine #ai #productivity #chatgpt #python Symbolic Constant Conundrum From Manual RAG to Real Retrieval — Embedding-Based RAG with NVIDIA NIM Building an outbound-only WebSocket bridge for local AI agents Our System's Sins in Ghana: Why We Had to Rethink Digital Product Sales Execution Governance, AI Drift, and the Security Paradox of Runtime Enforcement Differential Pair Impedance: Why USB and HDMI Routing Is a Geometry Problem Small AI database questions can become big scans Claude Code 2.1 Agent View & /goal: Autonomous Dev Guide 2026 Your AI database agent should not see every column Rust's Low-Latency Conquest: Why We Ditched C++ for a Treasure Hunt Engine Floating-point will quietly corrupt your emissions math, and 0.1 + 0.2 already warned you Autonomous Agents: what breaks first (and why that's the real product) [2026-05-23] Agent payments are the new cloud bill footgun ORA-00069 오류 원인과 해결 방법 완벽 가이드 How I Built a Local, Multimodal Gemma 4 Visual Regression & Patch Agent: Closed-Loop Validation, Canvas Pixel Diffing, and Reproducible Benchmarks Pressure-testing Ota on Supabase: from setup prose to executable repo readiness VPC CNI en EKS: cómo dejar de pagar nodos que no usás The Future of Text Analysis: Introducing TechnoHelps Semantic Engine I built a Chrome Extension that saves product images + context directly to Google Drive & Sheets 95+ browser-based dev tools that never touch a server Running Qwen 2.5 Coder 14B Locally in Cursor with Ollama From a 10,000-line OpenSearch export script to a log analysis tool Ghost Bugs Cost $40K: A Neural Debugging Postmortem SECPAC: A Lightweight CLI Tool to Password-Protect Your Environment Variables 🚀 PasteCheck v1.7 + v1.8 — Hints that tell you what to fix, and a nudge panel that tells you where to start 8 Real Ways Developers Make Money in 2026 (Ranked by Effort) I built a free AI-powered Git CLI that writes your commit messages for you sds-converter: Converting Safety Data Sheets to MHLW Standard JSON with Rust and LLMs OpenLiDARViewer: A Browser-Based LiDAR and Point-Cloud Viewer Local-First Browser Tools: What You Should Not Upload Online Why most freelancers undercharge (and the maths behind fixing it) We built a mahjong dangerous-tile predictor calibrated on 4.97M real hands Building a Chord Progression Generator in the Browser — Music Theory in JS, Sound via Web Audio API tutorial #10: 148 Opens, 0 Replies — How My Forge Cold Email v1 Completely Failed 9 in 10 Docker Compose files skip the basic security flags How to Forward Android SMS to Telegram Automatically I built the first security scanner for MCP servers — here's what I found Building an Interplanetary Quantum Logic Engine in Rust/Ovie From AI Code Generation to AI System Investigation I gave Gemini 3.5 Flash a CVE-fix PR to review. It found another bug in the same file. When I Realized We Were Throwing Away Half Our Engine's Potential TokenJuice and the 20-Minute Cron: Inside OpenHuman’s Aggressive Context-Harvesting Engine CodeDNA: AI Codebase Archaeologist Built with Gemma 4 Thinking Mode Building a semantic search API in Go with Meilisearch April 2026 DigitalOcean Tutorials: Inference Optimization and AI Infrastructure Looking for DTMF transceiver module Moving Beyond "Tribal Software": Why the Singularity Demands the Interplanetary Hybrid Human
One backend, four products: why we bet on platform-per-brand
MD RASHEDUL · 2026-05-23 · via DEV Community

by Rashed

We shipped an auth bandaid at 2am. Cookies wouldn't flow between platform.ginilab.com 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.

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

That bug — cookie-domain mismatch in a multi-brand platform — is the one-paragraph version of why this post exists.

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.

What "platform-per-brand" actually means

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.

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.

We took the tax on purpose. We knew CloudPOS and iSchool were coming. Without that, this would have been overengineering.

[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.]

We rejected the obvious answer twice

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.

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.

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

The design rule that makes it work

Every shared service in the platform — auth, addresses, payments, notifications, gateway — carries two identifiers on every query:

  • business_id is the specific business (UUID).
  • app_id is which product they're using: tomafood, cloudpos, ischool, and so on.

Not restaurant_id. There is no restaurant_id column anywhere in a shared service. restaurant_id is a Tomafood-only concept that lives only in the Tomafood product service.

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:

Shared services NEVER use restaurant_id — always business_id + app_id.
Repository enforces WHERE business_id = ? on every query.
JWT claims include businessId + appId.

Enter fullscreen mode Exit fullscreen mode

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

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.

[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.]

The multi-brand pain, made concrete

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.

The first version of the cookie-domain helper was a security hole. It checked host.includes('ginilab.com') to decide whether to set the cookie's domain attribute. A lookalike host like ginilab.com.evil.example would have passed that check. The second version checks suffix-with-leading-dot:

// packages/shared/src/cookies/pick-domain.ts
export function pickCookieDomain(
  origin: string | undefined
): string | undefined {
  if (!origin) return undefined;
  const host = new URL(origin).hostname;
  if (host.endsWith('.ginilab.com')) return '.ginilab.com';
  // ... plus one branch per brand registrable domain
  // Lookalike-domain defence: must end with the LEADING dot,
  // not just contain the string.
  return undefined;
}

Enter fullscreen mode Exit fullscreen mode

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.

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.

What we picked, what we rejected, why

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

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.

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.

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.

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.

The takeaway

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.

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.

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.