ByteBite Docs
Architecture

Tenancy & storefront

Hostname-based tenant resolution and the public storefront restaurant contract.

Tenancy & storefront

The first product slice is Tenant → Restaurant → Domain → Storefront.

The same Storefront application renders different restaurant branding based on the request hostname. Tenant authority is never a client-supplied restaurantId.

Local restaurants

HostRestaurant
http://luigi.bytebite.testPizzeria Luigi
http://mario.bytebite.testMario's Restaurant
http://unknown.bytebite.testRestaurant not found
http://storefront.bytebite.testNeutral ByteBite development host (not a restaurant tenant)

unknown.bytebite.test must be listed in /etc/hosts (no wildcard DNS). Add it alongside Luigi and Mario. See Getting started.

Seeds are idempotent (pnpm db:seed).

Database

restaurants

ColumnNotes
idUUID (opaque identifier)
namePublic name
slugUnique
statusACTIVE | INACTIVE
logo_urlOptional
primary_color / secondary_colorStorefront branding
created_at / updated_atTimestamps

restaurant_domains

ColumnNotes
idUUID
restaurant_idFK → restaurants.id
hostnameUnique, stored normalized (lowercase, no port)
typeBYTEBITE_SUBDOMAIN | CUSTOM_DOMAIN
is_primaryAt most one primary domain per restaurant
verified_atPresent on local seeds; custom-domain verification is a later slice
created_at / updated_atTimestamps

Lookup is an exact match on hostname after normalization. The first DNS label is not the tenant key.

Hostname normalization

normalizeHostname (in packages/domain) is the only normalization function:

  • trim surrounding whitespace
  • lowercase
  • strip :port for hostnames and IPv4
  • strip a trailing FQDN dot
  • understand bracketed IPv6 ([::1]:3000::1)

It does not strip subdomains or infer a slug.

Examples:

  • LUIGI.BYTEBITE.TEST:80luigi.bytebite.test
  • luigi.bytebite.test:3000luigi.bytebite.test

Tenant resolution flow

Host
  → normalize hostname
  → restaurant_domains exact lookup
  → restaurant
  → validate status is ACTIVE
  → TenantContext
  → request handling

Internal outcomes:

  • resolved
  • domain_not_found
  • restaurant_inactive

Anonymous clients see the same public 404 (Restaurant not found) for unknown hosts and inactive restaurants.

Database or infrastructure failures during hostname lookup return 503 Service Unavailable with a generic message. They are never reported as “restaurant not found”.

GET /health and GET /health/db do not require a restaurant. They stay available on api.bytebite.test.

Tenant security

External / browser (including same-origin /api): tenant authority is the Host header as Traefik delivered it. Fastify does not enable trustProxy. X-Forwarded-Host from clients is not used.

Internal Storefront SSR: Next.js calls API_INTERNAL_URL (http://api:3001). That request reaches Fastify on the private Docker service path; the api hostname is not publicly exposed. The Storefront sends the original normalized host in X-ByteBite-Tenant-Host.

Fastify honors that header only when the request Host is the API's internal identity:

  • api
  • localhost
  • 127.0.0.1
  • ::1

It is ignored on every public hostname, including:

  • luigi.bytebite.test (cannot switch to Mario)
  • api.bytebite.test (cannot spoof a tenant through the public API host)

restaurantId in the query string, body, or a custom browser header cannot select a tenant.

Public API

GET /storefront/restaurant

Example (Luigi, Host luigi.bytebite.test or trusted internal header):

{
  "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
  "name": "Pizzeria Luigi",
  "slug": "luigi",
  "branding": {
    "logoUrl": null,
    "primaryColor": "#1F7A4D",
    "secondaryColor": "#E8C547"
  }
}

Same-origin: http://luigi.bytebite.test/api/storefront/restaurant (Traefik strips /api, Host is preserved).

Direct API tools should send a Host override (Host: luigi.bytebite.test), not ?restaurantId=.

GET http://api.bytebite.test/storefront/restaurant has no restaurant tenant and returns 404.

Storefront

The Next.js Storefront loads this endpoint on the server (cache: "no-store"), parses the payload with StorefrontRestaurantSchema, and renders name + branding colors.

Unknown or inactive hosts render:

  • Restaurant not found
  • This ordering page is not available.

On this page