ByteBite Docs
Infrastructure

Infrastructure Architecture

Application topology, request flows, tenant trust boundaries, and runtime boundaries.

Infrastructure Architecture

ByteBite is a modular monolith in a TypeScript monorepo. All business logic flows through a single Fastify API; storefront and dashboard are separate front-end applications. PostgreSQL is the sole primary database.

Monorepo topology

apps/
  api/          Fastify 5 — central backend boundary
  storefront/   Next.js 16 — public restaurant sites
  dashboard/    React + Vite — operator UI (nginx in production)
  docs/         Fumadocs / Next.js — this documentation

packages/
  db/           Drizzle schema, migrations, seeds
  domain/       Pure domain logic (hostname normalization, tenancy)
  auth/         Better Auth (planned wiring)
  schemas/      Zod schemas
  types/        Shared TypeScript types
  config/       Shared configuration
  observability/ Sentry + pino logging

Runtime versions are pinned in Dockerfiles and package.json manifests (Node 24 LTS, pnpm 12.3.4). See Stack for the full application stack.

Runtime boundaries

AppDev runtimeProduction runtimePort
APItsx watchnode server.js (tsup bundle)3001
Storefrontnext devNext.js standalone (node apps/storefront/server.js)3000
DashboardVite dev servernginx unprivileged static8080 (prod) / 5173 (dev)
Docsnext devNext.js standalone3002

Storefront and dashboard never connect to PostgreSQL directly. All data access goes through the API.

External request flow (staging)

Browser
  Host: luigi.staging.getbytebite.co
    -> DNS (Cloudflare) -> 88.99.0.118
      -> Traefik :443 (TLS termination)
        -> storefront:3000 (Docker, dokploy-network)

For same-origin API calls:

Browser
  GET https://luigi.staging.getbytebite.co/api/health
    -> Traefik (Host + PathPrefix `/api`, priority 20)
      -> StripPrefix `/api`
        -> api:3001
          -> Fastify sees GET /health
          -> Tenant from Host header (luigi.staging.getbytebite.co)

Explicit Dokploy Host() routers take precedence over wildcard HostRegexp routers (lower priority). See Traefik.

SSR internal API flow

Storefront server-side rendering cannot use the browser's public hostname when calling the API container. It uses Docker DNS:

Next.js SSR (storefront container)
  fetch(API_INTERNAL_URL + path)   # default http://api:3001
    Header: X-ByteBite-Tenant-Host: luigi.staging.getbytebite.co
    Host: api                        # internal Docker service name
      -> Fastify
        -> resolveEffectiveHostname()
           request Host is "api" (internal identity)
           -> honor X-ByteBite-Tenant-Host
           -> tenant lookup for luigi.staging.getbytebite.co

API_INTERNAL_URL is set in compose.deploy.yml and local compose.yml. Browsers use same-origin /api via Traefik instead.

Tenant lookup and trust boundary

Tenant resolution is an exact hostname lookup in restaurant_domains after normalization (packages/domain). The first DNS label is not the tenant key.

Request contextTenant authorityX-ByteBite-Tenant-Host
Browser / same-origin /apiHost header from TraefikIgnored
Public api.staging.getbytebite.coHost headerIgnored
Storefront SSR → http://api:3001Internal header (if Host is api)Honored

Fastify runs with trustProxy: false. External X-Forwarded-Host is not trusted. The internal header is accepted only when the request Host is one of: api, localhost, 127.0.0.1, ::1.

Implementation: apps/api/src/lib/tenant-host.ts, apps/api/src/plugins/tenant.ts.

Public callers cannot spoof tenant identity by setting X-ByteBite-Tenant-Host. The raw API port is not publicly exposed.

Database relationship

api:3001
  -> DATABASE_URL (Dokploy env, internal network)
    -> PostgreSQL (Dokploy-managed service)
      -> restaurants
      -> restaurant_domains (hostname -> restaurant_id)

Migrations are explicit (node db/dist/migrate.js); the API does not auto-migrate on startup.

Wildcard staging tenants

Dynamic tenant hostnames (<slug>.staging.getbytebite.co) are routed by Traefik labels in compose.deploy.yml, not by Dokploy's domain UI (which rejects *.staging.getbytebite.co). DNS wildcard exists; a hostname resolving does not mean a restaurant row exists.

Unknown tenants:

  • Storefront: renders "Restaurant not found" (currently HTTP 200 — application gap)
  • API (tenant-aware routes): HTTP 404
  • /api/health: HTTP 200 regardless of tenant

Intentionally not used

TechnologyStatus
RedisNot present
BullMQNot present (ADR-016 uses DB-backed scheduler)
KubernetesNot present
GraphQLNot present
MicroservicesNot present — modular monolith

On this page