Guest cart
Tenant-isolated storefront cart with a host-only cookie and live menu prices.
Guest cart
The third product slice is Cart → Storefront. A guest adds active menu items to a cart that belongs to the restaurant resolved from the hostname.
Prices are not snapshotted on the cart. Reads and mutations resolve name, price_cents, and is_active live from menu_items. Snapshots happen at checkout. Cart GET stays 200 when the restaurant is closed; pickup submit is gated there.
Tables
carts
| Column | Notes |
|---|---|
id | UUID. Value stored in the bb_cart cookie |
restaurant_id | FK → restaurants.id (ON DELETE restrict) |
created_at / updated_at | Timestamps |
cart_items
| Column | Notes |
|---|---|
id | UUID. Path :itemId on PATCH/DELETE |
cart_id | FK → carts.id (ON DELETE cascade) |
restaurant_id | Defense-in-depth copy of the tenant key |
menu_item_id | FK → menu_items.id |
quantity | Integer 1..99 |
created_at / updated_at | Timestamps |
Unique (cart_id, menu_item_id). Adding the same item again increases quantity and caps at 99.
Seeds do not create carts.
Cookie bb_cart
The cookie value is the cart UUID. Fastify sets and rotates it. The browser is not the source of truth (no localStorage).
| Attribute | Value |
|---|---|
| Name | bb_cart |
| httpOnly | yes |
| SameSite | Lax |
| Domain | omitted (host-only). Luigi's cookie never reaches Mario |
| Secure | yes when NODE_ENV === "production" (staging and production) |
| Path | / |
| Max-Age | 30 days |
Resolution:
- No cookie on
GET→200withitems: []. No insert, noSet-Cookie - Cookie UUID bound to the resolved restaurant → that cart
- Missing, foreign, or other-restaurant UUID → ignore, create a new cart for the resolved tenant, rotate the cookie (no leak)
- First mutation without a cookie → create a cart and set the cookie
DELETE /storefront/cartempties lines; the cookie may remain
Public API
Cart routes use the same hostname tenant gate as GET /storefront/menu.
- Unknown or inactive restaurant →
404(Restaurant not found) - Database failure during hostname lookup →
503 - Query/body
restaurantIdis ignored - Empty cart →
200withitems: [], not 404
| Method | Path | Body |
|---|---|---|
GET | /storefront/cart | — |
POST | /storefront/cart/items | { menuItemId, quantity? } (default 1) |
PATCH | /storefront/cart/items/:itemId | { quantity } (0 deletes the line) |
DELETE | /storefront/cart/items/:itemId | — |
DELETE | /storefront/cart | — |
:itemId is the cart line UUID, not menuItemId.
Example (Luigi, two Margherita):
{
"restaurantId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
"items": [
{
"id": "<cart_item uuid>",
"menuItemId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5",
"name": "Margherita",
"quantity": 2,
"unitPriceCents": 850,
"lineTotalCents": 1700
}
],
"itemCount": 2,
"subtotalCents": 1700
}Inactive or cross-tenant items cannot be added (404). If such a line is already stored, GET omits it and PATCH/DELETE on that line returns 409. Inactive seed items (Secret Calzone, Secret Lasagna) never appear in a public cart.
There is no dashboard cart API.
Storefront
The tenant start page SSR-loads the cart (API_INTERNAL_URL + X-ByteBite-Tenant-Host + incoming Cookie, cache: "no-store"). Browser mutations use same-origin /api with credentials so bb_cart stays host-only.
The technical host storefront.bytebite.test stays a placeholder and does not load a cart.
Not in this slice
Payment providers, delivery, variants/extras/allergens, minimum order, delivery fee, object storage, dashboard cart, production deploy. Weekly hours and checkout gating: Opening hours. Checkout lives in Checkout & orders.