Catalogue & product data
Bulk Product Edit
This ships in the core. It is implemented in src/pages/api/product-bulk.ts. What follows is the plan it was built from, kept as written — the shipped code may go further than it, or differ in the details.
Core feature for editing multiple products at once. Merchants can select products by category, tag, or manual checkbox selection, then bulk-update fields like price, category, stock, status, or tags without editing each product individually. This is essential for scaling product management.
Shipped so far
In the core: tick products in the admin’s product list — or Select all N matching the search, across pages — queue one or more changes (status, featured, add/remove category, add/remove tag, price set/±%/±amount, sale percent-off/end, stock), optionally Only where… a condition (status, stock state, on sale, category with its subcategories, price range), Preview, then Apply. Update from CSV does the same from a spreadsheet keyed by SKU, slug or id (a variant’s SKU updates that variant; an empty cell changes nothing; any bad row refuses the file), with a template of the current values. Every applied change is listed under Recent bulk changes with an Undo that restores only what still holds the change’s value — later edits and sold stock are kept. Every product is saved through the ordinary product save (webhooks, price history, audit), stock sold while a batch runs stays sold, and changes that would make a product free or flatten variant prices are skipped with the reason.
Still open: a queue for very large catalogues (a change is capped at 1,000 products per request).
The problem
Merchants update 1,000 prices manually; each product edit takes 2 minutes.
What it does
- Bulk selection: checkbox for each product in the list, ‘Select all’, ‘Deselect all’, count of selected products
- Batch update form: merchant selects fields to update (price, tax class, category, tags, status, featured, etc.), enters new values, applies
- Partial updates: only changed fields are updated; other fields are preserved (e.g., update price without changing category)
- Conditional updates: apply updates only to products matching a condition (e.g., ‘if status=draft, then set status=published’)
- Bulk operations: delete (soft delete), restore, publish, unpublish, add tag, remove tag, change category
- Preview changes: before committing, show summary of what will change (X products, Y field changes)
- Audit trail: bulk edit logged with who, when, which products, what changed
- Rate limiting: prevent accidental rapid clicks (bulk update takes a few seconds to apply)
- Filter + bulk edit: apply filters (category, tag, price range, status), select filtered results, bulk edit
- Bulk import replacement: import CSV with SKU column, bulk-update matching products (alternative to bulk selection UI)
- Undo (optional): recent bulk edits can be undone if supported by storage driver
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Conditional logic (if-then updates based on product attributes) — merchant applies manual selection and single update
- Scheduled bulk updates (bulk edit scheduled for future date) — use product scheduling per-product; bulk scheduling is advanced
- Bulk operations on variants (change variant price across many products) — variant management is per-product; bulk is product-level only
- Bulk AI-powered updates (auto-generate descriptions) — manual or plugin-driven only
Data model
No new schema; uses existing Product model. Add bulk_edit audit log entries: bulk_edit_job (id, created_by, created_at, product_ids: array, updates: JSON, status: ‘pending’ | ‘completed’ | ‘failed’, result_summary: {count: integer, errors: array}).
API
- PATCH /products/bulk — apply bulk update {product_ids: […], updates: {…}}, return {success: boolean, updated_count: integer, failed_count: integer, errors: array}
- POST /products/bulk/preview — preview bulk update {product_ids: […], updates: {…}}, return {will_update: integer, changes_per_field: {…}}
- GET /products/bulk/history — audit log of recent bulk edits (admin only)
- DELETE /products/bulk/:job_id/undo — undo a bulk edit (if supported by storage)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Bulk edit UI: (1) Product list with checkboxes, ‘Select all’ / ‘Deselect all’, count of selected products, (2) Bulk edit button → modal with form fields (only show fields that are commonly bulk-edited: price, category, tags, status, featured, tax_class), (3) Preview button (show summary of changes), (4) Confirm button (apply changes), (5) Success message with count of updated products. Optional: undo button if last bulk edit is recent.
The seam — why this is core
Core owns bulk update API and audit logging. This is foundational. Paid tier could own advanced features: conditional updates, scheduled bulk edits, ML-driven suggestions (e.g., ‘recommend price increase for this category’).
Core owns the interface + honest bulk editor; product operations are infrastructure, not a per-country obligation or credential.
Dependencies
- product-management (core; bulk edit applies to products)
- audit-log (core; bulk edit job logged)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- Selecting 100 products with checkboxes shows ‘Bulk actions for 100 products’ header.
- PATCH /products/bulk with {product_ids: [‘prod_1’, ‘prod_2’], updates: {price_cents: 1200}} updates both products and returns {updated_count: 2, failed_count: 0}.
- Preview shows: ‘Will update 100 products, 1 field (price_cents): 1500 → 1200, no errors detected’.
- Bulk edit does not update products not listed in product_ids; only specified products are changed.
- Only fields supplied in updates object are changed; omitted fields remain unchanged (partial update).
- Bulk edit with invalid price_cents (non-integer) shows error: ‘5 products have invalid price_cents’ and skips those products.
- Audit log entry records: {created_by: ‘user_123’, product_ids: [‘prod_1’, ‘prod_2’], updates: {price_cents: 1200}, status: ‘completed’, updated_count: 2}.
- Filtering by category=‘tops’ and selecting all (50 products), then bulk updating price updates only the 50 filtered products.
Risks
Bulk updates without confirmation can accidentally change hundreds of products. If partial update logic is wrong (e.g., overwrites instead of preserving), entire fields are lost. If rate limiting is too aggressive, merchant waits a long time; too lenient, rapid clicks duplicate the operation. No undo leaves merchants without recourse for accidental bulk deletes.
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included; Magento: included |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and CONTRIBUTING.md for how to work on this repository.