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
| User | Surface | Jobs |
|---|---|---|
| Shopper | Storefront | Browse, build a basket, choose pickup or delivery, pay, answer substitution requests, check in at the curb |
| Picker | Fulfillment | Pick items, record weights, propose substitutions |
| Order manager | Orders, pickup, subscriptions, messages | Advance orders, cancel, run recurring baskets, answer messages |
| Delivery coordinator | Delivery | Slots, routes, stop outcomes |
| Inventory and buying | Inventory, suppliers, purchasing | Lots, counts, recalls, purchase orders, receiving |
| Finance | Payments, reports | Refunds, voids, close, statement import |
| Admin | Settings, staff, communications | Store profile, delivery policy, invitations |
Customer and operator surfaces
- Storefront (
app/(storefront), logic infeatures/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 infeatures/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
Storeholds code, name, timezone, currency, and an active flag. GraphQL cannot create, update, or delete a Store. The only Store comes from the migration.User.storeis a required relationship. The first-user flow at/dashboard/initconnects the new user toPUBLIC_STORE_IDand creates anAdminrole with every capability.StoreSettingsis one row per Store (uniquestorerelationship). 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 callrequireSessionStore. - 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 withcanManageProducts. - Roles carry
canAccessDashboard,canManageProducts,canManageOrders,canManageInventory,canManageSuppliers,canManageDelivery,canManageUsers,canManagePayments, andcanManageOnboarding. Role fields andUser.rolecannot 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:
| Area | Lists |
|---|---|
| Store and staff | Store, StoreSettings, User, Role, StaffInvitation, StaffAccessEvent, RateLimitBucket |
| Catalog | Department, Product, ProductInformationVersion, CatalogImport, Coupon, UserCoupon, Recipe, RecipeIngredient |
| Inventory | InventoryLot, InventoryMovement, InventoryAdjustment, InventoryCount, InventoryRecall, InventoryConversion, OrderLineInventoryAllocation |
| Suppliers and purchasing | Supplier, PurchaseOrder, POItem, PurchaseReceipt, SupplierInvoice, SupplierCredit |
| Baskets and orders | Cart, CartItem, CheckoutAttempt, Order, OrderLineItem, OrderItemSubstitution, SubstitutionPreference, OrderLineFulfillment, OrderSettlement |
| Pickup and delivery | PickupSlot, DeliverySlot, ParkingSpot, DeliveryRoute |
| Payments and finance | PaymentProvider, PaymentSession, Payment, PaymentRefund, OrderLineRefund, PaymentWebhookEvent, SettlementMatch, FinancialClose |
| Customers and retention | Address, ShoppingList, ShoppingListItem, FavoriteProduct, Subscription, SubscriptionRun, LoyaltyProgram, LoyaltyTransaction, LoyaltyRedemption, BackInStockAlert, PriceAlert, NotificationPreference |
| Messages and privacy | ContactMessage, 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.
initiatePaymentSessioncreates a Stripe payment with manual capture.submitGroceryOrderre-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.resumeGroceryCheckoutandrestartGroceryCheckoutrecover 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).finalizeGroceryOrderActualsfreezes 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 throughmanageSupplierSettlement. - 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
ContactMessagerecords 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/paymenthas two entries: Stripe (pp_stripe_default, public checkout) and a local adapter (pp_test_default, not public, refused in production). Amanual.tsfile exists but is not registered, and the selectormanualis rejected. Stripe is off unlessSTRIPE_ENABLED=trueand all three Stripe keys are set. Production requires live keys unlessSTRIPE_ALLOW_TEST_MODE=true. Custom remote adapters must use an HTTPS origin listed inPAYMENT_ADAPTER_ALLOWED_ORIGINS. Changing provider records needs a user id listed inPAYMENT_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 aPaymentWebhookEvent. - 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_URLis set, the event worker posts outbox events to that HTTPS URL, signed with HMAC-SHA256 usingGROCERY_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 unlessMCP_ENABLED=true./api/completionneeds a dashboard user plusOPENROUTER_API_KEYandOPENROUTER_MODEL. - Other routes.
/api/healthchecks the database./api/store-iconserves 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.