Skip to content
All work
2026Solo — architecture, services, frontends, infrastructure

MicroService Restaurant

Multi-tenant food-ordering platform — six services with Kafka in between

Live in production on a single-node Kubernetes cluster

TypeScript · Node.js · Express 5 · Next.js 16 · React 19 · MongoDB · PostgreSQL · TypeORM · Mongoose · Apache Kafka · Socket.IO · Stripe · JWT / JWKS (RS256) · Kubernetes (k3s) · nginx ingress · cert-manager · Docker · AWS S3 · Vercel · Cloudflare

MicroService Restaurant
The MicroService Restaurant storefront — Brick & Basil Pizza Co. with the live menu
MicroService Restaurant
Category tabs and the product grid on the MicroService Restaurant storefront

01

Context

MicroService Restaurant is a food-ordering platform where restaurants are tenants: each one gets its own storefront, menu, and order stream, served from the same fleet. Customers browse a menu, build a cart, and pay through Stripe; admins manage products, toppings, and incoming orders; both sides see order state change in real time.

It exists because I wanted to answer a specific question honestly: what does it actually take to run an event-driven system, not sketch one? The answer turned out to be six services, an event bus, a replica set, two frontends, an ingress with per-path routing, and a lot of decisions about where state lives and who is allowed to know what.

02

Architecture

Services are independently deployable and communicate asynchronously through Kafka. HTTP traffic enters through an nginx ingress: /api/auth, /api/catalog, and /api/order route to their services, while the WebSocket service owns the upgrade path. Auth is the only service that touches the users/tenants database; everyone else verifies tokens locally against a published JWKS endpoint.

auth-service

Express 5 · TypeORM · PostgreSQL

Tenants, users, refresh tokens; signs RS256 JWTs and publishes JWKS at /.well-known/jwks.json

catalog-service

Express 5 · Mongoose · S3

Products, categories, toppings and price configuration; product images to S3

order-service

Express 5 · Mongoose · Stripe

Carts, orders, coupons; Stripe Checkout sessions and the payment webhook receiver

ws-service

Socket.IO · Kafka consumer

Fan-outs order events to customer and admin UIs in real time

notification-service

Kafka consumer · Nodemailer

Sends order emails off the same event stream — no HTTP surface at all

client-ui

Next.js 16 · Tailwind

Customer storefront: menu, cart, checkout, order tracking

admin-dashboard

React 19 · Vite · Ant Design

Restaurant admin: products, toppings, live order queue

deployment

Kubernetes · nginx ingress · cert-manager

Per-service manifests: deployments, ingresses, TLS certificates

brokers

Kafka (KRaft) · Docker Compose

Event bus configuration — topics for products, toppings, and orders

  • Data is deliberately split: PostgreSQL behind the auth service, MongoDB (single-node replica set, so transactions work) for catalog and orders.
  • The public storefront runs against Stripe test keys — nothing anyone does in the demo can move real money.

The source lives as per-service repositories under the paaradox-labs org — the architecture section links each one.

03

The hard parts

01

Distributed auth without a shared session store

Problem
Six services need to trust the same identity. A shared session table or an introspection call to auth-service would couple every request to one service being up, and turn auth into a bottleneck.
Decision
auth-service signs RS256 JWTs and exposes the public keys at /.well-known/jwks.json. Every other service holds only the public key material and verifies tokens locally, with no callback to auth on the request path.
Tradeoff
Verification is stateless and fast, but JWTs can't be revoked mid-life. I accepted that for short-lived access tokens, paired with refresh tokens stored server-side in Postgres, which is where revocation actually happens.
Outcome
Catalog, order, and ws services can restart or scale independently; the only shared secret in the system is a public key.

02

Making a Stripe webhook behave like an event in a system that already has an event bus

Problem
Payments finish out of band. The browser must not be the source of truth for whether an order is paid, but the UI shouldn't poll either.
Decision
order-service verifies the webhook signature, writes payment/order state, then publishes an order event to Kafka. From there it's one stream feeding two consumers: notification-service sends the email, ws-service pushes the update to open UIs.
Tradeoff
The system is eventually consistent — for a moment the customer can see 'processing' after Stripe has succeeded. In exchange, one write path serves email, storefront, and admin, and retries are Kafka's problem, not mine.
Outcome
Checkout completion travels storefront → Stripe → webhook → Kafka → customer UI, admin dashboard, and inbox without any component asking another 'is it done yet?'.

03

Real-time order updates that survive horizontal scaling

Problem
Socket.IO connections are sticky — each browser is attached to one process. If every ws instance consumed Kafka for every tenant, fan-out would duplicate work and state would drift.
Decision
ws-service keeps a single Kafka consumer group for order events and an in-memory registry of authenticated sockets keyed by tenant and order. Events are routed to the right rooms by the process that holds the connection; tenant scoping comes from the JWT, not from client input.
Tradeoff
An instance holds connection state in memory, so a pod restart drops its sockets — clients reconnect and re-subscribe. That's a visible blip, but it keeps the service stateless from Kafka's perspective and lets it scale horizontally.
Outcome
One payment event updates the customer's order page and the admin's live queue at the same time, with no polling anywhere in the product.

04

A seven-process dev environment people can actually start

Problem
Locally, the system is two frontends, four HTTP services, a consumer, Kafka, MongoDB in replica-set mode, Postgres, and an ingress that splits /api/* by prefix. 'Just read the code' is not onboarding.
Decision
The monorepo README is a runbook: exact Docker commands for Kafka and the Mongo replica set, a one-liner Postgres, per-service install and env steps, Stripe CLI forwarding with the webhook secret wiring, a quick-start cheat sheet, and troubleshooting entries for the failures I actually hit.
Tradeoff
Maintaining the runbook takes time on every port or config change, and there's an nginx container in the dev loop that isn't 'real' Kubernetes. The alternative was everyone inventing their own broken startup order.
Outcome
A cold clone goes from zero to a working storefront with live payments in about fifteen minutes of copy-paste — including the replica-set initialization that silently breaks transactions if you skip it.

04

What I'd change

What I'd change next: structured logging with correlation IDs per order, because tracing a single order across five services currently means grepping five terminals. After that, schema contracts for Kafka topics — the event payloads are typed in code but the topics deserve versioned schemas, and it's the one place where a producer change could surprise a consumer.

The single-node cluster is a deliberate cost decision, not an accident: it gives me real ingress, TLS, and deployment semantics for a fraction of a managed control plane. The manifests are written so moving to a multi-node cluster is a kubeconfig change, not a rewrite.

05

Stack

TypeScript · Node.js · Express 5 · Next.js 16 · React 19 · MongoDB · PostgreSQL · TypeORM · Mongoose · Apache Kafka · Socket.IO · Stripe · JWT / JWKS (RS256) · Kubernetes (k3s) · nginx ingress · cert-manager · Docker · AWS S3 · Vercel · Cloudflare