I built two products that need to talk to each other constantly: Saturdays, a food delivery platform, and DineGuru, a restaurant POS. A restaurant running DineGuru has to be able to work its Saturdays orders from the POS it already uses. When you own both codebases, the shortcut is right there โ point them at the same database and skip the API. I didn't, and the reasons apply to any two systems you happen to control.
Why "they're both mine" is the trap
Owning both sides removes the social pressure to design an interface. It doesn't remove the engineering need for one. A shared database means:
- Every schema change is a two-product deploy. Rename a column in one and the other breaks at runtime.
- There is no authority. Either product can write anything, so business rules enforced in one are silently bypassed by the other.
- You can't reason about blast radius. A bug in the POS can corrupt delivery data, and nothing in the architecture says it shouldn't be able to.
The day the two products have different release cadences, different teams or different customers, all of that becomes a crisis. Designing the boundary up front is far cheaper.
What the boundary looks like
DineGuru drives day-to-day restaurant operation through a separate, versioned API surface on Saturdays, and the design of that surface is where the real decisions are.
Authenticated by an API key, scoped to one restaurant. The key doesn't identify "DineGuru." It identifies one restaurant โ and every request made with it can only see and act on that restaurant's data. There is no endpoint where a key reaches across restaurants, because the scoping is applied before any view logic runs.
Identity is read-only across the boundary. The POS can operate a restaurant's day. It cannot rename the restaurant, and it cannot clear a platform suspension. Those are platform decisions, and the integration surface simply doesn't expose them. A key that operates the day is a very different permission from a key that administers the account.
The same domain rules apply. Orders changed through the POS pass through the same state machine and the same payment gate as orders changed anywhere else. The integration is one more caller of the domain layer, not a side door around it.
Where the key lives
One decision ended up simplifying the whole integration. The API key is stored encrypted on DineGuru's server and used server-to-server. It never reaches a browser.
That's the right security posture on its own, but it had a useful side effect: because no browser ever calls Saturdays with that key, no cross-origin surface had to be opened for the integration. No CORS allowlist for a partner domain, no preflight handling, no chance of a misconfigured origin rule exposing the API to arbitrary sites. The server-to-server design removed a category of configuration rather than securing it.
In the other direction, events flow out through an HMAC-signed webhook relay, so the receiving side can verify a message genuinely came from Saturdays.
The takeaway
Integrate your own systems the way you'd integrate a stranger's: a versioned contract, credentials scoped to the smallest unit that makes sense, identity and administration kept off the operational surface, and every request routed through the same rules as everyone else. It's more work on day one, and dramatically less on the day the two products stop moving in lockstep.
How that integration fits into the rest of the platform is covered in the case study.
๐ See it: www.divyakush.com/projects/saturdays
Divyakush Punjabi โ Full-Stack & AI Systems Engineer
๐ https://www.divyakush.com ยท ๐ผ LinkedIn ยท ๐ป GitHub
Top comments (0)