Skip to main content

Checkout Settings

EverShop 2.2.1 adds a guest-checkout toggle to store settings, and with it a small pattern worth understanding: a setting that changes what a shopper is allowed to do is enforced in two independent places — once at the page level for the browser experience, and once in order validation so a direct API call cannot slip past it.

This page covers both checkout settings and the extension seam for adding your own pre-order checks.

allowGuestCheckout

Whether a shopper may place an order without a customer account.

Resolution order

modules/checkout/services/checkoutSettings.ts is the single reader:

import { toBoolean } from '@evershop/evershop/lib/util/coerce';
import { getConfig } from '@evershop/evershop/lib/util/getConfig';
import { getSettingSync } from '@evershop/evershop/setting/services';

export function getAllowGuestCheckout(): boolean {
return toBoolean(
getSettingSync<unknown>(
'allowGuestCheckout',
getConfig('checkout.allowGuestCheckout', true)
)
);
}

Three layers, highest priority first:

SourceWhere it livesNotes
Admin settingsetting table, key allowGuestCheckoutSet from Settings → Store Settings → Checkout. Wins whenever present.
Legacy configconfig/default.jsoncheckout.allowGuestCheckoutDeclared in the checkout module's configurationSchema processor as a boolean. Used only when the setting row is absent.
DefaultHard-coded trueGuest checkout is allowed on a fresh install and on upgrade — the toggle is opt-out, so 2.2.1 changes nothing for existing stores.

Two implementation details that matter if you call this yourself:

  • It is synchronous and cache-only. getSettingSync reads the in-memory settings cache, never the database. That is what makes it safe to call from the order validator and from request handlers without an await.
  • The result is coerced. The setting table stores scalars as strings, so the raw value may be 'false', '0', 'true' or '1', and the config fallback may be a real boolean. toBoolean normalises all of those. Never compare the raw setting value against true yourself.

Enforcement point 1 — the checkout page

modules/checkout/pages/frontStore/checkout/index.ts redirects an anonymous shopper to login, carrying a return URL:

export default async (request: EvershopRequest, response, next) => {
const customer = request.getCurrentCustomer();
const cart = await getMyCart(request.sessionID || '', customer?.customer_id);
if (!cart) {
response.redirect(302, buildUrl('cart'));
return;
}
const items = cart.getItems();

if (items.length === 0 || cart.hasItemError()) {
response.redirect(302, buildUrl('cart'));
} else if (!customer && !getAllowGuestCheckout()) {
response.redirect(
302,
`${buildUrl('login')}?redirect=${encodeURIComponent(request.originalUrl)}`
);
} else {
setPageMetaInfo(request, {
title: translate('Checkout'),
description: translate('Checkout')
});
next();
}
};

This handles the normal browser flow. It does not handle a shopper who logs out while sitting on the checkout page — that is client-side, with no page load. The page component covers that case separately with a useEffect that watches the customer and only fires on a genuine sign-out (was authenticated → is not), so a first render as a guest does not bounce anyone.

Neither of these is a security boundary. They are user experience.

Enforcement point 2 — the order validator

The actual gate is a validation rule in modules/checkout/services/orderValidator.ts:

{
id: 'guestCheckout',
func: (cart: Cart) => {
if (getAllowGuestCheckout()) {
return true;
}
return Boolean(cart.getData('customer_id'));
},
errorMessage: 'You must be logged in to place an order'
}

This runs inside orderCreator's transaction, on every path that creates an order — the REST createOrder endpoint included. An API client that skips the page entirely and posts straight to the order endpoint still gets rejected:

Order validation failed: You must be logged in to place an order

The rule is genuinely an authorization check rather than a spoofable one because cart.customer_id is populated from the session by the cart-checkout handler (via request.getCurrentCustomer()), never from client input. A guest cannot set customer_id on their own cart.

The general pattern: page-level checks are for humans, validation rules are for correctness. If a setting restricts what may be placed as an order, add a validation rule for it. A redirect alone is decorative.

GraphQL exposure

The flag is exposed on the Setting type, not on a CheckoutSetting type — the folder is named CheckoutSetting/ but it contains a type extension:

extend type Setting {
showShippingNote: Boolean
allowGuestCheckout: Boolean
}

So the storefront query is:

query Query {
loginUrl: url(routeId: "login")
setting {
showShippingNote
allowGuestCheckout
}
}

modules/checkout/graphql/types/CheckoutSetting/CheckoutSetting.resolvers.js resolves allowGuestCheckout through getAllowGuestCheckout(), so the GraphQL field goes through exactly the same setting → config → default chain as the server-side checks. Both fields are on the non-admin schema, so the storefront can read them.

showShippingNote

Whether the checkout page and the order-success page offer a free-text "order note" field.

Unlike guest checkout this one is config-only — there is no admin setting and no setting row. It is declared in the checkout module's configurationSchema processor and read straight from config:

showShippingNote: () => getConfig('checkout.showShippingNote', true)

To turn it off:

{
"checkout": {
"showShippingNote": false
}
}

The flag is consumed twice on the checkout page — once in the sticky summary rail on large screens and once inline above the place-order button below the lg breakpoint, because the rail is hidden there. Both instances edit the same note value. It is also read by the order-success page's shipping note block.

Adding your own pre-order checks

Order validation is a registry-backed rule list. Two functions form the public seam, both exported from @evershop/evershop/checkout/services:

FunctionSignatureUse
addOrderValidationRule(rule: Validator<Cart>) => voidRegister a rule. Call from bootstrap.ts only.
validateBeforeCreateOrder(cart: Cart) => Promise<{ valid: boolean; errors: string[] }>Run every rule. Core already calls it inside orderCreator; call it yourself only to pre-flight a cart (e.g. to grey out a button).

A rule is a plain object:

import { addOrderValidationRule } from '@evershop/evershop/checkout/services';

export default () => {
addOrderValidationRule({
id: 'maxOrderValue',
func: (cart) => {
const total = cart.getData('grand_total');
return typeof total !== 'number' || total <= 10000;
},
errorMessage: 'Orders above 10,000 must be placed by contacting sales'
});
};

Rules to follow:

  • Register from bootstrap.ts. addOrderValidationRule wraps addProcessor('orderValidator', ...), and the registry is locked once bootstrap finishes. Calling it from a middleware throws.
  • id is a key, not a label. Rules are stored in a Map keyed by id, so registering the same id twice replaces the earlier rule. That is the supported way to override a core rule — re-register guestCheckout or billingAddress with your own func.
  • func may be sync or async. All rules run concurrently through Promise.allSettled, so a slow rule does not serialize the others. It also means rules must not depend on each other's side effects.
  • A rule that throws counts as a failure, and its message becomes "<errorMessage> (exception occurred)". Prefer returning false over throwing so the shopper sees your real message.
  • Failures are joined, not raised individually. orderCreator throws Order validation failed: <errors joined> and the surrounding transaction rolls back. Write errorMessage as something a shopper can act on.

The core rules

For reference, these ship in initialValidators:

Rule idRejectsError message
checkCartErrorA cart carrying any errorCart has errors
checkEmptyA cart with no itemsCart is empty
shippingAddressA shippable cart with no shipping addressShipping address is required
shippingMethodA shippable cart with no selected methodShipping method is required
billingAddressA cart with a total above zero and no billing addressBilling address is required
guestCheckoutAn anonymous cart when guest checkout is disabledYou must be logged in to place an order

shippingAddress and shippingMethod both short-circuit to valid when the cart is no_shipping_required (a fully virtual cart). billingAddress short-circuits when grand_total is zero or below — see Zero-Total Checkout for why.

See also



Support us


EverShop is an open-source project that relies on community support. If you find our project useful, please consider sponsoring us.