Going live
Use this checklist before accepting the store's first real order. It matches the checks bin/plugin kahunacart check runs. Every finding names its check, so a red line in the terminal or on the admin's Health screen maps to a section below.
Before you begin
- Access to the site's command line, so you can run
bin/plugin kahunacart check. - A backup, if you plan to run
resetafterwards. Copyuser/and the database, or export a store bundle.
Run the doctor
Run it from the site root:
bin/plugin kahunacart check # every check, with a fix for each one that is not green
bin/plugin kahunacart check --offline # skip the network probes (provider credentials, webhook pings)
bin/plugin kahunacart check --json # the same findings as JSON, for a deploy script
bin/plugin kahunacart check --report # the redacted support report, to paste into a ticket
The exit code is the verdict: 0 when every check passes or only warns, 1 when any fails. A deploy pipeline can refuse to go live on a red doctor.
The command is safe to run as often as you like. It writes nothing to the store, except that with network checks on, every active outbound webhook subscription is pinged and the ping is recorded in the delivery log.
A store with demo.readonly on never runs the network checks; check and the Health screen answer as if --offline had been given. See Demo store.
The admin's Health screen runs the same checks, and its Copy support report button hands over the same redacted text.
Before the checks
Two things the doctor cannot do for you, because both destroy data on purpose.
Wipe the test orders and keep everything else
reset deletes the orders, with their lines, payments, refunds, shipments, history, download grants, stock holds, credit notes, gift cards, report rollups, the job queue and both webhook logs. With --customers it also deletes the customer rows with their sign-in accounts, addresses, wishlists and reviews. It never touches a product, a category, a tax or shipping table, a coupon, a payment method or a setting, and it prints every table it is about to empty with a row count before asking.
bin/plugin kahunacart reset --orders --customers --keep-settings
bin/plugin kahunacart reset --orders --keep-settings --keep-numbers # orders only, and keep the order-number sequence where it is
bin/plugin kahunacart reset --orders --customers --keep-settings --force # no question, for a script
--keep-settings is required. The order-number sequence restarts at order_number_start unless you pass --keep-numbers. Coupon usage counters go back to zero, and the merchant-notification dedupe stamps are cleared so a low-stock warning can fire again.
The command is refused on a store with demo.readonly on. See CLI → reset.
Look at every email and document before a customer does
The Health screen's Previews section renders each order email and each PDF against a real completed order and sends nothing. See Orders and the admin.
The checks
Each heading below is a check id.
Database
Which engine the store runs on, its version, and whether it refuses bad data instead of truncating it.
Fails when MySQL or MariaDB is not running in strict mode, so a value too long for its column is silently cut; or when the SQLite file sits under the document root with nothing denying HTTP access to its directory.
Fix. Add STRICT_TRANS_TABLES to sql_mode in my.cnf (MySQL 5.7+ and MariaDB 10.2+ default to it), or set database.mysql.options to send SET sql_mode on connect. For SQLite, move the file out of the document root with database.sqlite.path, or add the nginx or Caddy deny rules from Hardening → Deny the data directories. Apache gets an .htaccess in the directory for free. Then curl -sI https://your-site/user/data/kahunacart/db/kahunacart.sqlite must not answer 200.
Migrations
The schema has to match the plugin version running against it.
Fails when any migration step is pending.
Fix. Run bin/plugin kahunacart migrate. bin/plugin kahunacart migrate --status lists the steps first. On MySQL and PostgreSQL put it in the deploy pipeline. On SQLite the plugin applies it on the next request while database.auto_migrate is at its default, so this only turns red there when that has been switched off. See CLI → migrate.
Scheduler
Confirmation emails, download grants, webhook deliveries and every sweep are queued work, drained by bin/plugin kahunacart work on Grav's scheduler every minute. If the scheduler is not in cron, all of it waits forever. This is the most common cause of a missing download email.
Fails when the worker has never finished a tick on this store, or its last finished tick is more than ten minutes old. Warns when the worker is ticking but Grav reports no crontab entry.
Fix. bin/grav scheduler --install prints the crontab line. Add it, then run bin/grav scheduler --run once. The check goes green on the first finished tick. If the scheduler runs but the worker never finishes, bin/grav scheduler --jobs confirms kahunacart-jobs is listed and enabled, and bin/plugin kahunacart work by hand shows the failure. See CLI → Run the scheduler.
Mail transport
Every order email goes through Grav's Email plugin. It has to be installed, enabled and pointed at a transport that can deliver.
Fails when the Email plugin is missing or disabled, the transport is none, SMTP is chosen with no server, or sendmail is chosen with a binary that is not executable. Warns when no From address is set, which most receiving servers treat as spam.
Fix. Run bin/gpm install email. On the Email plugin's settings page choose SMTP for most hosts, sendmail where the server has a local MTA, or one of the provider plugins (Mailgun, Postmark, SendGrid). Set a From address on your own domain. Then send yourself one with Send an email again on any order. See Emails.
Worker environment
The background worker has to send email with the same settings as the site. On a store with a user/env/<host>/ folder, Admin Next saves settings into user/env/<host>/config/ instead of user/config/, and only a process running as that environment reads them. A web request runs as that environment because of its host name. The worker runs from cron on the command line, which is the cli environment unless something pins it, so it reads user/config/ alone and sends order emails with whatever mail settings are there. With no SMTP login there, the mail server answers "relay access denied", while email sent from the site itself works.
The check reads security.site_urls and system.custom_base_url for the store's host names and looks for plugins/email.yaml and plugins/kahunacart.yaml in each host's environment folder. It lists the settings in them that differ from what the worker loads (user/config, plus the worker's own environment folder when it runs as one), naming the keys and never their values. A store on two addresses whose folders hold the same settings passes with the worker pinned to either one. The worker records the environment it ran under on every tick, so the check knows instead of guessing. Before the first tick it assumes cli, or whatever GRAV_ENVIRONMENT the root .env sets.
Fails when the email settings for one of the store's hosts sit in an environment folder the worker does not run as. Warns when only KahunaCart's own settings do. The same problem shows as an alert on the admin dashboard.
Fix. Pick one:
- Move the settings into
user/config/plugins/and delete them from the host's folder. This is the simplest answer for a store on one address, because every process then reads the same file. If the host folder holds nothing else, removing it makes Admin Next save intouser/config/from then on. - Pin the environment in the
.envfile in the Grav root:GRAV_ENVIRONMENT=shop.example.com. Grav reads it before it picks an environment, on the web and on the command line alike. It also pins every web request to that folder, which is right for a store on one address and wrong for a Grav install serving several sites. - Pin it on the cron line:
* * * * * cd /path/to/grav && GRAV_ENVIRONMENT=shop.example.com php bin/grav scheduler 1>> /dev/null 2>&1. The variable reachesbin/plugin kahunacart workwhen the scheduler starts it.bin/grav scheduler --env=shop.example.comloads the folder for the scheduler, but an older Grav that does not pass its environment on to the commands it starts still runs the worker ascli, so use the variable. A cron line that runs the worker directly takes--env:php bin/plugin kahunacart work --env=shop.example.com.
The check goes green on the next tick after any of them. KahunaCart does not switch the worker into the host's environment on its own, because a staging copy whose config still names the live address would then send its test orders' email through the live mail account. See Troubleshooting.
Download roots
Every directory in downloads.allowed_roots has to exist, and every product file has to resolve inside one of them. Otherwise customers get "temporarily unavailable".
Fails when an entry in downloads.allowed_roots is not a directory (it is dropped silently, which narrows the allowlist), or when a product file resolves outside every allowed root or is missing from disk. Warns when the list is set but does not name the files root the admin uploads into.
Fix. Create the directory, fix the path (a Grav stream such as user-data://files works too), or remove the entry. bin/plugin kahunacart downloads:check lists every file that cannot be served. Move them under an allowed root, add their directory to the list, or upload them again. See Hardening → Restrict where downloadable files may live.
Trusted proxies
Behind Cloudflare, a load balancer or an ingress, security.trusted_proxies has to name the proxies. Otherwise every visitor shares one rate-limit bucket.
Fails when an entry does not parse as an IP address or a CIDR range. Warns when Grav's system.reverse_proxy_setup is on and the list is empty.
Fix. List the proxy's addresses or ranges, in either IP family. Cloudflare publishes its ranges; a load balancer inside your own network is usually one entry such as 10.0.0.0/8. See Hardening → Trusted proxies.
Site address
Links in emails are built only from an address the store names, because the address a request arrived on comes from a header the visitor can set. system.custom_base_url or security.site_urls has to name it.
Fails when neither is set, so a customer's sign-in, set-password and cart links are not sent and every other emailed link arrives without the site address, or when an entry in security.site_urls is not a web address. Warns when the address emails use is a development one, such as localhost or a .test name.
Fix. Open the store admin over HTTPS at the address customers use. The address is filled in as Site Addresses (Settings → Security) the first time an administrator opens the dashboard or KahunaCart's admin page, and the sign-in, set-password and cart links held back so far go out on the worker's next run. Or set security.site_urls (or system.custom_base_url) yourself, such as https://shop.example.com, and add any other address customers reach the store on. See Hardening → Site address for emailed links.
Store route
A shop mounted at the site root (route: '/') answers /cart, /account, /search and its other addresses itself, so a top-level Grav page with one of those names can't be opened any more, and an add-on older than the first version made for the root has pages and links that break. The check runs only while the shop is at the root. See Storefront → Mounting the shop at the site root.
Fails when an add-on that builds links for the shop is older than the first version that works at the root: Subscriptions below 1.1.8, Licenses below 1.0.17, Bookings below 1.1.8, Rentals below 1.0.7, Newsletters below 1.1.11 or Polar below 1.1.11. Each one is named with the version it has and the one it needs, and only add-ons that are installed and switched on count. The same problem shows on the dashboard as an alert. Warns when a top-level page folder has the name of one of the shop's addresses (03.cart is /cart), when a top-level page folder is named like an address the shop only answers below (03.product still opens as /product, but the shop answers /product/{slug}, so the page's own child pages may be hidden), or when the API plugin's route is /api, which takes /api/... from the storefront JSON API. Addresses for parts of the shop you've switched off (a store with cart: false has no /cart) are left out.
Fix. Update the add-ons, or mount the shop under its own route such as /shop until you can. Rename or move a page that has the same name as one of the shop's addresses. A store that uses the storefront JSON API keeps the shop under its own route.
Store status
A store that is coming soon or paused takes no orders except from signed-in staff, so the owner who looks at it sees it working. Left that way after a move or a test order, it sells nothing. See Configuration → Store status.
Fails when never. It is at most a warning, so a deploy script that refuses a red doctor can still deploy a store that isn't open yet. Warns when the store is coming soon or paused, naming the status and what shoppers are told.
Fix. Open it when you're ready to sell: Store status on the General tab of KahunaCart's settings, the switch on the dashboard, or bin/plugin kahunacart store --status=open.
Provider credentials
Every registered payment provider is asked whether its credentials work, so you find a rotated API key before a customer does.
Fails when a provider's connection check fails. Warns when no provider is registered at all. Skipped with --offline.
Fix. The finding carries the provider's own message. Open the provider's settings page, correct the key, and press Check connection on the Payments screen, which runs the same check. A store that takes bank transfer or cash enables the built-in offline method with offline.enabled. See Payment providers.
Webhook reachability
The inbound webhook URL providers are given has to be https on a public host, and every active outbound subscription has to answer a ping.
Fails when the inbound URL is not https, or an active outbound subscription does not answer a ping with a 2xx. Warns when the inbound URL is on a host the internet cannot reach (localhost, a .test or .local domain). Skipped for the pings with --offline.
Fix. Serve the store over https, set system.custom_base_url to the public https address, and register each provider's webhook again with bin/plugin kahunacart webhooks --register --provider=<slug>. For an outbound subscription, fix the receiving end and press Ping on the Webhooks screen. The subscription stays paused until a ping succeeds. See Outbound webhooks and CLI → webhooks.
Agentic readiness
When product feeds, the storefront MCP endpoint or UCP is on, everything it needs has to be in place. The check runs only while agentic.enabled is on and reports nothing while it is off. With the master switch on and all three parts off, it passes with a sentence saying so. See Agentic commerce.
Fails when UCP is on and /.well-known/ucp still answers 404 because of a readiness problem: the site is not served over HTTPS, Grav sits in a subdirectory rather than at the host root, policies.terms_url or policies.privacy_url is empty, there is no signing key yet, the cart is switched off, or ucp.accept_unknown_platforms is off with nothing in ucp.platforms. The finding lists every one. Warns when no feed file has been built yet, or the oldest is more than twice feeds.max_age old; when no return policy is published because policies.refund_url is empty; or when ucp.google_pay.environment is TEST while Stripe is in live mode, so agent checkouts send test tokens a live account refuses.
Fix. Work through the list. The Agentic commerce screen in the store admin shows the same problems, and its Rotate signing key button makes the first key. So does bin/plugin kahunacart agentic:keys, and the worker makes one on its next tick while UCP is on. For stale feeds, check that the worker runs, then press Rebuild feeds or run bin/plugin kahunacart feeds:build. Set the returns page under Settings → Agentic Commerce, and switch Google Pay to PRODUCTION once Google approves your merchant id. See UCP checkout → Switch it on.
Semantic search
When semantic search is on, the product index has to hold vectors from the model you configured, and the last embedding run has to have finished without an error. The check runs only while search.products.semantic.enabled is on. It passes with the counts (products embedded, products waiting and the model) and the noise gate's min_margin, with when it was measured on your catalog.
Warns when the last embedding run stopped on a provider error (a wrong API key, an unknown model, a provider that was down), which it names; when nothing is embedded yet; or when the store isn't on the yetisearch engine that semantic search needs. Never fails. Without vectors the store still searches by keywords, which is what it did before you switched semantic search on.
Fix. Check the API key, base URL and model under Settings → General → Semantic Search, then run bin/plugin kahunacart search:embed, which prints the provider's error if there still is one. When nothing is embedded and there's no error, check that the job worker runs. See CLI → search:embed.
Template drift
A theme override is frozen at the version it was copied from, so a later plugin release that changes that template never reaches the site.
Fails when an override's @version stamp is lower than the shipped template's. Warns when an override carries no stamp at all. Skipped when no theme is initialised.
Fix. For each file listed, diff the theme's copy against the plugin's templates/ copy, carry the changes across, and update its @version to match. bin/plugin kahunacart templates:drift prints the same list, and the Health screen's Template drift panel shows both versions side by side. See CLI → templates:drift and Theming.
Report rollup
A rollup built before a measure existed makes every report that needs that measure scan the orders table instead of reading a day row. The figures are correct but slow.
Warns when the rollup was last built against an older set of measures, usually because the store upgraded past the migration that added cost of goods and nobody has rebuilt since. It never fails.
Fix. Run bin/plugin kahunacart reports:rebuild. It reads the orders table once, so run it out of hours on a large store. The dashboard shows the same thing as The report rollup needs rebuilding. See Reports and the rollup and CLI → reports:rebuild.
After the checks
- Turn
demo.readonlyoff if the store was a demo. The reset command and every admin write are refused while it is on. See Demo store. - Selling in more than one currency? Check that every currency has a payment method that takes it (
bin/plugin kahunacart statuslists them and warns about any that nothing takes), that every product you mean to sell has a price in each currency, that shipping methods, coupons and the order minimum have amounts in each, that the catalog index has been rebuilt since you added a currency, that the switcher is in your theme's header, and, if a CDN caches your pages, that it followsCache-Controland bypasses its cache when thekc_currencycookie is present. See Currencies. - Read Hardening once, end to end. The doctor does not cover PHP-FPM sizing, opcache or the CDN's treatment of
Cache-Control; those are in Performance and the last section of Hardening. - Keep
bin/plugin kahunacart checkin the deploy pipeline. Its report is the first thing to attach to a support request.
Related
- CLI
- Troubleshooting
- Hardening
- Performance
- Demo store
- Agentic commerce
- Currencies
- Selling through more than one company: a UK company in GBP and an EU company in EUR, with separate invoice books and payment method accounts, and the company fixed on each issued order.