Skip to content

About

A microservices demo behind a Ferron 3 gateway

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

FerronShop — microservices demo behind a Ferron 3 gateway

A miniature e-commerce app that mimics a real-world setup: visitors use a storefront (served by the gateway itself), all API traffic goes through Ferron 3 as an API gateway, and several microservices sit behind it — some public, some gated by authentication and rate limiting. Postgres for state, Redis for distributed rate limiting, RabbitMQ for async order events.

                    ┌─────────────────────────────────────────────┐
 visitors ──► :8080 │  Ferron 3 gateway (ferronserver/ferron:3)   │
                    │  static /  +  /api/* reverse proxy          │
                    └──────┬────────┬────────┬────────┬───────────┘
                           │        │        │        │
                    ┌──────▼──┐ ┌───▼────┐ ┌─▼──────┐ ┌▼────────────┐
                    │  auth   │ │catalog │ │ orders │ │notifications│
                    │  :8001  │ │ :8002  │ │ :8003  │ │   :8004     │
                    └───┬─────┘ └───┬────┘ └───┬────┘ └┬────────────┘
                        │           │          │       │
                        ▼           ▼          ▼       ▼
                  ┌─────────────┐ ┌─────┐ ┌────────────────┐
                  │  postgres   │ │redis│ │   rabbitmq     │
                  │ users,      │ │rate │ │ orders ──►     │
                  │ products,   │ │limit│ │ order.created  │
                  │ orders,     │ │buckets│                │
                  │notifications│ └─────┘ └────────────────┘
                  └─────────────┘

Quickstart

cp .env.example .env          # optional; sets JWT_SECRET
docker compose up -d --build
docker compose logs -f gateway   # gateway access/error logs
docker compose down              # stop everything
docker compose down -v           # also wipe postgres/redis/rabbitmq data

What the gateway does (gateway/ferron.conf)

Feature How Where
Static frontend + API on one origin root for /, proxy for /api/* (mixed setup, no CORS needed) host block
Forwarded auth auth_to http://auth:8001/verify + copy X-Auth-User X-Auth-Role /api/orders, /api/notifications, /api/admin/products
Distributed rate limiting rate_limit tiers + rate_limit_backend { type redis … } shared by all gateway replicas per location (see below)
Fail-closed login fail_open false on the auth backend: Redis outage ⇒ logins denied, not unlimited /api/auth
Response caching cache { … }, origin sends Cache-Control: public, max-age=30 /api/products only
Resilience active_check health probes + circuit_breaker { record_5xx … } /api/orders
Hardening security headers, compressed + dynamic_compressed, json_errors whole host

Rate-limit tiers (per client IP, token bucket capacity = rate + burst):

Route rate / burst Notes
/api/auth/* 5/s, burst 10 brute-force protection, fail-closed
/api/products 100/s, burst 200 public reads, cached
/api/orders, /api/admin/products 20/s, burst 40 authenticated writes
/api/notifications 30/s, burst 60 authenticated reads

Request flows

  • Browse (public, cached): GET /api/products → gateway (cache lookup, generous bucket) → catalog → Postgres. Repeat hits served from gateway memory for 30s.
  • Checkout (authenticated, async): POST /api/orders → gateway verifies JWT via auth_to → orders service prices items, decrements stock transactionally, stores the order, publishes order.created → 202/201 returns immediately → notifications worker consumes the event and writes the "Order confirmed" row → frontend polls /api/notifications.
  • Admin write (authenticated + authorized): POST /api/admin/products → same backend as the public catalog, but the gateway enforces auth and the service enforces role == admin. (Catalog cache refreshes within ~30s TTL.)

API reference (all behind http://localhost:8080)

Method & path Auth Service
POST /api/auth/register {email, password} → 201 {token,…} no auth
POST /api/auth/login → 200 {token,…} no auth
GET /api/auth/me JWT (service-checked) auth
GET /api/products, GET /api/products/<id> no catalog
POST /api/admin/products, PUT/DELETE /api/admin/products/<id> forwarded auth, admin catalog
GET /api/orders, GET /api/orders/<id> forwarded auth orders
POST /api/orders {items:[{product_id, qty}]} → 201 forwarded auth orders
GET /api/notifications forwarded auth notifications

Make a user admin (product writes require it):

docker compose exec postgres psql -U shop -d shopdb \
  -c "UPDATE users SET role='admin' WHERE email='you@example.com';"
# then log in again to get a token with the admin role

Two things I learned about Ferron 3 while building this

  1. location strips its prefix before proxying. /api/auth/login reaches the auth container as /login. Backend routes are written against stripped paths. Exact-match locations strip to /, so an auth endpoint must handle /.
  2. auth_to appends the stripped path to its backend URL. With auth_to http://auth:8001/verify, a check for GET /api/orders arrives as GET /verify/. The auth service therefore answers verification on /verify and on any other path (catch-all), except its own public routes. A future Ferron 3 release would not append the stripped path to auth_to URLs.

Services also verify a Bearer JWT themselves (shared JWT_SECRET) instead of blindly trusting X-Auth-User, so they stay safe if called directly — gateway auth is the edge check, service auth is defense in depth.

Repo layout

docker-compose.yml        # gateway + 4 services + postgres/redis/rabbitmq
gateway/ferron.conf       # the whole gateway config (validated with `ferron validate`)
frontend/                 # vanilla JS SPA, served by Ferron from /var/www/frontend
services/auth/            # register/login/JWT + /verify for the gateway
services/catalog/         # product CRUD (public reads, admin writes)
services/orders/          # orders + publishes order.created to RabbitMQ
services/notifications/   # GET API + background worker consuming order.created
db/init.sql               # schema + 8 demo products
scripts/demo.sh           # e2e smoke test through the gateway

Production notes (deliberately demo-grade)

  • Services run Flask's dev server; use gunicorn/uvicorn + replicas in production.
  • All services share one Postgres database for simplicity; real systems give each service its own schema/database and talk service-to-service via APIs.
  • Order-event publishing is best-effort; the transactional outbox pattern would make it reliable.
  • Per-IP limiting sees the Docker NAT address for all visitors in this setup; behind a real LB/CDN, set client_ip_from_header so remote_address is real.
  • No TLS here (plain :80 → :8080); Ferron supports automatic ACME TLS — switch the host block to a real domain and it just works.
  • JWT_SECRET must be a strong random value outside demos ($ openssl rand -hex 32).

License

Unlicense, since the whole demo is AI-generated, and AI-generated works would be public domain (and Unlicense is public domain-equivalent). 😅

About

A microservices demo behind a Ferron 3 gateway

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages