AZ
All writing
3 min read

Per-operation services and IAM for a small business

nextjsarchitectureiamredisobservability
Part of the projectGolden BiteA premium treats/catering business in Abuja delivered as three surfaces in one Next.js 16 app — storefront, ops dashboard, and staff app — and the origin of the "per-operation" arch.View project

Golden Bite is a premium treats and catering business in Kubwa, Abuja, served by three surfaces in one Next.js 16 app: a customer storefront, an operations dashboard, and a staff (kitchen + delivery) app. The interesting decision is that a small business runs on an isolation discipline usually reserved for much bigger systems — and this app is where the "Golden Bite arch" that later apps clone was born.

Per-operation services

Rather than a few fat service objects, each operation is its own file. orders alone has createOrder.ts, updateOrderStatus.ts, checkOrderCapacity.ts, computeOrderTotals.ts, attachDriver.ts, setDeliveryProof.ts, and more, with a barrel re-exporting per namespace. Below them the model layer is likewise per-operation *DB functions, each Prometheus-instrumented. One operation → one service call → one authority check → one audit action.

IAM sized for a bakery

Authority is a five-role union — customer | kitchen | delivery | manager | owner — with role groups defined once (STAFF_ROLES, ADMIN_ROLES). Two enforcement styles coexist: a wrapper that gates by allowed roles, and inline sentinel-error checks inside handlers.

export function withAuth(handler: TAuthedHandler, ...roles: TRole[]) {
  return async (req: Request) => {
    const session = await getSessionUser();
    if (!session) return fail(401, "Not authenticated");
    if (roles.length && !roles.includes(session.role)) return fail(403, "Forbidden");
    return handler(req, session);
  };
}

The edge proxy.ts does only a cheap cookie-presence redirect and is explicitly "not a security boundary" — the role is re-checked in every handler, so authority is never trusted at the edge. Each write is independently gated and audited:

export const POST = withApiHandler(
  { route: "/api/admin/menu/products" },
  auditAdmin(postHandler, { action: "product.create", targetType: "product", captureBody: true }),
);

The wrapper enforces order

withApiHandler composes the cross-cutting concerns in a fixed order, and the Prometheus observation always fires — success or thrown error:

const rl = await consume({ identifier: ip, scope: rlScope, max: rlMax, windowSeconds: rlWindow });
if (!rl.allowed) { /* 429 + Retry-After + X-RateLimit-* */ }
else response = await handler(req, ctx);
// … finally, always:
restResponseTimeHistogram.observe({ ip, method: req.method, route, status_code }, elapsedSeconds);

The supporting cast, each doing one thing

Zod validates at the edges with .strict() objects; an ioredis singleton throws loudly on a 5-second boot ping rather than silently falling back to memory; reads are cache-first per operation under a namespaced key (services:products:listProducts:{cat}:{q}:{f}:{l}) with a short TTL and explicit invalidation on write; two Prometheus histograms (http_request_duration_seconds and database_request_duration_seconds) make every route and every DB call independently observable; and parallel auditAdmin/auditUser streams fire in a finally so they survive a throw.

Honest edges

Two things get stated plainly rather than polished away: there's no unit-test runner here — the automated coverage is Playwright e2e (serial, hermetic S3) + ts.check — and /api/metrics plus the dev otp-peek/reset-token-peek routes are unauthenticated and should be prod-disabled or protected at ingress.

Why bother at this size

Because the discipline is nearly free once it's a habit, and it scales down as gracefully as up. The same instinct — isolate operations, give each the least authority it needs, measure them separately — is what shows up, much larger, in the platforms. Practising it on a bakery keeps it sharp, and Adverta later cloned this exact shape.

Back to the projectGolden BiteSee the full case study and related write-ups.View project