Skip to content

Powered by Grav CMS + Helios Theme

Export and restore

A store bundle is the whole store in one zip: settings, catalog, pictures, digital files, customers, promotions, orders, store credit, reviews, and the data of every add-on that offers it. Restore it into a fresh install and you get the same store on a new site, ids and all. Merge it into a running store and you add what it has without losing anything. This page is for store owners moving a shop, keeping an off-site copy, or building a staging store from the live one.

A bundle carries the store, not the website. Pages, the theme, other plugins and Grav's own configuration move the usual way: copy user/, or use Grav's own backup. A restore never touches them.

Before you begin

  • Exporting needs kahunacart.export, and restoring needs kahunacart.restore. Each part of the store also needs its own permission, listed in the table below. Someone who cannot see the customer list cannot take it away in a bundle either.
  • Grav's scheduler must be running. An export or a restore started in the admin is queued and done by the background worker. See CLI → work.
  • PHP needs the zip extension to write or read a bundle, and libsodium to seal keys and secrets. The Export screen says so when either is missing.

What a bundle holds

The Export screen lists these parts as ticks under five headings, each with its row count. An add-on's parts are listed under the add-on's own name. Each part has an id, which is what the command line uses.

Part Holds Needs Restored after
settings The plugin's settings file and each provider's (with secrets taken out), the settings kept in the database, countries, checkout fields, order statuses, tax zones and rates, shipping profiles, zones, methods and pickup points, payment methods and seller accounts, saved report views and column sets, saved import mappings, and exchange rates kahunacart.settings
webhooks Outgoing webhook subscriptions and notification channels kahunacart.settings
customer_groups Customer groups kahunacart.customers.view
catalog Products, variants, options, categories, tags, attributes, manufacturers, product types, custom fields, labels, releases, kits and bundles, relations, price tiers and price lists, revisions and product history, translations, and amounts in other currencies kahunacart.products.manage settings, customer groups
media The product and category picture files kahunacart.products.manage catalog
files The digital product files customers download kahunacart.products.manage catalog
customers Customers, addresses, group memberships and custom field values. With Customer passwords ticked, their passwords and Grav accounts too kahunacart.customers.view customer groups
promotions Coupons and coupon groups, sales with their rules and tiers, and conditions, including the conditions on payment methods kahunacart.settings settings, catalog, customer groups
orders Orders with their lines, adjustments, payments, refunds, credit notes, shipments, history, changes, notices, download grants, coupon uses, payment schedules, the emails sent about them, and the order number sequence. Carts are not orders and stay behind kahunacart.orders.view catalog, customers, promotions, settings
credit Store credit accounts and their ledgers, and gift cards kahunacart.orders.view customers, orders
reviews Reviews, wishlists and back-in-stock alerts kahunacart.customers.view catalog, customers, orders

You can untick a part another one depends on. The restore report says what that meant: in a merge, orders restored without their customers become guest orders, for example.

What a bundle leaves out

  • The site secret. Every store keeps its own. Values encrypted with the old store's secret are opened on the way out and encrypted again with the new store's on the way in. Links the old store signed, such as the order links in emails customers already have, stop working. A customer asks for a fresh one from the expired-link page.
  • Runtime state. The job queue, locks, rate limits, the migration log, agent sessions, hook runs, webhook deliveries and their log, import runs, stock holds, cart links, paywall reads and the export history itself. None of it is store data, so none of it is copied. The store you restore into keeps its own: a restore does not empty these tables.
  • What the store works out from its data. The catalog index, the product and order search indexes and the report rollups are rebuilt from the restored rows once the restore finishes.
  • Carts. Abandoned carts are runtime state too, and they hold cart links the new store would not honour.
  • Single-use sign-in tokens. Password-reset, magic-link and activation tokens are dropped. A customer who was halfway through a reset asks again.
  • Whether the store is open. The store status and its message (store.status and store.status_message) belong to each site. A bundle never carries them, and a restore keeps the new store's own, even from an older bundle or one edited by hand that has them. See Configuration → Store status.
  • Add-ons that offer nothing. An add-on with tables of its own and nothing registered for export is named in the bundle under "not included". The Export screen says so, and so does the restore report, so a restore never claims to be complete when it is not.

Inside the zip

Path What it is
manifest.yaml The format; the KahunaCart and add-on versions; the old site's address, store route, languages, currency and database; the parts with their counts; what was left out; which secrets are sealed; and every file's SHA-256 checksum
README.txt What the bundle is and how to restore it, for whoever finds it in a backup folder
data/<part>/<table>.jsonl The rows, one JSON object per line, ids included
catalog/catalog.yaml The readable catalog, the same file bin/plugin kahunacart export writes. It imports on its own with bin/plugin kahunacart import catalog.yaml
promotions/sales.yaml The sales, in the catalog file's format
customers/customers.csv Every customer, in the CSV importer's own column names
orders/orders.csv Every order exported, in the order export's columns
config/config.yaml The readable configuration, as config:export writes it, with secrets as $KAHUNACART_… references
media/, files/ Pictures and digital files, when those parts are ticked
secrets.enc Only with Keys and secrets ticked: every secret, sealed with your passphrase

The readable copies are there for a merchant who never restores anything. A bundle is also the answer to "give me my data": you can open those files without KahunaCart.

A restore checks every file against its checksum before it opens the secrets or writes anything, and refuses a bundle that was changed or damaged. A queued restore runs in steps, and each step checks that the bundle on the server is still the one the first step checked. A bundle copied over it halfway stops the restore.

Files a restore will not write

A checksum only proves the bundle is the one that was written, not that whoever wrote it meant well. So a restore decides for itself which files it puts on disk:

  • No hidden files. A name starting with a dot, such as .htaccess, is never written.
  • Nothing a web server could run. A file with .php, .phtml, .phar, .php3 to .php8, .pht, .cgi, .pl, .py, .sh, .shtml, .htaccess, .htpasswd or .ini anywhere in its name is refused, so photo.php.jpg is refused too.
  • Only pictures and media in the picture folders. They take what the media upload takes: jpg, jpeg, png, webp, avif and gif, plus the mp4, webm, mp3, m4a, ogg, flac and pdf samples.
  • Nothing through a link. A folder that is a link to somewhere outside the store's own folder is never written through.

The digital files folder gets its deny-all .htaccess back after a restore writes into it. Each refused file is a failed row in the report, with the reason.

Passwords and secrets

The Export screen has two ticks for these. Both are off to start with.

Customer passwords puts customers' sign-in passwords in the bundle, hashed exactly as the store keeps them, never readable. The hashes travel in the ordinary data files, not in secrets.enc, so the passphrase does not protect them. Handle a bundle with this ticked the way you would a database backup. A customer's Grav account comes along when it belongs to them alone, so they sign in on the new site with the password they already have. An account whose own access or any of whose groups grants more than the site (admin., api. or any other area) is left behind, so a staff account never travels in a bundle. A restored account gets site access only and no groups. Leave the tick off unless you are moving the store. Without it, a restored customer signs in with a fresh link, and two-factor sign-in is switched off for anyone whose secret did not come across.

Keys and secrets puts every secret the store holds into one file, secrets.enc, sealed with a passphrase you type twice. It needs at least 8 characters. Apart from the password hashes above, nothing else in the bundle holds a secret, ticked or not. The secrets are:

  • payment provider keys and webhook secrets from the provider settings files, and seller payment account credentials
  • secrets kept in the database settings
  • webhook signing secrets and notification channel settings
  • saved payment methods and payment provider customer references, including the ones a provider's answer on a transaction names
  • customers' two-factor secrets
  • the tokens behind download links, back-in-stock alert links and shared wishlist links

The passphrase is stretched with Argon2id, and the file is sealed with libsodium's secretbox. Nothing opens it without the passphrase, so keep the passphrase somewhere apart from the bundle.

Note

Without the secrets, or with the wrong passphrase, the rest of the bundle still restores. The report lists every key to type in again under Enter these again. Provider settings that held a key are left as they are on the new store. Download, alert and wishlist links get new tokens, so links in emails already sent and wishlist links already shared stop working. Notification channels come across switched off until their settings are entered again.

In a merge, a webhook subscription, notification channel or seller payment account the store already has keeps its own secret and settings, and is not listed to enter again.

Export the store

  1. Go to KahunaCart → Import & Export, on the Export tab.
  2. Leave Everything ticked, or untick it and pick the parts you want.
  3. Set the options below.
  4. Click Export. The screen shows the progress.
  5. When it is done, click Download. The button shows the size.
Option Default What it does
Picture files On The product and category pictures themselves. Off, the media part is left out even when it is ticked. The bundle keeps the file names, and a restore lists the pictures it cannot find
Digital product files On The files customers download. They can be large, so leave them out if you would rather copy them across yourself. Off, the files part is left out even when it is ticked
Orders from, Orders to Empty Only orders placed between these dates. Empty means every order
Customer passwords Off See Passwords and secrets
Keys and secrets Off See Passwords and secrets. The passphrase boxes appear when it is ticked

The worker writes the bundle one part at a time and checks the clock between parts. Once a step has used its slice of jobs.job_timeout_seconds, the next part waits for the next step. A part is always written whole, though, and so is the final zip, so one very big part (years of orders, or gigabytes of digital files) can still outrun the worker's time limit. Export a store that size from the command line. The passphrase waits on the queued job, encrypted with this store's own key, until the export is over, so a worker restarted halfway carries on. Then it is removed.

If the progress stays at the start, the worker is not running. See CLI → work.

Export… on Products and Customers

Products has an Export… menu with the catalog file (catalog.yaml) and a products CSV. Customers has one with a customers CSV. Each downloads that one file straight away, not a zip. Both menus need kahunacart.export, on top of the screen's own permission.

The CSVs use the importer's own column names, so they import again with every column found by name and nothing to map. The products CSV has a row per product with its default variant. A product's other variants and options are in the catalog file. Each menu ends with The whole store…, which opens the Export tab.

Restore a bundle

  1. Go to KahunaCart → Import & Export, on the Import tab, and open Restore from a bundle.
  2. Drop the zip on the page, or pick one of the bundles already on the server.
  3. Read what is in it.
  4. Choose Merge into this store or Full restore.
  5. Tick or untick parts, and type the passphrase if the bundle carries secrets.
  6. Click Dry run, and read the report.
  7. Click Restore. It is enabled once a dry run has finished.

The list of bundles on the server holds this store's own exports, earlier uploads, and any bundle copied into user/data/kahunacart/exports by hand. A bundle too big to upload can be copied into that folder over SFTP, and it appears in the list.

Before you choose anything, the screen shows where the bundle came from and when, the KahunaCart and add-on versions that wrote it, the database, each part with its row count and whether it can be restored here, and what the bundle does not carry. A part whose add-on is not installed and enabled on this store is listed with the add-on and version to install.

Merge or full restore

Merge into this store Full restore
Use it for Bringing a staging store's catalog live, combining two stores, or putting back what is missing Moving the store to a new site
Into Any store. A catalog merge needs the same base currency on both stores, because prices are not converted. The other parts still run A store with no orders and no customers: a fresh install, or one with only the demo catalog
Ids New ones where needed, and every reference points at the new id Exactly as they were
A row both stores have Updated from the bundle, matched by its natural key (a product's slug, a customer's email, an order's number, a coupon's code). Left alone and counted as skipped when it already matches Replaced
A row only this store has Left alone Removed, for the tables the bundle restores
Orders already here Left alone. A merge never updates an order Not possible: a full restore refuses a store with orders
Running it twice Changes nothing. The second run counts every row as skipped Refused. A full restore runs once: after it the store has customers or orders, so it no longer qualifies

Full restore is offered only when the store qualifies. When it does not, the screen says why in one line.

A merge has one exception to "update what matches, create the rest". Rows with no key of their own, such as a customer's addresses and group memberships, the shipping methods under a zone and the tax rates under a zone, are only added under a parent the same merge created. A customer the store already has keeps their own addresses, and a zone it already has keeps its own methods and rates, so a merge from staging does not carry changes to those across.

Conditions travel with the thing they belong to. A merge moves the product, variant, category and tag ids inside a condition to this store's own, on a coupon, a sale, a campaign template and a payment method. An id this store has no match for is dropped from the condition. A condition left with nothing is left out, and the report says so.

The dry run

The dry run rehearses the database work inside a transaction that is always rolled back. It writes nothing, not even an empty folder for the digital files. The files, the Grav accounts, the settings files and the rebuilds only happen on the real run, so one of those can still fail there. Read the final report too, not only the dry run's.

What a restore never does

It sends nothing. No email, no notification, no webhook delivery and no payment provider call comes from a restored row. No order event fires, and no job is queued for one. A restored order that was paid stays paid, nobody is charged, and a customer whose order is restored is not told about it again.

The report

For each part, the report counts what was created, updated, skipped and failed, with the notes and errors behind them. It also lists the parts that could not run and why, what the bundle does not carry, and Enter these again: the keys and settings to put back by hand.

Each part runs in its own transaction. A part that fails is rolled back whole, counts and all, and the report names it as failed while the others go ahead. The settings files are only written once the settings rows are in. In a merge, a row that fails fails alone. So does a product or a sale this store refuses, such as a product type or a sale rule no plugin here knows: the rest of the catalog and the other sales still come in.

When the bundle came from a different site address or store route, the report ends with On the old site. It has the steps for the site the store moved from, a redirects: block for that site's user/config/site.yaml with a copy button, and the new webhook address for each payment provider. See Moving your shop to its own site.

History

The History tab lists every export and restore, newest first: what it was, when, who ran it, the bundle's size and how it went. A run from the command line shows Command line as who ran it. A restore's line opens its report again. An export's bundle can be downloaded from here until it is deleted.

Bundles are deleted by hand, or by the worker once they are older than export.keep_days (7 by default). Set it to 0 to keep them until you delete them yourself. When a bundle is deleted its line stays, because "who exported our customer list on Tuesday" needs an answer after the file is gone.

Bundles live in user/data/kahunacart/exports, which carries the same deny-all .htaccess as the downloads and imports folders. A bundle is only ever handed out through the signed-in admin download, never from a public address. On nginx and Caddy, the rules in Hardening cover it with the rest of user/data/kahunacart.

Warning

A bundle is your whole customer list, every address and every order. With Customer passwords ticked it holds their password hashes too, unsealed, and with Keys and secrets ticked, your payment keys sealed under one passphrase. Keep downloaded bundles somewhere as safe as the server itself, and keep export.keep_days short on a server you share.

Databases

A bundle restores into any database KahunaCart runs on, whichever one wrote it: SQLite, MySQL and MariaDB, or PostgreSQL. Rows travel as JSON with their ids. After a full restore, each table's id counter is moved past the highest restored id, so the next order or product gets a new id rather than one already used.

From the command line

bin/plugin kahunacart export --bundle writes a bundle, and bin/plugin kahunacart import with a .zip restores one.

BASH
bin/plugin kahunacart export --bundle=store.zip --passwords --secrets
bin/plugin kahunacart export --bundle=catalog-only.zip --only=settings,catalog,media
bin/plugin kahunacart export --list-sets

bin/plugin kahunacart import store.zip --full --dry-run
bin/plugin kahunacart import store.zip --full
bin/plugin kahunacart import staging.zip --merge --only=catalog,media

The command line runs every step in one go, in the foreground, so it is the way to export a store too big for the job worker's time limit. It is recorded in History like an export from the admin. --secrets asks for the passphrase twice, or reads KAHUNACART_EXPORT_PASSPHRASE.

For a restore, --merge or --full is required, and naming both is refused. If the bundle carries secrets, the command asks for the passphrase or reads KAHUNACART_EXPORT_PASSPHRASE. Leave it empty to restore without them.

Every flag is on CLI → export and CLI → import.

Moving your shop to its own site

This is the commonest reason to have a bundle: the shop has been at /shop on a big site, and it is moving to a site of its own. Rehearse first while the old shop stays open, then pause it and stop it for the real move, so nothing is written to the old store after the final export.

  1. Set up the new site. Install Grav, then KahunaCart and every add-on the bundle's manifest lists, at those versions or later, and enable them. The restore screen tells you if one is missing. Set the store route you want. It can be anything, including /. Set up the languages too, in user/config/system.yaml, with the same languages and the same choice of whether the default language gets a prefix. A bundle does not carry them, and the redirects only keep a language prefix the new site has. Add bin/plugin kahunacart work to its crontab, because the worker does the restore. Set its Store status to Paused, or Coming soon if search engines shouldn't see it yet (bin/plugin kahunacart store --status=paused), so nobody can buy there before you've checked it. A bundle never carries the store status, so the restore leaves this one as it is.
  2. Rehearse. Export everything on the old site with Customer passwords and Keys and secrets ticked. On the new site, open Import & Export → Import → Restore from a bundle, upload the bundle (or copy it into user/data/kahunacart/exports), choose Full restore, type the passphrase, and run the dry run only. Fix whatever its report names, such as a missing add-on. A dry run writes nothing, so the new store is still empty for the real restore.
  3. Pause the old shop. Set its Store status to Paused, with a message saying what is happening, such as "We're moving to a new home and will be back on Monday." Use the switch on its dashboard, the General tab of its settings, or bin/plugin kahunacart store --status=paused --message="…". That one switch closes every door to a new purchase: the cart, checkout, express pay, the storefront API, MCP and UCP all refuse with your message, and every page shows it as a banner. Pausing doesn't freeze the store, though. Customers can still pay what they owe on an existing order, payments already in flight still settle, provider webhooks still arrive, and the worker still runs renewals and sends emails. The next three steps stop those. Make no more changes in its admin from here on.
  4. Let the old site settle. Wait for payments still in flight to settle and for the job queue to empty. bin/plugin kahunacart jobs:list --state pending shows what is left.
  5. Stop the old worker. Take bin/plugin kahunacart work off the old site's crontab, and check that bin/plugin kahunacart jobs:list --state running is empty. Renewals, reminders and anything else that falls due from now on waits in the store, and the new site's worker picks it up after the restore.
  6. Take the final export, then turn the old store off. Export the same way as the rehearsal, download the bundle, and keep the passphrase somewhere else. As soon as it's done, turn KahunaCart off on the old site, with enabled: false in its user/config/plugins/kahunacart.yaml, so nothing more is written to the old store. Until step 9, a provider webhook sent to the old address fails and the provider tries it again later, and a customer who opens a pay link gets a page that isn't found. Keep the next steps short.
  7. Restore on the new site. Choose Full restore, type the passphrase, run the dry run, read it, then click Restore. Or run bin/plugin kahunacart import store.zip --full over SSH. A full restore runs once: afterwards the store has customers and orders, and another full restore is refused.
  8. Check the new site. Read the final report, not only the dry run's, because files, accounts and settings files are only written on the real run. Run bin/plugin kahunacart check.
  9. Change the webhooks. At each payment provider, change the webhook address to the new one the report lists. Providers try a failed webhook again for hours or days, so the ones that failed during the move reach the new site now. Look at each provider's webhook log for deliveries from the move that failed for good, and send those again from there.
  10. Redirect the old shop. Paste the redirects: block from the report into the old site's user/config/site.yaml. Grav only reads its redirects for an address no page answers, so they work now that KahunaCart is off there. A real page at an old shop address, such as a placeholder /shop page, is served before the redirects too, so remove that page or move it.
  11. Open the new shop. It is still paused from step 1, and signed-in staff can still buy. So sign in to the new site's admin and place a test order through the storefront first; the banner there says "Your store is paused. Only staff can buy." Refund or cancel the test order, then set Store status to Open (bin/plugin kahunacart store --status=open). Everything opens at once.

Product, category and account addresses all land on the same page of the new shop, because a restore keeps every slug. Links the old site signed, such as the receipt, invoice and payment links in emails already sent, redirect but stop working, because the new site signs with its own site secret. A customer asks for a fresh one from the expired-link page. Download links in old emails keep working only when the secrets came across with the passphrase.

The redirects block has one rule per store prefix, and one more per language prefix on a multilanguage site:

YAML
redirects:
  '/shop(/.*)?$': 'https://new-store.example/store$1'

/shop/product/reef-tee goes to https://new-store.example/store/product/reef-tee, and /shopping-guide on the old site is left alone.

Tip

Moving the same site to a new server is a different job. Copy user/ and the database across as you would for any Grav site, and the store comes with it. A bundle is for when the store changes sites, or when you want a copy you can open.

For add-on developers

An add-on with tables of its own registers them on onKahunaCartRegisterExports, usually as a TableExportSet naming the same tables it declares for reset. Until it does, the Export screen and the restore report both name it under "not included". See Provider developer guide → Store export and restore.