Openfront Grocery

Grocery storefront and operator platform for catalog, lots, purchasing, picking, substitutions, pickup, delivery, payments, and recurring baskets.

Source · Live site · Catalog

Openfront Grocery is a self-hostable grocery storefront and store-operations app. Shoppers browse departments and products, build a basket, keep lists and favorites, choose pickup or delivery, pay with Stripe, and track their order. Store staff use /dashboard/platform/* pages for products, merchandising, lot inventory, suppliers, purchasing, picking, substitutions, pickup, delivery, payments, recurring baskets, staff, reports, and customer messages.

It runs on Next.js 16, React 19, Keystone 6, Prisma, and PostgreSQL. Start with Getting started.

The app serves one store per deployment. A migration creates the Store row store_juniper, and production requires PUBLIC_STORE_ID=store_juniper. Data is scoped to that Store, but there is no organization or multi-location model.

Who uses what

UserSurfaceJobs
ShopperStorefrontBrowse, build a basket, choose pickup or delivery, pay, answer substitution requests, check in at the curb
PickerFulfillmentPick items, record weights, propose substitutions
Order managerOrders, pickup, subscriptions, messagesAdvance orders, cancel, run recurring baskets, answer messages
Delivery coordinatorDeliverySlots, routes, stop outcomes
Inventory and buyingInventory, suppliers, purchasingLots, counts, recalls, purchase orders, receiving
FinancePayments, reportsRefunds, voids, close, statement import
AdminSettings, staff, communicationsStore profile, delivery policy, invitations

Customer and operator surfaces

  • Storefront (app/(storefront), logic in features/storefront): home, departments, products, specials, delivery and pickup times, basket, checkout, order tracking, guest order access, account, lists, favorites, recurring baskets, help, and policies. See External storefronts for the route table.
  • Operator platform (app/dashboard/(admin)/platform, logic in features/platform): seventeen task pages gated by role capabilities. See External dashboards for the route table.
  • Generic admin (features/dashboard): Keystone list and item pages at /dashboard/<list-key> for records without a dedicated page. They follow the same list access rules.

Store model and tenancy

  • Store holds code, name, timezone, currency, and an active flag. GraphQL cannot create, update, or delete a Store. The only Store comes from the migration.
  • User.store is a required relationship. The first-user flow at /dashboard/init connects the new user to PUBLIC_STORE_ID and creates an Admin role with every capability.
  • StoreSettings is one row per Store (unique store relationship). It holds the profile, hours, rolling fulfillment policy, delivery postal prefixes, minimums, fees, tax rate, logo, and brand hue.
  • Signed-in access uses currentMembershipStoreId. It reads the user's current Store from the database on each request and denies access when the account is erased or the Store is inactive. Custom commands call requireSessionStore.
  • Public reads (products, departments, coupons, availability, settings) use PUBLIC_STORE_ID. Shoppers see only published products and active departments. Cost, supplier, and internal product fields are readable only with canManageProducts.
  • Roles carry canAccessDashboard, canManageProducts, canManageOrders, canManageInventory, canManageSuppliers, canManageDelivery, canManageUsers, canManagePayments, and canManageOnboarding. Role fields and User.role cannot be changed through generic GraphQL. Staff access changes go through the staff commands.

Data models

The model registry in features/keystone/models/index.ts defines 66 lists:

AreaLists
Store and staffStore, StoreSettings, User, Role, StaffInvitation, StaffAccessEvent, RateLimitBucket
CatalogDepartment, Product, ProductInformationVersion, CatalogImport, Coupon, UserCoupon, Recipe, RecipeIngredient
InventoryInventoryLot, InventoryMovement, InventoryAdjustment, InventoryCount, InventoryRecall, InventoryConversion, OrderLineInventoryAllocation
Suppliers and purchasingSupplier, PurchaseOrder, POItem, PurchaseReceipt, SupplierInvoice, SupplierCredit
Baskets and ordersCart, CartItem, CheckoutAttempt, Order, OrderLineItem, OrderItemSubstitution, SubstitutionPreference, OrderLineFulfillment, OrderSettlement
Pickup and deliveryPickupSlot, DeliverySlot, ParkingSpot, DeliveryRoute
Payments and financePaymentProvider, PaymentSession, Payment, PaymentRefund, OrderLineRefund, PaymentWebhookEvent, SettlementMatch, FinancialClose
Customers and retentionAddress, ShoppingList, ShoppingListItem, FavoriteProduct, Subscription, SubscriptionRun, LoyaltyProgram, LoyaltyTransaction, LoyaltyRedemption, BackInStockAlert, PriceAlert, NotificationPreference
Messages and privacyContactMessage, CustomerMessage, CustomerPrivacyRequest, CustomerBatchRequest, GroceryOutboxEvent

Order status values are pending, picking, packed, out_for_delivery, delivered, delivery_failed, delivery_refused, returned, and cancelled. Pickup readiness is tracked in order metadata, not as its own status value.

Workflows in the code

  • Checkout. initiatePaymentSession creates a Stripe payment with manual capture. submitGroceryOrder re-reads products, lots, the chosen slot, the coupon code, the delivery policy (postal prefix, minimum, fee), and the payment session. It locks lots in expiry order, allocates stock, and writes the order with line snapshots. Guests get a signed order token. resumeGroceryCheckout and restartGroceryCheckout recover an interrupted checkout.
  • Picking and final sale. Staff advance orders through picking, packed, and ready for pickup. They record actual weights or quantities per line (resolveOrderLineFulfillment) and record or propose substitutions. The customer accepts or declines proposals on the order page (respondToOrderLineSubstitution). finalizeGroceryOrderActuals freezes the picked goods and captures payment. If the final total is higher than the authorization, the customer must approve a new authorization (authorizeOrderAdjustment).
  • Pickup and delivery. Slots come from a rolling policy in store hours plus dated slot records. Customers check in for curbside pickup and pick a parking spot. Staff build delivery routes from orders, resolve each stop with proof and condition notes, and reschedule returned deliveries.
  • Inventory. Lots have quantity, expiry, condition, and safety status. Staff adjust lots, run counts and recounts, convert cases to units, and hold, quarantine, recall, or release lots. Recalls record notices and dispositions and close only after affected orders have a delivered notice.
  • Purchasing. Purchase orders move draft → sent → confirmed, and any of these can be cancelled. Receiving creates lots with lot number, expiry, and condition. Supplier invoices, claims, debits, and settlement are recorded through manageSupplierSettlement.
  • Refunds and reports. Staff refund by order line or by amount, void an authorization, and replay stored webhook events. Reports close a trading period, import a settlement statement CSV, review matches, and export the close.
  • Recurring baskets and rewards. Customers create, pause, resume, skip, and cancel recurring baskets. Due runs prepare a basket for the customer to check out; they do not place an order on their own. Staff can configure one loyalty program; customers redeem points.
  • Messages and privacy. The Help page creates ContactMessage records for the staff inbox. Customer emails are queued as outbox events. Customers request data export or erasure; staff review erasure requests.

Named GraphQL operations

These are custom roots added in features/keystone/mutations/index.ts (also in schema.graphql).

Public and customer queries: groceryCart, customerOrderSubstitutions, clippedCoupons, activeCartPaymentProviders, guestGroceryOrder, publicGroceryCoupons, publicGroceryStorefrontSettings, publicGroceryProducts, publicGroceryProduct, publicGroceryAvailability, availablePickupSlots, pickupSlotsByDate, availableParkingSpots, getGroceryLoyalty, groceryCustomerPrivacy, redirectToInit.

Customer commands:

  • Basket: addItemToGroceryCart, updateGroceryCartItem, removeItemFromGroceryCart, clearGroceryCart, mergeGuestGroceryCart, updateGrocerySubstitutionPreference, reorderGroceryOrder.
  • Lists and favorites: createCustomerShoppingList, deleteCustomerShoppingList, addToShoppingList, removeFromShoppingList, updateShoppingListItemQuantity, toggleShoppingListItemChecked, addShoppingListToCart, saveFavoriteProduct.
  • Alerts: requestGroceryBackInStockAlert, requestGroceryPriceAlert.
  • Checkout and orders: initiatePaymentSession, submitGroceryOrder, resumeGroceryCheckout, restartGroceryCheckout, requestGuestOrderAccess, respondToOrderLineSubstitution, authorizeOrderAdjustment, customerCheckIn, guestCustomerCheckIn, releaseParkingSpot.
  • Account: registerGroceryCustomer, updateActiveUser, updateActiveUserPassword, updateGroceryNotificationPreferences, manageGrocerySubscription, redeemGroceryLoyalty, requestGroceryCustomerPrivacy, exportGroceryCustomerData, acceptGroceryStaffInvitation, submitGroceryContactMessage.

Operator projections: groceryPlatformOrders, groceryPlatformFulfillment, groceryPlatformDelivery, groceryPlatformPickup, groceryPlatformInventory, groceryPlatformSuppliers, groceryPlatformPurchasing, groceryPurchasingOptions, groceryPlatformMerchandising, groceryPlatformCustomers, groceryPlatformCustomer, groceryPlatformPayments, groceryPlatformSettings, groceryDeliveryRescheduleSlots, groceryRecurringOverview, groceryPaymentWebhookRecovery, groceryStaffDirectory, getGroceryOrderRefundLines, groceryFinancialCloses, groceryFinancialCloseExport, groceryOutboxStatus.

Operator commands:

  • Orders and fulfillment: advanceOrderFulfillment, cancelGroceryOrder, reconcileCheckoutAttempts, resolveOrderLineFulfillment, recordOrderItemSubstitution, proposeOrderLineSubstitution, finalizeGroceryOrderActuals, completeOrderHandoff.
  • Capacity and delivery: configureDeliverySlot, configurePickupSlot, configureParkingSpot, createDeliveryRouteFromOrders, updateDeliveryRouteWorkflow, resolveDeliveryStop, rescheduleReturnedGroceryDelivery, updateGroceryDeliveryPolicy.
  • Inventory and purchasing: adjustInventoryLot, manageInventoryCount, manageInventorySafety, convertInventoryCases, createPurchaseOrderDraft, removePurchaseOrderDraftItem, transitionPurchaseOrder, receivePurchaseOrder, manageSupplierSettlement, updateSupplierMinimumOrder.
  • Catalog: importGroceryCatalog, updateProductInformation.
  • Payments and finance: refundPayment, refundGroceryOrderLines, voidGroceryPaymentAuthorization, recoverGroceryPaymentCreation, recoverRevisedAuthorization, reconcilePaymentRefunds, replayPaymentWebhookEvent, handlePaymentProviderWebhook, createGroceryFinancialClose, importGrocerySettlementStatement, resolveGrocerySettlementMatch.
  • Customers, staff, and retention: provisionGroceryCustomer, inviteGroceryStaff, revokeGroceryStaffInvitation, updateGroceryStaffAccess, reviewGroceryCustomerErasure, processDueGrocerySubscriptions, configureGroceryLoyalty.
  • Store and onboarding: runGroceryOnboarding, updateGroceryStoreProfile, updateGroceryStoreLogo, updateGroceryStorefrontBrandHue.
  • Outbox: claimGroceryOutboxEvents, completeGroceryOutboxEvent, replayGroceryOutboxEvent, reconcileGroceryCustomerDelivery, deliverGroceryOutboxNow.

Some storefront and dashboard pages also use generated Keystone queries and mutations, for example departments, orders, addresses, favoriteProducts, shoppingLists, subscriptions, createProduct, updateProduct, createDepartment, updateCoupon, and updateContactMessage. List and field access rules apply to them.

Onboarding

When no users exist, every page redirects to /dashboard/init. After the first admin signs in, the dashboard shows onboarding cards and a dialog (features/platform/onboarding). It offers three templates:

  • Complete Setup: 6 departments, 3 suppliers, 8 products, 8 lots, 5 delivery windows, 5 pickup windows, 4 parking spots, the Stripe provider record, and 2 coupons (WELCOME10, FRESH5).
  • Basic Setup: 4 departments, 2 suppliers, 4 products, 4 lots, 2 delivery windows, 2 pickup windows, 2 parking spots, the Stripe provider record, and 1 coupon.
  • Custom Setup: start from the smaller seed JSON and edit it before running.

runGroceryOnboarding needs canManageOnboarding. It takes a PostgreSQL advisory lock, checks the actor still belongs to the active Store, and writes everything in one transaction. Store settings are seeded with "Replace with your …" placeholder text. The storefront does not publish those placeholders, and the store is not counted as ready until it has a real name. Edit them in Settings after onboarding.

If the Store already has a settings row, running onboarding again does not reseed. It reports what is still missing (store name, active department, published product, sellable stock, delivery windows, pickup windows) and marks the user complete only when nothing is missing. The Stripe provider record is created as installed only when the three Stripe environment variables are set. Onboarding does not create customers, orders, or a loyalty program.

Payments and integrations

  • Payments. The adapter registry in features/integrations/payment has two entries: Stripe (pp_stripe_default, public checkout) and a local adapter (pp_test_default, not public, refused in production). A manual.ts file exists but is not registered, and the selector manual is rejected. Stripe is off unless STRIPE_ENABLED=true and all three Stripe keys are set. Production requires live keys unless STRIPE_ALLOW_TEST_MODE=true. Custom remote adapters must use an HTTPS origin listed in PAYMENT_ADAPTER_ALLOWED_ORIGINS. Changing provider records needs a user id listed in PAYMENT_PROVIDER_ADMIN_USER_IDS.
  • Webhooks. POST /api/payments/webhooks/[providerCode] reads a size-limited raw body, verifies the provider signature, throttles invalid signatures per sender, and records a PaymentWebhookEvent.
  • Email. Customer messages and password resets use SMTP (SMTP_HOST, SMTP_FROM, and related settings). Without SMTP, queued messages stay pending and reset links cannot be sent.
  • Outbound events. If GROCERY_EVENT_WEBHOOK_URL is set, the event worker posts outbox events to that HTTPS URL, signed with HMAC-SHA256 using GROCERY_EVENT_WEBHOOK_SECRET (at least 32 characters).
  • Storage. Product images use an S3-compatible bucket (S3_* variables).
  • AI and MCP. /api/mcp-transport/[transport] exposes generic Keystone tools (listModels, queryData, createData, updateData, deleteData, and others) that run GraphQL as the signed-in user. It returns 404 unless MCP_ENABLED=true. /api/completion needs a dashboard user plus OPENROUTER_API_KEY and OPENROUTER_MODEL.
  • Other routes. /api/health checks the database. /api/store-icon serves the store logo as an icon.

Background workers run as separate processes: npm run worker:events (emails, stock and price alerts, outbound events), npm run worker:subscriptions (prepares due recurring baskets), and npm run worker:refund-reconcile (checks pending refunds with the provider). Each needs a staff user id from the store with the right capability; see .env.example.

On this page