Storefront ordering

The guest-facing menu, cart, checkout, and order-confirmation flow in current Restaurant source.

Openfront Restaurant includes a customer-facing menu and ordering app. Current routes cover the menu, item pages, checkout, order confirmation, sign-in, profile, addresses, and order history.

Current routes

  • / for the menu, popular items, house picks, and cart entry
  • /menu/[id] for an item page with modifier selection
  • /checkout for contact, pickup or delivery, tip, payment, and review
  • /order/confirmed/[id] for confirmation
  • /account, /account/profile, /account/addresses, /account/orders, and /account/orders/details/[id]

The storefront lives under app/(storefront) and features/storefront. Keystone models and custom GraphQL operations are the backend contract. See External storefronts for the full route and operation list.

What drives the storefront

  • StoreSettings supplies the name, logo, hero copy, hours, locale, currency, tax rate, delivery rules, pickup discount, and rating display. If no settings exist, the home page shows a "not available yet" message.
  • MenuCategory, MenuItem, and MenuItemModifier supply menu content and customization. Sold-out items show as unavailable.
  • Cart and CartItem hold checkout state.
  • PaymentCollection and PaymentSession hold the selected provider session and a server-priced checkout quote.
  • RestaurantOrder, OrderItem, Payment, and (for successful payments) Receipt are created when checkout completes.

Ordering rules enforced by the server

Each time an item is added, the server checks:

  • the item is available;
  • the restaurant is open, allowing for the ordering cutoff minutes set in store settings;
  • the item and its category are inside their meal-period service windows, if set;
  • modifier selections meet the group's required, minimum, and maximum rules.

Store settings can also set a maximum number of active online orders. When the limit is reached, new checkouts are refused with a message to retry later.

Current checkout sequence

Create or resume a cart

createActiveCart returns the cart and a signed accessProof. The server stores the cart ID in _restaurant_cart_id and the proof in _restaurant_cart_proof (HTTP-only, 7 days). Cart operations check either the signed-in owner or a valid proof.

Add items and contact details

The cart stores items, modifiers, special instructions, contact data, pickup or delivery, delivery address, and tip percent. If the guest is not signed in, submitting contact details creates a customer account with a random password, signs the guest in, and links the cart. If the email already has an account, checkout asks the guest to sign in.

Validate delivery and calculate totals

Delivery must be enabled, and the address country and postal code must match the store's allowlist. The subtotal must meet the delivery minimum. Totals (tax, tip, pickup discount, delivery fee) are calculated on the server from current cart and StoreSettings data.

Select and confirm payment

initiatePaymentSession creates a session for the chosen provider and freezes a priced quote. Stripe confirms the card in the browser. PayPal uses its approval flow. The manual provider records a pending payment. A guest can cancel a pending session with cancelCheckoutPaymentSession.

Create the order

completeActiveCart checks the provider status and captures if needed. Then, in one serializable transaction, it creates the RestaurantOrder, its OrderItem rows, the Payment, a sale Receipt when paid, an audit event, and a queued confirmation email. It then creates kitchen tickets. Card and PayPal orders start as sent_to_kitchen. Manual-payment orders start as open.

Checkout mutation

mutation CompleteRestaurantCart($cartId: ID!, $paymentSessionId: ID) {
  completeActiveCart(cartId: $cartId, paymentSessionId: $paymentSessionId) {
    id
    orderNumber
    status
    secretKey
  }
}

Calling it again for a cart that already has an order returns that order. If a provider webhook confirms payment first, the webhook handler completes the same checkout on the server.

Use the generated schema.graphql from the same revision. Do not replace this workflow with generated list CRUD from an untrusted client.

Order access

getCustomerOrder(orderId, secretKey) returns an order to its signed-in customer. A secretKey is only generated for orders placed without a customer account. Treat it as a credential: keep it out of analytics, logs, referrers, and shared caches.

Recovery

If payment succeeded but no order was placed, staff can see the attempt in the POS "Checkout recovery" panel (unplacedCheckoutAttempts). They can then recover it into an order (recoverCheckoutPayment) or cancel the session.

Current limitations

Provider capture happens before the order transaction. If the order write fails after capture, the attempt shows up in checkout recovery and needs staff action.

  • There are no /api/categories, /api/menu-items, or /api/orders/* REST routes. External clients use GraphQL.
  • The storefront has no reservations, waitlist, or gift-card purchase flow.
  • Discount codes cannot be entered at checkout.
  • Order confirmation emails are queued, not sent automatically. Staff send them from the notification queue.
  • Provider buttons in the UI do not prove that refunds, webhooks, or reconciliation work for that provider.

Where to go next

On this page