Where it is going
The commerce plan
Generated from docs/COMMERCE-PLAN.md in the AstroBaaS repository. The repository is the source of truth; this page is a copy of it.
Merged from the Magento and Shopify capability audits, deduplicated. 352 items: 166 free in the GPL core, 186 paid. Nothing here is built.
Every item has a directory under
docs/plan/with a fullPLAN.mdand the working agreement.
The rule that decides free from paid
No licence gating, ever. The line is drawn by seams: the core owns the interface and an honest hand-modelled implementation; a paid pack owns what needs a credential, a per-country obligation, or a support commitment.
One consequence outranks the commercial logic: a merchant must never be unable to comply with the law because they did not pay. Storing price history is core; the maintained per-country Omnibus display pack is paid. Computing tax from a rate table is core; maintaining twenty-seven countries’ rates is paid.
Already true, so not on the list
- Guest orders work — and did not until this branch.
POST /api/orderswas public, but every cookie-less cross-origin POST was403 CSRF_FAILED, so a shopper on a headless storefront could not buy. Fixed and asserted nine ways. - The shop currency is a setting. It was the literal
'EUR'inplaceOrder(). - Moderated product reviews with verified-buyer ship. The paid layer is photo/video UGC and external aggregation, not reviews themselves.
What is missing entirely
- GraphQL. None anywhere — REST plus OpenAPI plus a typed client is the whole surface, and that client covers posts, keys, webhooks, audit and auth: no commerce at all.
- Storefront customer accounts, multi-location inventory, POS, B2B, subscriptions, gift cards, draft orders, order editing, carrier connectors, automation flows, expansion stores.
The catalogue
Catalogue & product data — 41 (31 core / 10 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Attribute Management | Merchants can’t define Brand, Manufacturer, or Material consistently; product data is inconsistent. | core | M | free (core) | attribute-management |
| Attribute Sets (Product Type Templates) | My frames, lenses, and coatings are completely different products — they shouldn’t all have ‘color’, ‘material’, and ‘size’ fields | core | M | Free; foundation for multi-family catalogues | attribute-sets |
| B2B Wholesale Portal | My wholesale buyers need bulk-order pages with custom pricing per buyer, quote-to-order workflow, and 30-day payment terms | core | L | €50–100/month | b2b-wholesale-portal |
| Bulk Product Edit | Merchants update 1,000 prices manually; each product edit takes 2 minutes. | core | M | free (core) | bulk-product-edit |
| Bundle Products (Kit Builder) | I sell PC builds, gift hampers, and lens kits — customers pick one option per category with price totals and inventory for the complete kit | core | L | Free; core for kit-based business models | bundle-products |
| Bundle, grouped, and kit product types | Product.type is a two-member union (simple | variable). No bundles, no grouped products, no kits. Can’t model composite products. | core | M | Core | bundle-grouped-and-kit-product-types |
| CAPTCHA Integration | Merchants get flooded with spam submissions from bots; form data becomes unusable noise. | core | S | free (core) | captcha-integration |
| Cart and catalog promotion rules | No cart price rules or catalog price rules. Can’t offer ‘buy 2 get 1 free’ or ‘spend €50, free shipping’ without a coupon code. Bulk editing required for category discounts. | core | L | Core | cart-and-catalog-promotion-rules |
| Catalog Price Rules (Scheduled) | I run a weekend sale on sunglasses — I need one rule ‘category:Sunglasses, 20% off, Friday–Sunday’ applied to all 400 products without editing each one | core | L | Free; core promotion | catalog-price-rules |
| Category Management | Merchants can’t organize products into categories; all 5,000 SKUs appear as a flat list. | core | M | free (core) | category-management |
| Color Swatches & Visual Variant Selection | Customers can’t see what colors or designs they’re selecting; text dropdowns lose sales on visual products | core | M | free (core) | color-swatches-visual-variant-selection |
| Contact Form Builder | Merchants have no way to receive customer inquiries; potential sales emails go to /dev/null. | core | M | free (core) | contact-form-builder |
| Digital Download Entitlement | I sell PDF guides and video courses — I need time-limited download links that expire after 30 days, and audit who downloaded what and when | core | M | Free; completes product types | digital-download-entitlement |
| Faceted Navigation with Counts | When I filter by ‘material: plastic’ my customer wants to see ‘color: 12 options, price: 8 ranges, size: 5 options’ — not just a filter menu | core | M | Free; core search UX | faceted-navigation-with-counts |
| Flexible Product Attributes (EAV System) | I need custom fields for my products without writing code — frames need ‘lens size’, shoes need ‘width’, wine needs ‘vintage year’ | core | XL | Free; core platform primitive | flexible-product-attributes |
| GPSR Product Safety Metadata | GPSR (enforced 2023+) requires product listings to carry manufacturer/importer info, batch numbers, safety instructions, and compliance docs; merchants selling optics, automotive parts, or industrial goods can’t store this structured data. | core | M | Free (core) | gpsr-product-safety-metadata |
| Grouped Products | I want a page that shows five related products together — tripod + camera + lens + battery — where each is buyable alone or with the group | core | M | Free; completes product-type system | grouped-products |
| Hierarchical Category Tree | My categories are nested — Eyewear > Sunglasses > Polarized — and I need different rules or content per level, not a flat list | core | M | Free; core navigation structure | hierarchical-category-tree |
| Newsletter Signup Form | Merchants can’t build email lists because there’s no newsletter signup; customer acquisition costs stay high. | core | M | free (core) | newsletter-signup-form |
| Order Tracking Portal (Branded Customer View) | Customers ask ‘where’s my order?’ 50+ times; I need a branded tracking page to reduce support tickets | core | L | free (core) | order-tracking-portal |
| Product Archival | Merchants want to hide old products without deleting; they need to preserve historical data. | core | S | free (core) | product-archival |
| Product Collections | Merchants need to curate themed product groups (e.g., ‘Summer Sale’, ‘Gift Ideas’); static categories don’t fit. | core | M | free (core) | product-collections |
| Product Inquiry Form | Customers can’t ask questions about products (sizing, compatibility, stock); merchants lose sales when customers abandon due to uncertainty. | core | M | free (core) | product-inquiry-form |
| Product Management | Merchants need to create, edit, and delete products; this is the core CMS function. | core | XL | free (core) | product-management |
| Product Status (Draft/Published) | Merchants can’t prep products before launch; there’s no draft state, so they must publish incomplete products. | core | S | free (core) | product-status |
| Product Tags | Merchants can’t cross-cut organize products (e.g., ‘bestseller’, ‘new’, ‘clearance’); categories alone aren’t flexible. | core | M | free (core) | product-tags |
| Product Variant Selector | Customers can’t select size/color; merchants’ dropdowns are confusing, causing checkout abandonment. | core | M | free (core) | product-variant-selector |
| Product attributes and custom fields | Product is a fixed TypeScript interface. Can’t add manufacturer, safety warnings, color swatches without redeploying. GPSR compliance fields don’t exist. | core | L | Core | product-attributes-and-custom-fields |
| Tier-0 Recommendation Rules | When a customer views a camera, I want to show 5 suggested lenses automatically — based on rules I control, not ML I don’t | core | M | Free; rule-based tier stays free | tier-0-recommendation-rules |
| Variable Products (First-Class) | My variants need independent product pages, URLs, and stock counts — when I sell out of size L, that shouldn’t hide size M from the catalogue | core | L | Free; core storefront feature | variable-products |
| Variant Management | Merchants with size/color variants must create separate products; they triple their workload. | core | L | free (core) | variant-management |
| Abandoned Cart Recovery | 30% of my visitors add items but don’t checkout — I need automatic emails reminding them, with a discount code, at 4 hours and 24 hours | paid | M | €15–30/month | abandoned-cart-recovery |
| Advanced Product Filtering & Bulk Operations | I have 2,000 products with 40 attributes — finding the 100 ‘discontinued’ frames or bulk-changing ‘material’ for 200 products takes hours | paid | M | €15–25/month | advanced-product-filtering-bulk-operations |
| Brand-Customized Invoices | Generic invoices don’t match our brand or include custom compliance fields | paid | S | EUR 100-200/year or EUR 250 one-off | brand-customized-invoices |
| Customer Surveys & Feedback Collection | I don’t know why customers churn; I need post-purchase surveys to collect actionable feedback | paid | M | $19-49/mo per 1,000 responses/mo | customer-surveys-feedback-collection |
| GPSR Compliance Templates & Validation | GPSR compliance requirements vary dramatically by product category (optics, automotive, food, electronics, toys); merchants can’t build category-specific checklists or validate product data without expert help, risking regulatory fines. | paid | M | €19.99-39.99/mo per vertical, or €49.99/mo for all verticals — owns the per-country, per-category obligation | gpsr-compliance-templates-validation |
| Gift Card Products | I want to sell gift cards — with custom denominations, expiry dates, and a redemption code flow — as a revenue stream I don’t need to fulfill | paid | M | €12–25/month | gift-card-products |
| Multi-Location Inventory Management | I have stock in a warehouse and a retail store — I need real-time availability per location and fulfillment routed to the closest warehouse | paid | L | €30–60/month | multi-location-inventory-management |
| Product Data Hub (PIM) | Managing product data across channels is fragmented; quality and consistency suffer | paid | XL | EUR 500-5000/year or EUR 2000-10000 one-off | product-data-hub |
| Product Feed Generation (Google, Meta, TikTok, Pinterest) | Each marketplace needs a different feed format; I can’t manually maintain feeds without mistakes | paid | L | $25-59/mo per channel or bundled $49-99/mo | product-feed-generation |
| Product Information Management (PIM) & Data Enrichment | Product data is scattered across spreadsheets; syncing attributes to multiple channels is error-prone | paid | L | $49-199/mo depending on product count and channel count | product-information-management-data-enrichment |
Search & discovery — 28 (23 core / 5 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Autocomplete Suggestions | Customers don’t know what to search for; they type partial searches and get no suggestions. | core | M | free (core) | autocomplete-suggestions |
| Basic Product Search | Customers can’t find products by name; they either browse forever or leave. | core | L | free (core) | basic-product-search |
| Breadcrumb Navigation | Customers get lost in deep category trees and can’t navigate back; bounce rates increase. | core | S | free (core) | breadcrumb-navigation |
| Canonical URL Manager | Merchants have duplicate content across URL variations; Google penalizes them for duplicate pages. | core | S | free (core) | canonical-url-manager |
| Customer Search | Merchants can’t find a customer by email or name; they manually scroll through lists. | core | S | free (core) | customer-search |
| Fuzzy Search | Customers searching ‘shert’ instead of ‘shirt’ get zero results; they leave instead of browsing. | core | M | free (core) | fuzzy-search |
| Keyboard Navigation Testing | Merchants don’t test with keyboard only; users without mice can’t navigate. | core | M | free (core) | keyboard-navigation-testing |
| Mega Menu | Merchants with many categories need rich dropdown navigation; simple dropdowns hide products and confuse customers. | core | M | free (core) | mega-menu |
| Menu Builder | Merchants can’t organize navigation without code; they hire developers to move a menu item. | core | M | free (core) | menu-builder |
| Meta Tags Editor | Merchants can’t set custom title tags and meta descriptions; search results show truncated or auto-generated text, losing clicks. | core | S | free (core) | meta-tags-editor |
| Mobile Menu | Merchants’ navigation doesn’t adapt to phones; customers can’t navigate on mobile and abandon carts. | core | M | free (core) | mobile-menu |
| Open Graph & Social Previews | When merchants share products on social media, previews show placeholder text instead of attractive images and descriptions; clicks drop 60%. | core | M | free (core) | open-graph-social-previews |
| Order List & Filtering | Merchants can’t find specific orders in a list of 1,000; they need search and filtering. | core | M | free (core) | order-list-filtering |
| Product Filtering & Faceted Navigation | Customers give up when they can’t narrow results by price, size, color, brand, or custom attributes | core | M | free (core) | product-filtering-faceted-navigation |
| Product Search & AI Semantic Discovery | Customers can’t find products; basic search is broken and fuzzy matching fails on typos or synonyms | core | L | free (core basic), paid module $29-49/mo for AI | product-search-ai-semantic-discovery |
| Redirect Manager | Merchants rename products and lose old Google rankings; customers get 404 errors and leave. | core | M | free (core) | redirect-manager |
| Search Analytics | Merchants don’t know what customers search for; they can’t optimize product names or add missing SKUs. | core | M | free (core) | search-analytics |
| Search Filters & Facets | Search results show 5,000 products; customers can’t filter by price, brand, or size and abandon. | core | L | free (core) | search-filters-facets |
| Search Results Sorting | Customers search and can’t sort by price or popularity; they leave to check competitors. | core | M | free (core) | search-results-sorting |
| Sitemap Page | Customers and search engines get lost navigating large sites; there’s no way to find pages. | core | S | free (core) | sitemap-page |
| Sticky Header | Customers scroll deep into a product page and can’t access the cart or search without scrolling back up; checkout abandonment increases. | core | S | free (core) | sticky-header |
| Structured Data (Schema.org) | Search engines don’t understand product prices, ratings, or availability; rich results with stars and price don’t appear in Google. | core | M | free (core) | structured-data |
| XML Sitemap | Search engines can’t discover all pages because merchants don’t have a sitemap; products are never indexed. | core | S | free (core) | xml-sitemap |
| Advanced Comment Moderation & Spam Filtering | My popular blog gets 100+ comments per post; manual review is overwhelming, I have no spam filtering, and I can’t detect toxic or off-topic comments | paid | M | $15–40/mo for AI-powered moderation, sentiment analysis, and bulk review tools | advanced-comment-moderation-spam-filtering |
| Advanced Search (Elasticsearch) | Native search can’t handle typos, facets, or relevance; need Elasticsearch | paid | L | €49/mo; faceted search, typo tolerance, relevance tuning | advanced-search |
| Frequently-Bought-Together Recommendations | My bundle suggestions are random; I need ML to show pairs/triplets that actually sell together to boost AOV | paid | M | $19-39/mo | frequently-bought-together-recommendations |
| Post-Purchase Upsell & Recommended Products | Customers leave after checkout; I lose 10-20% AOV by not recommending add-ons post-order | paid | M | $19-39/mo | post-purchase-upsell-recommended-products |
| Product Recommendations Engine | Related products in sidebar; recommendations increase AOV 15-30% | paid | L | €49/mo; collaborative filtering, content-based recs, A/B testing | product-recommendations-engine |
Pricing & promotions — 26 (12 core / 14 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| B2B Wholesale Pricing & Custom Price Lists | I can’t set different prices for bulk orders or wholesale customers without manual overrides | core | XL | free (core) | b2b-wholesale-pricing-custom-price-lists |
| Background Task Scheduler (Cron) | My Black Friday sale starts at midnight — I shouldn’t need to wake up and re-save every product. It should flip automatically at time | core | M | Free; core platform necessity | background-task-scheduler |
| Cart Price Rules (Automatic) | I want ‘spend €50 get free shipping’ or ‘20% off orders over €100’ to apply automatically when customers checkout — no coupon code needed | core | L | Free; core promotion engine | cart-price-rules |
| Customer groups and wholesale pricing | No customer groups, no tier pricing, no quantity breaks. Can’t give members a discount, can’t apply wholesale pricing, can’t use group-specific tax classes. | core | M | Core | customer-groups-and-wholesale-pricing |
| Multi-Currency Pricing Engine | Merchants operating in multiple countries can’t display prices in local currencies, forcing customers to see EUR only; conversion rates drop 40%+ per language/currency tier when prices aren’t localized. | core | L | Free (core) | multi-currency-pricing-engine |
| Omnibus 30-day lowest-price display | No price-history table. EU Omnibus requires showing lowest price over past 30 days when advertising discount. Legally required across EU. | core | M | €49–99/month | omnibus-30-day-lowest-price-display |
| Price Display Component | Merchants can’t show list price crossed out with sale price; customers don’t see the discount. | core | S | free (core) | price-display-component |
| Price History (Omnibus) | EU Omnibus Directive (2024) requires displaying the lowest price of the last 30 days for any promotion; merchants have no system to track historical prices or prove compliance to regulators and competitors. | core | M | Free (core) | price-history |
| Product Bundling & Mix-and-Match | My average order value drops when customers buy single items; I need to suggest bundles without custom development | core | L | free (core) | product-bundling-mix-and-match |
| Product Customization & Options (Swatches, Uploads, Pricing Add-ons) | Shopify’s 3-variant limit chokes businesses selling customizable items (color, size, text, files); I need unlimited options | core | L | free (core) | product-customization-options |
| Quantity Discounts & Tiered Volume Pricing | I can’t show ‘buy 3+ and save 10%’ offers without manual price management at scale | core | L | free (core) | quantity-discounts-tiered-volume-pricing |
| Tier & Quantity Pricing | My wholesale buyers and VIP members need different prices — member pays €18, wholesaler at qty 10+ pays €12 — without custom code | core | L | Free; core for B2B-friendly shops | tier-quantity-pricing |
| B2B & Wholesale Tiered Pricing | I need to offer different prices to wholesale buyers, apply bulk discounts, and support credit-term orders, but the cart has no wholesale pricing layer. | paid | L | $399–799/year | b2b-wholesale-tiered-pricing |
| Draft Orders & Approval Workflow | My sales team prepares orders for approval before I process payment — I need a draft workflow where the order holds until approval, then payment is collected | paid | M | €12–20/month | draft-orders-approval-workflow |
| Dynamic Pricing by Season (Hotels) | Hotels need €80/night in winter, €150 in summer; static pricing loses revenue | paid | M | €39/mo; seasonal rules, demand-based pricing, blackout dates | dynamic-pricing-by-season |
| Fraud Scoring | I process 100 orders a day and 5% are fraud — I need automated flagging so I review high-risk orders before fulfillment, not after disputes | paid | M | €20–35/month | fraud-scoring |
| Gift Cards | Gift cards increase AOV; merchants need issuance, balance tracking, expiry rules, resale prevention | paid | M | €19/mo; we hold liability for merchant-issued gift cards | gift-cards |
| Lens Coating Upsell Engine (Optics) | Coatings are 30% margin; merchants lose upsells when RX is unknown pre-checkout | paid | M | €24/mo; auto-recommend coatings based on RX prescription, material | lens-coating-upsell-engine |
| Live Exchange Rates | Static currency rates stale within days; merchants either manually update rates daily (error-prone, labor-intensive) or expose themselves to currency arbitrage losses and customer complaints. | paid | S | €4.99-9.99/mo — owns the support commitment (daily ECB/OANDA updates, freshness guarantee) | live-exchange-rates |
| Loyalty Points Program | Customers repeat-buy when they earn points; need point earning, redemption, tiers | paid | M | €34/mo; points earning, redemption, tier tiers (VIP), referral bonus | loyalty-points-program |
| Loyalty Points, VIP Tiers & Rewards | I can’t retain customers without a points system that makes spending feel rewarded; tier logic is complex | paid | L | $29-79/mo; anchor: Smile $29 entry tier | loyalty-points-vip-tiers-rewards |
| Loyalty Rewards Program | One-time buyers never return; I need to gamify repeat purchases | paid | L | EUR 250-400/year or EUR 500 one-off | loyalty-rewards-program |
| Post-Purchase Insurance & Warranty Sales | I leave money on the table not offering extended warranties or damage protection at post-purchase | paid | M | $9-25/mo + commission (insurance partners take 15-25% of premium) | post-purchase-insurance-warranty-sales |
| Scheduled Promotions Manager | I plan campaigns months ahead and want a calendar UI where I see ‘Memorial Day sale starts Friday, ends Sunday’ — not a spreadsheet | paid | M | €20–30/month (€240–360/year) | scheduled-promotions-manager |
| Smart Promotions Engine | Complex deals—free gifts, conditional discounts, volume breaks—require coding each time | paid | L | EUR 300-500/year or EUR 600 one-off | smart-promotions-engine |
| Subscriptions, gift cards, payment plans | Subscriptions, gift cards, draft orders, and order editing are on paid track. Customers can’t set up recurring billing or buy gifts. | paid | L | €49–199/month | subscriptions-gift-cards-payment-plans |
Checkout & payments — 23 (10 core / 13 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Add to Cart Notification | Customers don’t know add-to-cart worked; they click again and accidentally order 10 copies. | core | S | free (core) | add-to-cart-notification |
| Cart Persistence | Customers’ carts disappear on page refresh; they re-add products or abandon. | core | M | free (core) | cart-persistence |
| Multi-currency checkout | Currency is hardcoded to EUR in checkout (src/lib/commerce-service.ts:798). Money formatter supports other currencies but checkout does not. Can’t trade in multiple currencies. | core | L | Core | multi-currency-checkout |
| Order Confirmation Page | Customers don’t know order succeeded; they must check email or refresh the page. | core | M | free (core) | order-confirmation-page |
| Payment Status Tracking | Merchants can’t see if payment is pending, authorized, or captured; cash flow forecasting is impossible. | core | M | free (core) | payment-status-tracking |
| Payment gateway sandbox round-trip verification | Payment integrations (Stripe, PayPal, Klarna) are well-designed but never sandbox-verified against real gateways. No round-trip validation before claiming commerce-readiness. | core | M | Core | payment-gateway-sandbox-round-trip-verification |
| Quick Add to Cart | Customers must click into each product then add to cart; one-click add-to-cart cuts friction 30%. | core | M | free (core) | quick-add-to-cart |
| Shopping Cart | Customers need to hold products before purchase; without a cart, each product adds instantly to order. | core | L | free (core) | shopping-cart |
| Structured Address Model | My invoices are untrackable because billing address is a text string, not structured fields. I cannot validate addresses or separate line-1 from line-2. | core | L | Free | structured-address-model |
| Terms & Conditions Checkbox | Merchants lack legal protection if customers don’t accept terms; they need a mandatory checkbox. | core | S | free (core) | terms-conditions-checkbox |
| Adyen Payment Gateway | Merchants outside US need local payment methods (iDEAL, Bancontact, etc.) | paid | M | €24/mo + 0.5% gateway fee; credential: Adyen merchant account setup | adyen-payment-gateway |
| Apple Pay / Google Pay Setup | Mobile checkout: Apple Pay converts 3x better; setup is complex | paid | M | €19/mo; credential: merchant ID setup, certificate management | apple-pay-google-pay-setup |
| B2B Net Terms & Payment Terms (30/60/90 Days) | Wholesale customers demand ‘net 30’ or ‘net 60’ payment terms; I can’t extend credit without risk | paid | M | $25-75/mo + fraud scoring fees | b2b-net-terms-payment-terms |
| B2B Payment Terms & Deposits | B2B buyers expect Net-30 invoicing + deposits; needs credit tracking & collections | paid | M | €39/mo; automatic invoice generation, aging reports, dunning | b2b-payment-terms-deposits |
| BNPL (Klarna Advanced Tier) | Customers want payment plans; Klarna Advanced gives merchant control | paid | M | €99/mo + 1% per transaction; credential: Klarna API key | bnpl |
| Cart Abandonment Recovery | 30-40% of carts are abandoned; I lose revenue by not auto-reminding customers with personalized recovery sequences | paid | L | $15-29/mo as module or $5 add-on to Email Marketing | cart-abandonment-recovery |
| Cart Recovery Automation | 80% of abandoned carts get no reminder; recovery is left to manual email blasts | paid | M | EUR 150-300/year or EUR 350 one-off | cart-recovery-automation |
| Fraud Prevention & Security | Chargebacks and data breaches threaten profitability and customer trust | paid | M | EUR 250-500/year or EUR 600 one-off | fraud-prevention-security |
| Multi-Address Checkout | I need to ship different items in one order to different addresses, but the order model forces a single destination. | paid | M | $99–199/year | multi-address-checkout |
| One-Step Checkout | Cart abandonment spikes when checkout requires three pages of form-filling | paid | M | EUR 150-250/year or EUR 350 one-off | one-step-checkout |
| PayPal Suite Expansion | I only have one PayPal integration. Merchants need Express Checkout, Payments Standard, Payflow Link, and subscription support. | paid | M | $199–399/year | paypal-suite-expansion |
| Production Payment Gateway Certification | My payment integrations (Stripe, PayPal) are sandbox-only. I need production credential sign-off and PCI compliance certification before going live. | paid | S | $99–299 one-time or $49–99/year | production-payment-gateway-certification |
| Tuition Payment Plans (Education) | Families can’t pay tuition upfront; schools lose students who can’t afford semester rates | paid | M | €24/mo + 1% per plan; installment tracking, non-payment alerts | tuition-payment-plans |
Orders & fulfilment — 54 (24 core / 30 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Address book with billing/shipping split | Order.address is a flat string with no separate billing address, no address book, no validation. Can’t handle orders where billing differs from delivery. Blocks real invoicing. | core | M | Core | address-book-with-billing-shipping-split |
| Bulk Order Actions | Merchants can’t update 50 orders at once; they manually change status one-by-one. | core | M | free (core) | bulk-order-actions |
| CPU Usage Monitoring | Merchants’ servers get throttled during peak hours; slow page loads cost sales. | core | S | free (core) | cpu-usage-monitoring |
| Content Performance Analytics | I don’t know which blog posts drive traffic or engagement or where visitors come from—I’m guessing what content strategy to invest in | core | L | free (core): post view counts, referral sources, basic engagement time; paid plans $15–40/mo for heatmaps, audience segmentation, predictive analytics | content-performance-analytics |
| Credit Memo Documents | When I refund an order, there is no credit memo document for accounting. My bookkeeper cannot reconcile refunds without a formal document. | core | S | Free | credit-memo-documents |
| Database Query Monitoring | Developers don’t know which database queries are slow; a missing index causes 30-second page loads. | core | M | free (core) | database-query-monitoring |
| Fulfillment Status | Merchants can’t mark orders as fulfilled; they don’t know which orders are ready to ship. | core | M | free (core) | fulfillment-status |
| Invoice Generation | Merchants need to generate invoices for accounting; non-fiscal receipts don’t meet accounting requirements. | core | M | free (core) | invoice-generation |
| Lighthouse Audit Tool | Merchants don’t know their SEO, performance, or accessibility scores; they can’t prove site quality to investors. | core | M | free (core) | lighthouse-audit-tool |
| Memory Usage Monitoring | Merchants’ servers randomly crash during traffic spikes; they don’t know memory is exhausted. | core | M | free (core) | memory-usage-monitoring |
| Order Detail View | Merchants need to see all order information (items, shipping, payment, customer) in one place. | core | L | free (core) | order-detail-view |
| Order Invoice Documents | I have no formal invoice entity or invoice numbers. The /receipt endpoint is marked as non-fiscal, so I cannot use it for tax filing or bookkeeping reconciliation. | core | M | Free | order-invoice-documents |
| Order Notes | Merchants need to document special handling (gift wrap, expedited, fragile); they can’t track notes per-order. | core | S | free (core) | order-notes |
| Order Status History | Merchants can’t see when an order was placed, shipped, or refunded; they manually track events. | core | M | free (core) | order-status-history |
| Order Tags | Merchants need to categorize orders (VIP, damaged, fraud); they can’t sort or filter by custom tags. | core | M | free (core) | order-tags |
| Order Timeline | Merchants see fragmented order events; a visual timeline showing all changes chronologically is missing. | core | M | free (core) | order-timeline |
| Page Speed Monitoring | Merchants don’t know if their site is slow; a 3-second page load means 40% fewer conversions, but they’re unaware. | core | M | free (core) | page-speed-monitoring |
| Reorder One-Click | Repeat customers must re-browse and re-add items; one-click reorder increases LTV 15%. | core | M | free (core) | reorder-one-click |
| Returns & Exchanges (Self-Service RMA Portal) | Returns are a customer service burden; I need a self-service portal so customers create RMAs without calling | core | L | free (core) | returns-exchanges |
| Shipment Management & Tracking | I cannot tell customers where their order is. There is no shipment entity, tracking number field, or way to mark partial shipments. An order is ‘completed’ or it is not. | core | M | Free | shipment-management-tracking |
| Shipments with tracking and carrier hooks | No shipment entity, no tracking field, no partial shipments. Merchant shipping 20 parcels/day has nowhere to record a tracking number and customer has no way to see one. | core | L | Core | shipments-with-tracking-and-carrier-hooks |
| Shipping Rules Engine | Merchants need to charge different shipping rates based on destination region, order weight, value, and product type, but current postcode-zone matching doesn’t support complex conditions. They’re stuck offering flat rates or building custom code. | core | M | Free (core) | shipping-rules-engine |
| Uptime Monitoring | Merchants don’t know their site is down until customers complain; they lose revenue during outages. | core | M | free (core) | uptime-monitoring |
| WCAG 2.1 AA accessibility audit and conformance | Partial WCAG 2.1 AA implementation, unaudited, no published conformance claim. Enforceable since 28 June 2025 under EU Accessibility Act. | core | M | Core | wcag-2-1-aa-accessibility-audit-and-conformance |
| Carrier Integrations (UPS, DHL, ELTA, DPD, Speedex) | Merchants can’t offer real-time shipping rates from major carriers; they resort to manual quoting or lose customers to competitors with instant rate transparency. Building bespoke carrier API integrations is expensive and fragile across 5+ providers. | paid | L | €99–299/month for all-5 bundle; or €20–60/month à-la-carte per carrier, plus per-label fees (€0.10–1.50 depending on service); owner sets | carrier-integrations |
| Carrier integrations (UPS, FedEx, DHL, USPS) | No carrier integrations. No live rate quotes, no label generation, no tracking sync from carrier APIs back to orders. | paid | L | €29–79/month per carrier | carrier-integrations-2 |
| Click-and-Collect In-Store Pickup | Customers want to order online and pick up from my physical shop, but I have no way to reserve stock per location or route orders to the nearest pickup point. | paid | M | $199–399/year | click-and-collect-in-store-pickup |
| Core Return Logistics (Mechanics) | Used cores must return to supplier; tracking is manual, deposits are lost | paid | M | €29/mo; return label generation, supplier reconciliation, deposit tracking | core-return-logistics |
| DHL Connector | Merchants use DHL Europe; need live rates for zones and labeling | paid | M | €24/mo; credential: we manage your DHL account, rate negotiation | dhl-connector |
| DPD Connector | DPD is major in UK/Central Europe; merchants need live quotes and tracking | paid | M | €24/mo; credential: we manage DPD API access | dpd-connector |
| Delivery Radius & ETA Estimation (Restaurants) | Order to 50km away = too long; need radius check, ETA calculation, delivery fee logic | paid | M | €24/mo; delivery-zone mapping, ETA calculation, dynamic fee by distance | delivery-radius-eta-estimation |
| Draft Orders | B2B & sales teams quote before payment; need to stage orders with line items, track quote expiry | paid | M | €24/mo; support for quote versioning and expired-quote audits | draft-orders |
| Draft orders and order editing | Merchants can’t draft orders on behalf of customers or edit orders after placement. Needed for wholesale and customer service workflows. | paid | M | €29–79/month | draft-orders-and-order-editing |
| Dropship Operations | I want to sell products without inventory investment but managing supplier APIs is complex | paid | L | EUR 300-500/year or EUR 750 one-off | dropship-operations |
| ELTA Connector | Greek merchants need ELTA (Hellenic Post); custom connector to reach customers | paid | S | €14/mo; credential: we maintain ELTA API integration | elta-connector |
| Energy Label Generator (EU) | EU: electronics/appliances need energy labels; non-compliance = fines | paid | M | €24/mo; per-country obligation: EU energy-label data + PDF generation | energy-label-generator |
| Exchange-First Returns (Store Credits & Swaps Before Refunds) | Full refunds hurt margins; I need customers to exchange items or get store credit first, refunds last | paid | M | $15-29/mo | exchange-first-returns |
| Fulfillment & Shipping Labels | Warehouse teams pick orders from paper printouts with zero coordination or tracking; misshipments occur, staff can’t track progress, and 3PL handoffs are manual. As order volume grows, the operation collapses because there’s no system. | paid | L | €199–499/month depending on order volume (e.g., €199 for <1000/mo, €399 for <5000/mo); per-label fees offset; owner sets | fulfillment-shipping-labels |
| Live Carrier Rate APIs | I’m typing shipping rates by hand into each zone. I need real-time rates from carriers so customers see accurate costs at checkout. | paid | L | $399–899/year | live-carrier-rate-apis |
| Marketplace Order Aggregation | Sell on Amazon, eBay, Facebook; need central dashboard for all orders | paid | L | €64/mo; supports 5 marketplaces, order sync, inventory sync | marketplace-order-aggregation |
| Multi-Warehouse Inventory | Syncing stock across warehouses creates oversells, shortages, and customer cancellations | paid | L | EUR 300-600/year or EUR 800 one-off | multi-warehouse-inventory |
| Order Editing | Customers email to change orders before shipment; manual edits are slow, error-prone | paid | M | €14/mo; audit trail for regulatory compliance | order-editing |
| Order Processing Automation | Manually processing 500+ orders daily across fulfillment, accounting, and CRM is unsustainable | paid | L | EUR 250-500/year or EUR 600 one-off | order-processing-automation |
| Pre-Order for Pickup/Delivery (Restaurants) | Customers want to order before arrival; needs menu access, time slot selection | paid | M | €39/mo; order time slots, delivery range, ETA estimation | pre-order-for-pickup-delivery |
| Print-on-Demand & Dropshipping Fulfillment | I can’t afford inventory for custom/niche products; I need zero-inventory fulfillment with auto-order routing | paid | L | $0-19/mo + 2-5% transaction fee, or commission-based per fulfillment partner contract | print-on-demand-dropshipping-fulfillment |
| Returns & RMA Management | Processing returns manually destroys customer goodwill; tracking and exchanges are chaos | paid | M | EUR 150-300/year or EUR 400 one-off | returns-rma-management |
| Shipping Label Generation | I cannot generate carrier-compatible shipping labels or push tracking numbers back to the carrier. Every label is typed and printed by hand. | paid | L | $299–699/year | shipping-label-generation |
| Shipping Labels & Carrier Sync | Manual label printing is slow; I need automated shipping-label generation and carrier rate comparison | paid | L | $19-49/mo per carrier tier; anchor: Easyship $15 entry | shipping-labels-carrier-sync |
| Shipping Protection & Extended Warranty | Orders arrive damaged or lost; I can’t afford the chargebacks and negative reviews without offering protection | paid | M | $9-29/mo + 1-2% transaction fee for claims reserve | shipping-protection-extended-warranty |
| Site Performance Optimization | Every second slower costs us conversion rate, but optimization requires specialist knowledge | paid | M | EUR 200-350/year or EUR 400 one-off | site-performance-optimization |
| Speedex Connector | Speedex is primary in Greece/Balkans; merchants need rates for regional coverage | paid | S | €14/mo; credential: Speedex API management | speedex-connector |
| Storefront Customer Accounts | No password, no session, no login, no order history, no saved addresses, no reorder, no wishlist. Order lookup is number-plus-email only. C-140. | paid | L | €49–149/month | storefront-customer-accounts |
| Tracking Number Sync | My tracking numbers never make it back to the carrier portal, and customers don’t see tracking links in their emails. | paid | M | $199–399/year (bundled with carrier rates) | tracking-number-sync |
| UPS Connector | Merchants use UPS; need live rate quotes and label generation | paid | M | €24/mo; credential: we hold your UPS API key, negotiate volume discounts | ups-connector |
Inventory & suppliers — 9 (1 core / 8 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Stock Status Badge | Customers can’t see if products are in stock before clicking; they get disappointed at detail page. | core | S | free (core) | stock-status-badge |
| Back-in-Stock Notifications | A customer cannot opt in to be notified when a sold-out product is back in stock. I have no way to capture and action that intent. | paid | S | $99–199/year | back-in-stock-notifications |
| Barcode Scanning for Inventory | Manual inventory counts are error-prone; need QR/barcode scanning | paid | M | €24/mo; barcode label printing, mobile scanning, cycle-count workflows | barcode-scanning-for-inventory |
| Inventory Forecasting | When to reorder? Running out mid-week kills sales; overstock wastes cash | paid | M | €39/mo; demand forecasting, reorder-point calculation, supplier lead-time input | inventory-forecasting |
| Inventory by Part Number (Mechanics) | 50k+ SKUs with cross-references; standard inventory fails with part complexity | paid | M | €39/mo; 100k+ SKUs, cross-reference lookup, supplier sync | inventory-by-part-number |
| Lot & Batch Tracking | Recall: 5000 units from batch XYZ; need to know which customers got them | paid | M | €34/mo; lot expiration, batch tracking, recall workflows | lot-batch-tracking |
| Multi-Source Inventory (MSI) | I have three warehouses and a shop floor, but I cannot balance stock across them or set per-warehouse fulfillment rules. One stock integer per product. | paid | XL | $599–1,299/year | multi-source-inventory |
| Room Inventory by Date/Occupancy (Hotels) | Hotels have 50 rooms, each with variable dates; standard stock inventory fails | paid | L | €64/mo; calendar view, occupancy blocking, overbooking prevention | room-inventory-by-date-occupancy |
| Supplier Auto-Sync (EDI/API) | Manual reorders are slow; suppliers have APIs; need automated low-stock reordering | paid | L | €49/mo; per-supplier setup, EDI/API sync, PO generation | supplier-auto-sync |
Tax & compliance — 37 (18 core / 19 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Accessibility Metadata & Validation | EU Accessibility Act (enforced 2025-2026) requires WCAG 2.1 AA compliance; merchants have no structured fields for alt text, descriptions, ARIA labels, and can’t validate if product pages meet requirements. | core | M | Free (core) | accessibility-metadata-validation |
| Advanced VAT Configuration | I need compound tax (tax on tax), fixed product tax, and per-customer-group tax classes, but AstroBaaS only supports flat rates per class. | core | M | Free | advanced-vat-configuration |
| Alt Text Assistant | Merchants leave product images without alt text; screen reader users can’t see products, and SEO suffers. | core | M | free (core) | alt-text-assistant |
| Color Contrast Validator | Merchants use hard-to-read color combinations; users with vision impairment can’t see the site. | core | S | free (core) | color-contrast-validator |
| Conversion Tracking | Merchants can’t measure checkout effectiveness; they don’t know if recent changes improve or hurt conversions. | core | M | free (core) | conversion-tracking |
| Cookie Consent Banner | Merchants face GDPR fines because they track without consent; legal risk is existential. | core | M | free (core) | cookie-consent-banner |
| Destination-Based Tax | Merchants selling across EU countries must charge different VAT rates per destination jurisdiction, but lack a system to store, apply, and audit rates, risking incorrect tax collection and regulatory penalties. | core | M | Free (core) | destination-based-tax |
| Email Consent Manager | Merchants send emails to customers who never opted in; CAN-SPAM and GDPR fines apply. | core | M | free (core) | email-consent-manager |
| Focus Indicators | Keyboard-only users can’t see which element is focused; navigation is impossible. | core | S | free (core) | focus-indicators |
| Font Size Adjuster | Users with vision impairment can’t enlarge text; they see nothing and leave. | core | S | free (core) | font-size-adjuster |
| Funnel Analysis | Merchants don’t know where visitors drop off; 90% leave at cart, but merchants don’t know why. | core | L | free (core) | funnel-analysis |
| GDPR Data Export | Customers request their data under GDPR but merchants can’t export it; legal risk. | core | M | free (core) | gdpr-data-export |
| Google Analytics Integration | Merchants have no traffic data; they can’t see which products, landing pages, or campaigns drive sales. | core | M | free (core) | google-analytics-integration |
| Invoices and credit memos | Receipt at /receipt is well-designed but explicitly NOT a tax invoice. No credit memo for refunds. Refund ledger exists but accounting documents do not. | core | M | Core | invoices-and-credit-memos |
| Privacy Policy Generator | Merchants don’t have legal templates and risk GDPR fines with hand-written policies. | core | S | free (core) | privacy-policy-generator |
| Right to Be Forgotten | Merchants can’t delete customer data when requested; GDPR violations cost 4% of revenue in fines. | core | M | free (core) | right-to-be-forgotten |
| Skip-to-Content Links | Keyboard users tab through 50 navigation links to reach main content; accessibility is painful. | core | S | free (core) | skip-to-content-links |
| WCAG 2.1 Compliance Checker | Merchants don’t know if their site is accessible to disabled users; they risk lawsuits and lose customers. | core | M | free (core) | wcag-2-1-compliance-checker |
| Accessibility Audit & Theme Certification | Keeping storefronts WCAG 2.1 AA compliant requires quarterly audits (€3k-5k per audit) and certified themes; merchants have no way to prove EAA compliance to regulators or customers, risking fines. | paid | L | €19.99/mo for quarterly audits + €9.99-29.99 per certified theme — owns the support commitment and EAA filing obligation | accessibility-audit-theme-certification |
| Accounting Sync (myDATA, Billtrust) | Greek merchants: myDATA reporting is mandatory; manual sync is painful | paid | L | €44/mo; credential & obligation: Greek myDATA API integration, weekly filing | accounting-sync |
| Age Verification (Alcohol/Tobacco) | Selling alcohol/tobacco online: must verify age; manual verification fails | paid | M | €34/mo; credential: age-verification API integration, audit trail | age-verification |
| B2B VAT ID Validation & Tax Exemption | B2B customers claim tax exemption; I need to validate their VAT ID via VIES or local APIs to avoid chargebacks | paid | M | $15-29/mo + per-validation fees (EUR 0.10-0.50/lookup) | b2b-vat-id-validation-tax-exemption |
| CCPA & Multi-Region Privacy Compliance (CPRA, LGPD, APPI) | US/Brazil/Japan privacy laws require different disclosures; I need region-aware consent flows | paid | M | $29-59/mo for 3+ regions | ccpa-multi-region-privacy-compliance |
| EU Tax Compliance Pack | Maintaining accurate VAT rates for 27 EU countries (each with standard, reduced, super-reduced, zero rates), quarterly updates, OSS filing, and staying current with law changes costs merchants hundreds of hours annually or €2k-5k in consultant fees. | paid | L | €14.99-24.99/mo for VAT rates; +€9.99/mo for OSS filing — owns the per-country obligation and quarterly maintenance commitment | eu-tax-compliance-pack |
| EU VAT Compliance & IOSS/OSS Automation | EU VAT rules (IOSS, OSS, reverse charge) change monthly; I can’t stay compliant without an expert | paid | L | $29-99/mo per country; anchor: EAS freemium, competitor $25-50/mo entry | eu-vat-compliance-ioss-oss-automation |
| GDPR & Cookie Consent Management | I need a cookie banner that blocks tracking pixels until consent is given; GDPR fines are EUR 20M+ | paid | L | $19-49/mo | gdpr-cookie-consent-management |
| GDPR Audit Trail | GDPR right-to-deletion: prove you deleted data within 30 days | paid | M | €24/mo; per-country obligation: GDPR audit logs, deletion proof | gdpr-audit-trail |
| GPSR product safety and traceability fields | No fields for manufacturer, EU Responsible Person, or safety warnings. Mandatory since 13 Dec 2024. Requires attributes system (core item). AstroBaaS merchants cannot add fields at all. | paid | M | €29–59/month or bundled with Omnibus | gpsr-product-safety-and-traceability-fields |
| Greece myDATA E-Invoicing | My orders are not e-invoiced to Greece’s tax authority (AADE myDATA). I can only record the receipt number manually, with no real-time compliance. | paid | L | $799–1,999/year | greece-mydata-e-invoicing |
| Greek myDATA e-invoicing | Greek myDATA refused by design. Mandatory e-invoicing for B2B transactions in Greece. No integration pending merchant docs (IRIS-PSP-blocker). | paid | L | €99–199/month (Greece-specific) | greek-mydata-e-invoicing |
| INTRASTAT (EU Trade Declaration) | EU merchants ship to EU; INTRASTAT filing is mandatory, complex | paid | M | €34/mo; per-country obligation: INTRASTAT data export, filing format | intrastat |
| OSS (One-Stop-Shop) | EU distance-selling VAT (OSS) is complex; merchants face fines for non-compliance | paid | L | €59/mo; monthly OSS reporting ready-to-file for France, Germany, Netherlands, etc. | oss |
| PCI Compliance Dashboard | PCI audits: need to prove card data is never stored, SSL is current | paid | M | €19/mo; support commitment: PCI audit documentation, SSL monitoring | pci-compliance-dashboard |
| Peppol eInvoicing (EU) | EU B2B invoices must be Peppol-compliant; manual conversion is slow | paid | M | €34/mo; per-country obligation: Peppol format generation, SMP directory lookup | peppol-einvoicing |
| Privacy & Compliance Suite | We face GDPR fines, CCPA penalties, and ADA lawsuits for non-compliance | paid | L | EUR 250-500/year or EUR 600 one-off | privacy-compliance-suite |
| Reverse Charge | A German business tries to buy from my Greek shop and provide a VAT ID for reverse charge, but I have no field to capture it, no way to validate it, and no logic to apply B2B tax rules. | paid | M | $199–499/year | reverse-charge |
| Table Reservation Calendar (Restaurants) | Walk-in vs reserved tables need blocking; no system = double-bookings | paid | M | €34/mo; table map, availability sync to Google/Facebook | table-reservation-calendar |
Customers & accounts — 18 (11 core / 7 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| API Key Management | Developers need programmatic access but can’t securely rotate keys; leaked keys can’t be revoked without disrupting integrations. | core | M | free (core) | api-key-management |
| Admin Activity Log | Merchants don’t know who changed product prices or deleted orders; a disgruntled employee causes chaos without accountability. | core | M | free (core) | admin-activity-log |
| Admin User Management | Merchants can’t add team members to their store; everything happens through a single admin account. | core | M | free (core) | admin-user-management |
| Customer Notes | Staff can’t document customer interactions; each rep independently rediscovers the same preferences. | core | S | free (core) | customer-notes |
| Customer Preferences | Customers can’t control communication frequency; merchants email them daily and they unsubscribe. | core | M | free (core) | customer-preferences |
| Customer Profiles | Merchants can’t see a customer’s purchase history; VIP customers are treated like one-time buyers. | core | M | free (core) | customer-profiles |
| Invite Links | Merchants can’t easily onboard new team members; they must manually create accounts for each person. | core | S | free (core) | invite-links |
| Password Policy Enforcement | Merchants’ team members use weak passwords like ‘123456’; hackers break in through credential stuffing. | core | S | free (core) | password-policy-enforcement |
| Role-Based Access Control | Merchants can’t trust employees with full access; they need to limit who can see orders, refunds, or edit prices. | core | M | free (core) | role-based-access-control |
| Session Management | Merchants’ devices get stolen and their store stays logged in; attackers access the admin forever. | core | S | free (core) | session-management |
| Two-Factor Authentication | Merchants’ accounts get hacked via password guessing; attackers delete content or steal customer data. | core | M | free (core) | two-factor-authentication |
| B2B Commerce Platform | B2B selling requires approval workflows, negotiated pricing, and tiered customer groups | paid | XL | EUR 500-1500/year or EUR 2000-3000 one-off | b2b-commerce-platform |
| B2B suite (company accounts, quotes, requisition lists) | No company entity, no VAT ID field, no purchase orders, no requisition lists, no shared catalogs per customer. B2B merchants locked out. | paid | XL | €199–599/month | b2b-suite |
| Behavioral Segmentation | Different messaging for browsers vs buyers; need to segment by behavior | paid | M | €34/mo; behavioral triggers, segment drill-down, export | behavioral-segmentation |
| ERP & Accounting Bridge | Store and accounting system have different data; reconciliation is manual and error-prone | paid | XL | EUR 500-2000/year or EUR 1500-3000 one-off | erp-accounting-bridge |
| Email Marketing Automation & Segmentation | I lose repeat sales without automated email campaigns for order confirmations, recommendations, and win-back sequences | paid | XL | $29-99/mo; anchor: Klaviyo $20/mo tier entry | email-marketing-automation-segmentation |
| HubSpot CRM Sync | Sales team lives in HubSpot; need customer data sync from ecom | paid | M | €34/mo; credential: we maintain HubSpot API integration, daily sync | hubspot-crm-sync |
| Session Recording, Heatmaps & User Behavior | I don’t know why customers abandon carts; I need session replays and heatmaps to see where they drop off | paid | L | $19-49/mo; anchor: Clarity free or $25/mo MIDA | session-recording-heatmaps-user-behavior |
Marketing & email — 12 (4 core / 8 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Conversion Tracking & Attribution (Pixels, UTM, Analytics) | I can’t track which ads, emails, or social posts actually drove sales; attribution is broken | core | L | free (core basic pixel firing), paid module $9-29/mo for attribution | conversion-tracking-attribution |
| Email Newsletter Campaigns | I can send order confirmations but have no way to send marketing newsletters to my customer and subscriber list—I manually export to Mailchimp | core | L | free (core): template builder, send to subscriber list, open tracking; paid plans $25–60/mo for segmentation, automation, advanced A/B testing | email-newsletter-campaigns |
| Email Service & Templates | Merchants need reliable email delivery; they don’t write every email from scratch. | core | L | free (core) | email-service-templates |
| Landing Page Builder | My blog pages have sidebars and navigation that distract from campaigns—I need distraction-free promotional landing pages for email campaigns and ads | core | M | free (core): landing page templates and drag-drop builder; paid plans $20–50/mo for premium template library and advanced components | landing-page-builder |
| Affiliate Program Management | Merchants want influencers to sell; need affiliate signup, commission tracking | paid | M | €49/mo; 50 affiliates included, commission rules, payout management | affiliate-program-management |
| Built-in Blog & Content Hub | Blog is critical for SEO but a separate platform adds cost, complexity, and data silos | paid | M | EUR 150-300/year or EUR 400 one-off | built-in-blog-content-hub |
| Email Campaign Builder | Merchants send product emails by hand; need drag-drop templates, A/B testing | paid | M | €39/mo; 1000 emails/mo included, templates, A/B testing | email-campaign-builder |
| Klaviyo Email (Advanced Tier) | Email marketing needs advanced segmentation; Klaviyo is the standard | paid | M | €59/mo; credential: we manage Klaviyo integration, template library | klaviyo-email |
| Personalized Email Campaigns | Generic emails convert at 1%; personalized at 3-5%; need dynamic content | paid | M | €39/mo; dynamic content blocks, A/B testing, deliverability | personalized-email-campaigns |
| Referral Program | Existing customers refer friends; need to track referrals, automate rewards | paid | M | €29/mo; referral tracking, reward automation, fraud prevention | referral-program |
| SMS Marketing | Email, SMS, push notifications run on separate platforms with no unified workflow | paid | L | EUR 50-500/month SaaS (varies by contact volume) | sms-marketing |
| SMS, WhatsApp & Push Notifications | Email alone doesn’t reach customers; SMS gets 95%+ open rates but requires carrier relationships and compliance | paid | L | $25-49/mo + per-SMS usage tier (0.5-2 cents/SMS); anchor: Postscript $30/mo for starter | sms-whatsapp-push-notifications |
Reviews & social proof — 4 (3 core / 1 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Draft & Preview | Merchants can’t review content before publishing because there’s no draft mode; published mistakes cost lost sales and credibility. | core | S | free (core) | draft-preview |
| Product Reviews with Photos, Video & UGC | Customers won’t buy without seeing what others say and have verified they purchased | core | XL | free (core) | product-reviews-with-photos-video-ugc |
| Rating Display Component | Product reviews exist but customers don’t see the star rating on cards; merchants lose trust signals. | core | S | free (core) | rating-display-component |
| Google Reviews & Social Proof Display | I lose credibility when Google reviews aren’t visible on my storefront, and collecting them requires OAuth and API integration | paid | M | $9-15/mo, covers Google API credentials and review sync automation | google-reviews-social-proof-display |
Content & editorial — 21 (11 core / 10 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Bulk Content Import | Merchants migrating from other platforms must manually re-enter products and content because there’s no CSV import; they lose weeks to manual entry. | core | L | free (core) | bulk-content-import |
| Content Scheduling | Merchants want to publish seasonal content at specific times but must manually remember to hit ‘publish’ or ask developers to do it for them. | core | M | free (core) | content-scheduling |
| Content Versioning | Merchants accidentally overwrite content and can’t recover older versions; a team member’s mistake deletes product descriptions. | core | M | free (core) | content-versioning |
| EXIF Data Stripper | Merchants’ product photos leak location data and camera EXIF metadata when uploaded to the web. | core | S | free (core) | exif-data-stripper |
| Image Gallery Builder | Merchants need to show products from multiple angles with zoom; customers can’t verify product appearance before buying. | core | M | free (core) | image-gallery-builder |
| Image Lazy-Loading | Merchants’ pages load off-screen images; bandwidth wastes and checkout performance suffers. | core | S | free (core) | image-lazy-loading |
| Image Optimization | Merchants upload 5MB photos from their phone; product pages load in 8 seconds, and customers abandon carts. | core | M | free (core) | image-optimization |
| Image Resizing on Upload | Merchants must resize images in Photoshop before upload; non-technical team members can’t do it and upload at wrong dimensions. | core | S | free (core) | image-resizing-on-upload |
| Product Localization Framework | Merchants selling in 20+ language markets have no way to localize product descriptions, categories, and metadata; forcing all customers to read English reduces conversion 40%+ per language tier. | core | L | Free (core) | product-localization-framework |
| Rich Text Editor | Merchants waste hours formatting product descriptions and blog posts manually because they can’t apply bold, links, lists, or tables without switching tools. | core | M | free (core) | rich-text-editor |
| WebP Conversion | Merchants’ images load slowly because browsers don’t get modern formats; checkout abandonment climbs when images take 3 seconds. | core | S | free (core) | webp-conversion |
| A/B Testing & Content Optimization | I change a headline or call-to-action but don’t know if it improved click-through or conversions—I’m testing blind without statistical confidence | paid | M | $35–80/mo depending on test volume and traffic tier | a-b-testing-content-optimization |
| AI Product Description Generator | 500 products × 2 hours each = slow; use AI to generate drafts | paid | M | €29/mo; 1000 descriptions/mo, multi-language, keyword optimization | ai-product-description-generator |
| Automated Translation Service | Manually translating product descriptions into 20 languages or hiring translators for quarterly updates costs merchants €5k-50k annually per language tier; staying current is prohibitively expensive. | paid | L | €9.99/mo + €0.08-0.15/word translated (variable cost model) — owns the credential (translation quality) and per-country language commitment | automated-translation-service |
| Blog CMS with Auto-Linking | Blog posts don’t link to products; need auto-linking to increase internal traffic | paid | M | €24/mo; blog authoring, auto-internal-linking, SEO optimization | blog-cms-with-auto-linking |
| Content Migration Tools | I have 500 blog posts in WordPress or Medium and don’t want to manually recreate them—I’m stuck or need expensive agency work to migrate | paid | L | $50–150/mo for self-serve tools; or $1,000–3,000 one-time professional service | content-migration-tools |
| Content Syndication & Multi-Channel Publishing | My blog only reaches visitors to my site; I want new articles auto-published to Medium, LinkedIn, and our newsletter to grow audience reach without manual work | paid | S | $20–50/mo depending on destination-platform count | content-syndication-multi-channel-publishing |
| EyeCare Content Library (Optics) | Opticians compete on trust; pre-written eye-health content builds authority | paid | M | €19/mo; 500+ articles, optometry-reviewed, SEO-tagged | eyecare-content-library |
| SEO Optimization Suite | Getting indexed and ranked requires SEO expertise we don’t have | paid | L | EUR 300-400/year or EUR 500 one-off | seo-optimization-suite |
| SEO Optimizer | Products rank nowhere; need meta tags, schema markup, sitemap | paid | M | €29/mo; meta optimization, schema markup, sitemap, redirect management | seo-optimizer |
| Visual Page Builder | Customizing homepage and campaigns requires hiring a developer for every change | paid | L | EUR 200-400/year or EUR 500 one-off | visual-page-builder |
Analytics & reporting — 13 (0 core / 13 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Attendance Tracking (Education) | Manual roll calls are error-prone; schools need audit trail for compliance | paid | M | €19/mo; QR check-in, absence alerts, absence patterns | attendance-tracking |
| Business Intelligence & Analytics | I don’t know which products, customers, or regions drive profit | paid | L | EUR 250-400/year or EUR 500 one-off | business-intelligence-analytics |
| Cohort Analysis | Which customer cohort drives repeat revenue? Need cohort retention analysis | paid | M | €39/mo; retention curves, cohort drill-down, export | cohort-analysis |
| Custom Reports Builder | Dashboard shows sales, but merchant needs cohort analysis, LTV by channel | paid | M | €49/mo; 10 custom reports, email delivery, drill-down | custom-reports-builder |
| Expense Tracking | Supplier invoices are manual; need expense categorization for bookkeeping | paid | M | €24/mo; OCR receipt capture, expense categorization, approval workflows | expense-tracking |
| Lifetime Value Dashboard | CAC is €20 but LTV is €15; merchant overspends on ads without LTV visibility | paid | M | €34/mo; LTV calculation, payback period, segment drill-down | lifetime-value-dashboard |
| Mobile App Builder | App store presence is expected but custom iOS/Android development is too expensive | paid | XL | EUR 500-1500/year or EUR 2000-3000 one-off | mobile-app-builder |
| Part Warranty Tracking (Mechanics) | Warranty claims are manual; merchants lose audit trail for supplier disputes | paid | M | €24/mo; warranty claim tracking, supplier billing, dispute audit trail | part-warranty-tracking |
| Revenue Analytics, Profitability & Reporting | My Shopify dashboard hides profit data; I don’t know which products or channels are actually profitable | paid | L | $29-79/mo | revenue-analytics-profitability-reporting |
| Stripe Billing (Subscriptions Setup) | Subscriptions need complex billing logic; Stripe Billing automates it | paid | M | €29/mo; credential: we configure Stripe Billing, webhook management | stripe-billing |
| Student Enrollment & Progress Tracking (Education) | Schools need to track enrollment, fees, and student performance; spreadsheets fail at scale | paid | L | €54/mo; 500 students included, support for grades & attendance | student-enrollment-progress-tracking |
| Subscriptions & Recurring Billing | Subscriptions require billing cycle automation, retry logic, and compliance I can’t build myself | paid | XL | $39-99/mo + 2% transaction fee; anchor: Appstle $30 entry | subscriptions-recurring-billing |
| Technical Compatibility Matrix (Mechanics) | Part fits 2015 Civic but not 2016; merchants must maintain massive compatibility matrix | paid | L | €49/mo; 50M+ vehicle-part combinations, monthly updates | technical-compatibility-matrix |
Integrations & channels — 29 (10 core / 19 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| API Error Documentation | Developers get HTTP 422 errors and don’t know what field caused it; debugging takes hours. | core | M | free (core) | api-error-documentation |
| API Rate Limit Info | Developers hit rate limits and don’t know why; they can’t predict API quotas. | core | S | free (core) | api-rate-limit-info |
| Backup Restoration | Merchants can’t recover from corruption or hacks; they’ve lost all data with no way to restore. | core | L | free (core) | backup-restoration |
| CSV Export | Merchants need to analyze data in spreadsheets or share with accountants; there’s no CSV export. | core | M | free (core) | csv-export |
| GraphQL API | Developers want flexible queries but the API is REST-only; they fetch too much data or make N+1 requests. | core | XL | free (core) | graphql-api |
| JSON Export | Merchants want to archive data in a portable format; they’re locked into AstroBaaS with no export option. | core | M | free (core) | json-export |
| REST API Documentation | Developers can’t use the API because there’s no documentation; they reverse-engineer endpoints. | core | L | free (core) | rest-api-documentation |
| Scheduled Backups | Merchants have no backups; a server crash or ransomware attack loses years of customer data. | core | L | free (core) | scheduled-backups |
| Webhook Management | Developers need event notifications but can’t register webhooks; they poll the API constantly. | core | M | free (core) | webhook-management |
| Webhook Testing Tool | Developers can’t test webhooks locally; they deploy to production to debug. | core | M | free (core) | webhook-testing-tool |
| Bulk Data Import & Migration | We’re stuck with spreadsheets; bulk product import from CSV/XML requires coding | paid | L | EUR 200-400/year or EUR 500 one-off | bulk-data-import-migration |
| CDN Integration | Customers in Australia load images slow from Greece; need CDN | paid | M | €34/mo; credential: we manage CDN (Cloudflare/Fastly), cache optimization | cdn-integration |
| Facebook Shop Sync | Sell on Facebook without maintaining two inventories; need sync | paid | M | €29/mo; credential: Facebook API integration, daily catalog sync | facebook-shop-sync |
| Google Shopping Feed | List products on Google Shopping; need XML feed with product attributes | paid | M | €19/mo; credential: Google Merchant Center integration, daily feed sync | google-shopping-feed |
| Helpdesk Integration | Support emails land in inbox; no ticketing, SLA, or assignment | paid | M | €29/mo; ticketing, SLA tracking, KB search integration | helpdesk-integration |
| Instagram Shopping | Instagram users want to buy; tagging products drives social commerce | paid | M | €24/mo; credential: Instagram Shop API, product tagging, order sync | instagram-shopping |
| Insurance Integration (Optics) | Patients expect to use insurance; merchant must verify coverage before sale | paid | M | €44/mo; credential: per-country insurance APIs (Greece, Germany, etc.) | insurance-integration |
| Legacy System Migration | Merchants move from old ecom to AstroBaaS; data is messy | paid | L | €399 one-time; support commitment: data ETL, cleanup, validation | legacy-system-migration |
| Marketplace & Social Shop Sync (Facebook, TikTok, Google) | I miss sales by not selling on social platforms; manual listing sync to Facebook/TikTok/Google Shop is broken | paid | L | $0-19/mo platform fee (channels pay their own commission) | marketplace-social-shop-sync |
| Marketplace Channel Sync | Selling on Amazon and eBay means manually syncing inventory, orders, and prices | paid | L | EUR 300-600/year or EUR 800 one-off | marketplace-channel-sync |
| Marketplace Feed Syndication | Manual feed uploads to Google Shopping, Facebook, TikTok waste hours every week | paid | M | EUR 200-350/year or EUR 400 one-off | marketplace-feed-syndication |
| Multi-Vendor Marketplace Platform | We want to become a marketplace with seller commissions but managing vendors is complex | paid | XL | EUR 1000-3000/year or EUR 5000-10000 one-off | multi-vendor-marketplace-platform |
| PMS Integration (Hotels) | PMS (Opera, Fosse) must sync with AstroBaaS; manual sync loses bookings | paid | L | €59/mo; credential: we maintain your PMS API integration, daily reconciliation | pms-integration |
| Shopify Import/Migration | Merchants want to leave Shopify; migration is manual and risky | paid | L | €249 one-time + €59/mo (6 month support); support commitment: we handle your Shopify export, data cleaning, testing | shopify-import-migration |
| Slack Notifications | Team needs instant alerts for high-value orders, returns, chargebacks | paid | M | €14/mo; credential: Slack workspace integration, alert customization | slack-notifications |
| TikTok Shop | TikTok Shop is growing; need to list products and handle orders | paid | M | €34/mo; credential: TikTok Shop API integration, order import | tiktok-shop |
| VIN Decoder Integration (Mechanics) | Customers don’t know exact parts; typing VIN auto-fills engine, trim, year | paid | M | €24/mo; credential: we license NHTSA/Edmunds VIN database | vin-decoder-integration |
| Xero/Netsuite ERP Sync | Accounting must match orders; need two-way sync with ERP | paid | L | €59/mo; credential: we maintain ERP API, daily reconciliation | xero-netsuite-erp-sync |
| Zapier/Make.com Connector | Merchants use Zapier/Make for business logic; need native integration | paid | M | €24/mo; credential: we publish official Zapier/Make app, webhook support | zapier-make-com-connector |
Storefront & headless — 5 (5 core / 0 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| JavaScript SDK | Developers manually make HTTP calls to the API; they reinvent request signing, error handling, and pagination. | core | L | free (core) | javascript-sdk |
| Product Card Component | Merchants rebuild product cards from scratch in every storefront; teams waste weeks on identical components. | core | M | free (core) | product-card-component |
| Quantity Selector | Customers type invalid quantities or negative numbers; merchants process invalid orders. | core | S | free (core) | quantity-selector |
| TypeScript SDK | TypeScript developers lack type definitions; they lose autocomplete and type safety. | core | L | free (core) | typescript-sdk |
| Wishlist / Save for Later | Customers can’t save products to check prices later; merchants lose abandoned browsing for retargeting. | core | M | free (core) | wishlist-save-for-later |
Operations & platform — 17 (3 core / 14 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Downloadable products with entitlement | Downloadable schema exists (downloadable, downloads) but delivery does not. No entitlement ledger, no signed download links, no count limits or expiry. | core | M | Core | downloadable-products-with-entitlement |
| Notification Preferences | Customers get bombarded with emails and can’t opt out of specific types; they mark everything as spam. | core | M | free (core) | notification-preferences |
| Scheduled sales execution on schedule | Sales are derive-on-write. A Black Friday price scheduled for tomorrow stays at old price until someone manually saves the product. | core | S | Core | scheduled-sales-execution-on-schedule |
| Advanced Product Customizer (3D, AI Design Tools) | Text customization isn’t enough; I need customers to preview designs in 3D or use AI to generate custom artwork | paid | M | $25-49/mo | advanced-product-customizer |
| Advanced Product Options | Simple variants aren’t enough; customers want to customize products visually | paid | M | EUR 200-350/year or EUR 450 one-off | advanced-product-options |
| Advanced Wishlist & Registry | Wishlists are an afterthought but could drive repeat visits and gift registry sales | paid | M | EUR 150-300/year or EUR 350 one-off | advanced-wishlist-registry |
| Backup & Disaster Recovery | Data loss = business death; merchants need daily backups, recovery SLA | paid | M | €29/mo; daily backups, 30-day retention, recovery SLA (4 hours) | backup-disaster-recovery |
| Digital Assignment Platform (Education) | Teachers email assignments; no submission tracking, late work, peer review support | paid | M | €34/mo; rubric grading, plagiarism detection (Turnitin integration) | digital-assignment-platform |
| Dynamic Product Badges | Static product images don’t highlight promotions, bestsellers, or low-stock warnings | paid | S | EUR 100-200/year or EUR 250 one-off | dynamic-product-badges |
| Guest Author Management & Revenue Payouts | I want to publish guest content and share revenue with authors, but I have no system to track bylines, unpaid contributions, or manage payouts to multiple contributors | paid | M | $40–100/mo, or commission 5–10% on author earnings routed through platform payments | guest-author-management-revenue-payouts |
| Housekeeping Workflow (Hotels) | Staff doesn’t know which rooms are dirty/ready; checkout delays, no-show tracking fails | paid | M | €29/mo; room-status dashboard, inspector sign-off, maintenance alerts | housekeeping-workflow |
| Knowledge Base | FAQs reduce support load 30%; self-service is cheaper than email | paid | M | €24/mo; article authoring, search, category management | knowledge-base |
| Live Chat | Customers bounce without talking to someone; live chat converts 20%+ better | paid | M | €34/mo; visitor tracking, chat routing, canned responses | live-chat |
| Multi-store with shared admin | Multi-store refused by design (WP-PARITY-ROADMAP.md:334). One instance per site. A merchant running 3 brands gets 3 deployments, 3 databases, 3 admins to keep in sync. | paid | XL | €99–299/month per additional store | multi-store-with-shared-admin |
| Scheduled Tasks | Run inventory forecast at 6am, send daily sales summary email | paid | M | €24/mo; unlimited scheduled tasks, timezone support, logging | scheduled-tasks |
| Subscriptions | Merchants want recurring revenue; subscriptions need billing, retry logic, cancellation workflows | paid | L | €39/mo; 2% per recurring transaction or €199/mo flat; support for dunning, tax recalc on each cycle | subscriptions |
| Workflow Builder (Automation) | When order >€500, alert warehouse + sales + finance; manual processes fail | paid | L | €39/mo; 50 workflows included, condition builder, webhook support | workflow-builder |
Retail & POS — 2 (0 core / 2 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Cancellation & Deposit Policies (Hotels) | Strict vs flexible policies; non-refundable bookings must enforce dates, automate refunds | paid | M | €24/mo; policy enforcement, automatic refund/void reversal, dispute audits | cancellation-deposit-policies |
| Retail Point of Sale | We operate retail stores and e-commerce separately with no inventory sync | paid | XL | EUR 1000-2000/year or EUR 2500-5000 one-off | retail-point-of-sale |
Vertical packs — 13 (0 core / 13 paid)
| Item | Problem | Tier | Size | Suggested price | Plan |
|---|---|---|---|---|---|
| Class Scheduling & Booking (Education) | Students can’t book classes; school has no visibility into occupancy by date/time | paid | M | €29/mo; calendar sync, no-show prevention, class-size limits | class-scheduling-booking |
| Core Exchange Management (Mechanics) | Used parts refurbishing needs deposits, return tracking, core credit reversal | paid | M | €34/mo; core deposit enforcement, return logistics, credit reconciliation | core-exchange-management |
| Guest Preferences & History (Hotels) | Repeat guests want same room, bed type, floor; manual notes get lost | paid | M | €24/mo; preference profiles, allergy tracking, VIP flags | guest-preferences-history |
| Installation Guides (Mechanics) | Customers lack confidence; installation guides reduce returns 30% | paid | M | €29/mo; 10k+ video guides, categorized by part type | installation-guides |
| Menu Versioning by Date/Time (Restaurants) | Lunch menu differs from dinner; seasonal items, limited-time specials | paid | M | €24/mo; menu scheduling, item availability by time, allergen tagging | menu-versioning-by-date-time |
| Parent Notifications (Education) | Parents miss announcements sent via email; need SMS/push for urgent alerts | paid | M | €24/mo; SMS, email, push; integration with attendance/grades | parent-notifications |
| Party Size & Seating Validation (Restaurants) | Table 2 can’t seat party of 8; need smart seating, no overselling | paid | M | €19/mo; table capacity rules, seating suggestions, turn-time optimization | party-size-seating-validation |
| Pre-Arrival Guest Communication (Hotels) | Guests arrive without parking details, wifi password, check-in time; customer service load | paid | M | €19/mo; SMS/email pre-check-in, QR check-in, property details | pre-arrival-guest-communication |
| Prescription Capture & Validation (Optics) | Opticians need prescription data at checkout; online RX capture improves conversion 40% | paid | M | €34/mo; credential: we validate RX format, integrate with EuroOptix | prescription-capture-validation |
| Staff Scheduling (Restaurants) | Who’s working tonight? Manual scheduling is error-prone, leads to understaffing | paid | M | €29/mo; shift scheduling, labor-cost analytics, shift-swap requests | staff-scheduling |
| Student/Parent Portal (Education) | Parents want to see grades & attendance; manual reporting is slow | paid | M | €29/mo; real-time grades, attendance, parent notifications | student-parent-portal |
| Virtual Try-On (Optics) | Online glasses shopping has 40% return rate; AR try-on cuts returns to 15% | paid | L | €49/mo; AR/WebGL, face detection, support for 500+ frames | virtual-try-on |
| Waitlist Management (Restaurants) | No tables now; need to capture walk-ins, offer wait slots, SMS when ready | paid | M | €24/mo; SMS notifications, wait-time estimates, no-show management | waitlist-management |
The six decisions
GraphQL for AstroBaaS — the second API surface is not worth it yet
No — don’t build GraphQL now; the typed client stops before commerce, so the first API surface isn’t finished yet, and every authorization, rate-limit, CSRF and public-read control in this codebase is keyed on URL path, which a single /api/graphql endpoint collapses into one bucket that today’s scoped API keys would flatly refuse. Confirmed no GraphQL in the repo: the only four hits are documentation, and two of them are the project already deciding this — VISION.md:109 “GraphQL (only if the audience asks; REST + SDK likely suffices)” and docs/ROADMAP.md:179 listing it under things deliberately not built. The user problem worth naming is not “we lack GraphQL”: it is that a storefront developer building the checkout path today writes untyped client.request(‘GET’, ‘/api/products’) calls, because /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/client/index.ts (640 lines) types posts, content, keys, webhooks, audit, auth and locales — and has no products, orders, categories, coupons, shipping or customers namespace at all.
Options
- (a) Full hand-written schema and resolvers over the service layer — Re-express the 85 documented REST paths as GraphQL types, queries and mutations resolving against src/lib/*-service.ts. The complete Shopify-shaped answer: a buyer who asks for GraphQL gets what they meant.
- Cost: Months, then permanent. Schema plus resolvers plus DataLoaders plus depth and complexity limiting plus persisted queries plus a SECOND authorization implementation, plus a second contract-test suite, plus a second set of docs — on a one-maintainer project. Every endpoint added after this becomes two pieces of work forever.
- For: Idiomatic and recognisable. Removes the sales objection completely. Field selection, no waterfalls, one round trip per page, introspection that AI codegen tools consume well.
- Against: This is the option the repo’s own history argues against hardest. src/lib/api-key-scopes.ts is deny-by-default per path: requiredScopeFor(method, pathname) returns null for /api/graphql, so scopedKeyAllowed returns false and EVERY scoped key — the exact credential a headless storefront is told to use — is denied at the endpoint on day one. The tempting fix (adding ‘graphql’ to SCOPED_RESOURCES) makes one ‘graphql:read’ token grant read across posts, content, media, products, orders, customers and messages simultaneously, deleting the least-privilege model via a URL. Doing it correctly means per-resolver scope and capability checks — a parallel authz path in a codebase whose stored memory names ‘one sibling fixed, one not’ and the call-site sweep as its recurring bug shape. Rate limiting breaks too: src/middleware.ts:75-76 counts REQUESTS (60/min per IP default, separate per-key and per-user ceilings), and one GraphQL POST decrements that by one no matter what it asks for.
- (b) Generate the schema from the existing OpenAPI document — Point a generator at /openapi.json and serve the result. Cheapest path to a GraphQL endpoint existing.
- Cost: Days to stand up, then a permanent public contract you cannot change. The illusion of cheapness is the trap.
- For: Fast. Automatically tracks the REST doc, so the sync problem appears solved.
- Against: Rejected on evidence, not taste. src/pages/openapi.json.ts states at the top that it is ‘Kept hand-curated (not generated)’ — and tests/openapi.test.mjs only verifies that every documented path and method resolves to a route file exporting that method. Nothing validates response SHAPES. So the generated schema is exactly as accurate as someone’s memory, and every stale field becomes a typed lie a storefront compiles against — the ‘No mock data ever’ rule violated at the schema level. It also inherits the {success, data} envelope from src/lib/api-response.ts, producing REST-with-extra-steps that satisfies nobody who asked for GraphQL. And money: the doc types price_cents, total_cents and amount_cents as OpenAPI ‘integer’, which a generator maps to GraphQL Int — a signed 32-bit value capping at 21,474,836.47 EUR. Fine per order, silently wrong on an aggregate, and it is precisely the ‘Integer-only money arithmetic’ rule that a hand-written custom Money scalar would have protected.
- (c) Narrow READ-ONLY Storefront GraphQL — catalogue and content only, writes stay REST — One /api/storefront/graphql endpoint, no mutations, exposing only what src/middleware.ts already treats as public read: products, categories, brands, posts, and content types whose definition says public. Checkout, forms and every write keep using the audited REST routes.
- Cost: Weeks, not months, IF it is genuinely held to read-only and persisted-queries-only. Realistically: schema, DataLoaders for the N+1 on category and media joins, a depth and complexity budget, a persisted-query allow-list, and a drift test against the REST routes. Ongoing maintenance is one more thing to keep in sync on every catalogue field added.
- For: The only defensible version. Read-only kills the hardest problems by construction: no mutations means no CSRF question and no public-write gate to re-implement, and restricting to already-public data collapses authorization from ‘map 7 capabilities across 200 fields’ to ‘is this content type public’ — a question src/pages/api/content/changes.ts already answers with care. It is also what a storefront actually needs.
- Against: Be honest about where this lands: once you require persisted queries — and for a public storefront endpoint you must, because that is what restores a finite request set you can rate-limit and cache — you have re-invented REST endpoints with a nicer authoring experience. That is the real value proposition, not a criticism, but it also explains why option (d) captures most of the same benefit for a fraction of the surface area. Separately: it does not answer a buyer who wants GraphQL MUTATIONS, so the sales objection is only half-closed.
- (d) Invest in the typed REST client and richer field selection — Finish the client that already exists: add the commerce namespace, add server-side field projection so a storefront stops fetching 40 fields to render 6, add expansion where the live storefronts actually make two calls, and put ETag and Cache-Control on public catalogue reads.
- Cost: Weeks. No new surface to secure, document, rate-limit or audit — the existing path-keyed middleware keeps working unchanged, which is the whole point.
- For: It fixes the problem that actually bites: src/client/index.ts has no products, orders, categories, coupons, shipping or customers, so the storefront work being sold at ~3K EUR is done against untyped request() calls today. There is also free performance sitting unclaimed — grep across src/pages/api finds Cache-Control on exactly two schema endpoints (commerce/frame-schema.ts:47, commerce/prescription-schema.ts:43) and no ETag anywhere, so catalogue GETs are uncached. And src/pages/api/content/changes.ts already exists as a change feed, which is the ISR revalidation story a headless Next.js storefront needs more than query flexibility.
- Against: Does not answer the buyer whose checklist says ‘GraphQL’. That objection is real and this option does not remove it — it only makes it cheap to answer with a document. There is also a drift risk: a fields= parameter plus expansion parameters plus filters can grow into a bespoke query language that is worse than GraphQL and undocumented. Bound it explicitly.
Recommendation. Take (d) now, and pre-write (c) as the escape hatch without building it.
Quoting the owner’s own rules back, because they settle this more cleanly than any technical argument: “NO licence gating ever — the free/paid line is drawn by SEAMS (core owns the interface + an honest hand-modelled implementation; a paid pack owns the credential, the per-country obligation, or the support commitment).” GraphQL has no credential and no per-country obligation. So under this rule GraphQL can never be a paid pack — it is free core or nothing, which makes it pure cost with no seam to monetise. The market confirms it: Shopify’s Storefront API is GraphQL and is bundled into every plan (Basic was around USD 39/month at last check — that is an anchor, not a recommendation, and verify it before quoting it to anyone), and Magento Open Source ships GraphQL for free under OSL-3.0. Nobody has ever bought a protocol. What IS sellable next to it is the managed AstroBaaS, where you run the query-cost limiter and the persisted-query allow-list on someone else’s behalf — a support commitment, which is a legitimate seam under the rule — and the frontstore services themselves, where a typed client raises the margin on the ~1K EUR sites and ~3K EUR eshops already being sold.
The other rule that matters here: “Both live storefronts are HEADLESS Next.js — an SSR-only feature does not exist.” GraphQL passes that test, which is exactly why it is tempting and why this deserves a written answer rather than a shrug.
The trigger for building (c), written down in advance so it is not decided under pressure: a paying buyer names GraphQL as a blocking requirement in writing, OR a frontend team outside the owner’s control needs to ship without waiting on a client release per field. Not the first time someone asks in a sales call. When that trigger fires it is (c) and only (c): a separate /api/storefront/graphql path so the path-keyed middleware still functions, read-only, persisted queries only, catalogue plus public content.
FIRST SHIPPABLE SLICE: the commerce namespace on the typed client — baas.products, baas.categories, baas.brands, baas.orders (including the guest-checkout POST that already ships), baas.coupons, baas.shippingMethods — mirroring the existing readonly posts shape in /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/client/index.ts and typed off src/core/models.ts, plus a test extending tests/openapi.test.mjs in the reverse direction (every client method’s path must appear in the spec). One PR, days not weeks, and it removes the concrete reason a storefront developer reaches for GraphQL in the first place.
Work, in order
- Write docs/GRAPHQL.md — one page: ‘AstroBaaS has no GraphQL, here is why, and here is what you write instead.’ Show the query a Shopify-trained developer would have written next to the typed-client call that replaces it. docs/ROADMAP.md:179 already states the position; this turns it into the sales artefact that answers the objection every time it appears, for the cost of a day.
- Ship the commerce namespace on the typed client (THE FIRST SLICE). Add products, categories, brands, orders, coupons and shippingMethods to /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/client/index.ts, mirroring the existing posts namespace shape and typed off src/core/models.ts. Include the anonymous POST /api/orders guest checkout that already ships. Extend /Users/Theo/Projects/AstroCMS/AstroCMS_v1/tests/openapi.test.mjs with the reverse check: every path a client method calls must exist in src/pages/openapi.json.ts.
- Add a fields= projection parameter to the public read routes — GET /api/products, GET /api/posts, GET /api/content/{type} — projected SERVER-side. Allow-list it against declared fields using the pattern already at /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/pages/api/content/[type]/index.ts:79 (declaredNames), so a caller can never name _status or any staff-only field. Reject an undeclared field with a 400 rather than ignoring it silently.
- Audit the two live Next.js storefronts for their actual request waterfalls BEFORE adding any expansion parameter, and add only the embeds those real pages need. /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/pages/api/products/index.ts already returns a fat response (localised products, embeds, order limits and media base in meta), so speculative expansion would add surface for nothing.
- Add ETag and Cache-Control to public catalogue and content GETs, keyed off the same updated_at the change feed uses. Today grep finds Cache-Control on only two schema endpoints and no ETag anywhere in src/pages/api — this is the largest TTFB win available and it is the one thing a GraphQL POST endpoint would have cost you.
- Document /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/pages/api/content/changes.ts as the ISR revalidation story for headless Next.js, with a worked revalidateTag example. The route already handles public visibility carefully (PUBLIC_CORE_TYPES plus contentTypeIsPublic for custom collections); it just is not sold as the answer it is.
- Write the (c) design document without building it: schema scope, the persisted-query allow-list mechanism, the depth and complexity budget, and how a custom Money scalar keeps the integer-only rule (never GraphQL Int, which caps at 21,474,836.47 EUR when the unit is cents). Costs a day now; stops a rushed GraphQL being designed under buyer pressure later.
- Add a guard test that fails if any new file under src/pages/api introduces a path the OpenAPI doc does not document. The sync discipline that makes a second API surface survivable should be proven on the first surface before a second one is ever considered.
Risks. SALES RISK, stated plainly: option (d) does not remove the objection. A buyer comparing against Shopify will ask, and ‘we have a typed REST client’ loses that conversation unless the one-page document exists. The mitigation is the document, and it is cheap. If you lose deals on this specific point more than twice, that is the trigger firing and you should build (c).
THE FIELDS= PARAMETER IS A NEW LEAK SURFACE. Field projection is the one item in this plan that touches authorization. It must be allow-listed against declared fields, never a denylist, and it needs a negative test proving that a request naming a staff-only or underscore-prefixed field is REJECTED rather than quietly dropped — the codebase’s own recurring-bug memory names ‘early gates’ and ‘verification that cannot fail’ as repeat offenders, and a projection test that passes because the field happened to be absent anyway is exactly that shape.
DRIFT INTO A BESPOKE QUERY LANGUAGE. fields= plus expand= plus filters plus sorts, accreted over a year, becomes a worse GraphQL that nobody documented. Bound it now: field selection plus the specific expansions the two live storefronts demonstrably need, nothing speculative. If a third orthogonal query dimension is ever proposed, that is the signal to reconsider (c) rather than to add it.
IF GRAPHQL IS EVER BUILT, TWO THINGS WILL BE GOT WRONG BY DEFAULT. First, scoped API keys: requiredScopeFor in src/lib/api-key-scopes.ts returns null for any unmapped path and scopedKeyAllowed treats null as DENY, so the endpoint is dead to scoped keys until per-resolver scope checks exist — and the shortcut fix silently grants cross-resource read. Second, rate limiting: src/middleware.ts counts requests, not cost, so a single deeply nested query is free. Neither is discovered by a test that only checks the endpoint returns 200.
DEFERRING COMPOUNDS ONLY IF THE DESIGN IS ALSO DEFERRED. Building (c) later is fine; designing it later, under a deadline from a buyer who is already unhappy, is how the second API surface ships without persisted queries and without a complexity budget. That is why the design document is in the work list even though the implementation is not.
UNVERIFIED — I did not confirm these and did not assume them: whether related PRODUCTS exists (related POSTS ships), and how many round trips the two live Next.js storefronts actually make per page. Step 4 exists precisely because that measurement has not been taken and should not be guessed.
Headless storefront toolkit, and what to charge for the skeleton
Build an SDK, not a framework — the whole toolkit is interface, so by the owner’s own seam rule every piece of it is free GPL core, and the highest-value gap is the preview-plus-cache-invalidation contract that no headless CMS gets right. On the business question: give the skeleton away as the lead magnet for the managed hosting, sell paid vertical themes on top of it, and reposition the €1,000 tier as setup-plus-hosting — the €3,000 eshop work is not what the skeleton replaces, and a paid commercial-use licence is ruled out by “NO licence gating ever.”
Options
- A — Free skeleton, paid support and maintenance subscription — Ship the starter under the same GPL-3.0 as the core. Sell a recurring “storefront care” plan on top: framework-upgrade PRs, security patches, a response-time commitment, and a compatibility promise when AstroBaaS makes a breaking change. The code is free; being able to phone someone is not.
- Cost: Low to build (the licence is the one you already have). High and open-ended to run: a support commitment is a standing obligation on someone else’s production shop, and the owner is currently the entire support team.
- For: This is the only one of the five that the owner’s own seam rule directly blesses — “a paid pack owns the credential, the per-country obligation, or the SUPPORT COMMITMENT.” A skeleton has no credential and no per-country obligation, so support is the seam that legitimately exists here. No licence friction, no policing, maximum adoption. Revenue is recurring.
- Against: Sells nothing until there is a user base — a support subscription with four customers is a pager, not a business. Anchors are also weak: nobody pays much for support on a template. Answers the adoption question but does not, on its own, answer “how will I make money.”
- B — Paid commercial-use licence (free for one site, paid for agencies) — Release the skeleton under a source-available licence: free for personal or single-site use, a paid licence the moment it is used for a client or a second site.
- Cost: Low to build, but it buys a permanent enforcement job — checking who is using it, chasing people who aren’t paying, and answering “does my case count” forever.
- For: Directly monetises the exact behaviour the owner is worried about (another agency doing his €3,000 job with his template). Superficially the most “fair” option.
- Against: RULED OUT by the owner’s own rules. “NO licence gating ever — the free/paid line is drawn by SEAMS” is the rule, and a commercial-use licence is licence gating with an invoice attached rather than a code check. It is also unenforceable at this scale (nobody is auditing a German UG’s template licence), and it poisons exactly the adoption the skeleton exists to create — developers route around source-available starters on principle. Kill this one and say why.
- C — Open skeleton, paid vertical themes on top — The free starter is the PLUMBING: data fetching, cart, cache invalidation, preview, SEO, images. The paid products are finished, designed storefronts for the five planned verticals — optician, frontistirio, mechanic/parts, hotel, restaurant. Shopify Theme Store shape: free platform, paid themes.
- Cost: High per theme — design is the expensive part and cannot be generated. Realistically 2–4 weeks for the first vertical theme, less for each subsequent one as the shared layer settles. Ongoing: every theme is a maintenance obligation against Next.js majors and AstroBaaS API changes.
- For: The design work IS the differentiated part, which is the honest answer to “what am I actually selling.” Maps 1:1 onto verticals the owner already planned, and the optics vertical already has two live shops to extract from. Each theme sale is also a qualified lead for paid customisation at the 1K/3K rates. ANCHOR (not a recommendation, and approximate from a May 2026 cutoff — verify before publishing): Shopify Theme Store paid themes sit roughly in the USD 140–500 one-time band; ThemeForest ecommerce templates commonly USD 30–150 one-time; Tailwind Plus all-access is around USD 299 personal / USD 999 team one-time. A €150–€400 one-time vertical theme is defensible against that.
- Against: One-time revenue against an ongoing maintenance treadmill — five verticals is five permanent upgrade obligations funded by a single payment each. Theme sales at €150–€400 will never replace a €3,000 project; the arithmetic requires volume the owner does not have yet.
- D — Skeleton as the lead magnet for the managed hosting — The skeleton is free and deliberately excellent.
npx create-astrobaas-storefrontscaffolds a real shop in minutes and the default happy path deploys it onto the managed AstroBaaS being built next week. Self-hosting stays a first-class documented option beside it. Revenue is the hosting subscription, not the code.- Cost: The skeleton itself is moderate — it is an extraction from two shops that already work, not a greenfield build. The real cost is the hosting: uptime, backups, tested restores, on-call, and an incident process. That obligation has not been scoped anywhere in this conversation and is the load-bearing unknown.
- For: Recurring revenue that compounds without consuming the owner’s hours — the only line here that does. The skeleton’s job becomes conversion rather than sales, so it can be given away without hesitation, which in turn makes it good, which is what drives adoption. Hosting is already being built, so the skeleton makes an existing investment pay. ANCHOR (approximate US list prices, verify): Shopify Basic USD 39/mo, Shopify USD 105/mo, Advanced USD 399/mo, Plus from roughly USD 2,300/mo. Adobe Commerce is quote-only and commonly cited in the USD 22,000–125,000/yr licence band. A managed AstroBaaS shop at €29–99/mo is credible in that field with room underneath Shopify.
- Against: A free skeleton that deploys anywhere means the hosting has to win on merit rather than lock-in — which is correct but harder. Hosting is an operational business with a different risk profile from selling code, and one bad restore costs more trust than ten theme sales earn. Slowest of the five to produce its first euro.
- E — Free skeleton with productised fixed-price service tiers — Give the skeleton away and publish a fixed “starting from” price list for the parts nobody wants to do themselves: catalogue migration from WooCommerce/Shopify, payment and shipping-zone configuration, VAT setup, launch, design customisation.
- Cost: Near zero to build — this is mostly writing a price page, since the services business already exists.
- For: Converts the skeleton’s readers directly into the existing 1K/3K pipeline instead of competing with it. Makes the value the owner already sells explicit, which is worth doing regardless of which option wins.
- Against: Not really a fifth option — it is the current services business with a landing page, and it still trades hours for euros with no compounding. Should be adopted alongside whatever else wins, not instead of it.
Recommendation. ON (1) — BUILD AN SDK, NOT A FRAMEWORK.
The honest answer to “can we make a framework to improve on this” is: do not. An AstroBaaS-specific rendering framework competes with Next and Nuxt and loses, and it puts the interface where the developer’s own framework already sits. Shopify’s Hydrogen is the counter-example worth naming — it works only because Oxygen hosting funds it, which is precisely the position option D would put the owner in, and he is not there yet. Build an SDK, adapters, and one real starter.
The rule that decides what belongs in core: “core owns the interface + an honest hand-modelled implementation; a paid pack owns the credential, the per-country obligation, or the support commitment.” Every item in the toolkit is interface. There is no credential in a cart helper and no per-country obligation in a srcset builder. So the ENTIRE toolkit — SDK, cache-tag vocabulary, preview contract, codegen, dev proxy, and one working starter — is free GPL core. There is nothing here to sell, and trying to find something would mean drawing a fake seam.
The wedge worth building first is the PREVIEW + REVALIDATION CONTRACT. Every headless CMS is bad at exactly this pair, and together they are what makes a headless shop feel like WordPress to the person editing it: change a price, see it live; open a draft, see it on the actual storefront. AstroBaaS already has the hard half of both — webhooks fire product./order./post.* HMAC-signed with a delivery log, and quote() prices a basket with the same function checkout uses. What is missing is the receiving end and a preview handoff. That gap is a few hundred lines, and closing it is more differentiating than any framework.
Two things already ship and should not be rebuilt: guest checkout ships (anonymous POST /api/orders, audited), and product reviews ship as a moderated content type with verified_buyer — the starter should surface reviews through content(‘review’), not invent a review system. Related posts ships too (/api/posts/{slug}/related, and posts.related() is already in the SDK).
I could not find any preview-token or storefront-preview-URL mechanism, any cache-tag or revalidation concept, or any commerce method in the client SDK. I am treating all three as absent; if something exists under a name I did not grep for, that changes step 4, 7 and 8 but not the shape.
ON (2) — RECOMMEND OPTION D, WITH C STACKED ON TOP. FREE SKELETON, PAID VERTICAL THEMES, MANAGED HOSTING AS THE REVENUE LINE.
Concretely: one free GPL Next.js starter extracted from the two real optical shops; paid vertical themes at a one-time price anchored to the Shopify Theme Store band; managed hosting as the recurring line the skeleton feeds. Option B is ruled out by the owner’s own “NO licence gating ever.” Option A is right in principle but sells nothing until there is a user base. Option C alone cannot replace project revenue at €150–€400 a sale. Option E should be adopted alongside regardless.
WHAT THIS DOES TO THE 1K/3K SERVICES BUSINESS — the straight answer.
The fear is real but aimed at the wrong target. What gets paid for at €3,000 is not the skeleton. It is discovery, the client’s own content, design decisions, catalogue migration, payment and shipping-zone configuration, VAT, launch, and a person to call when the shop breaks on a Friday. The skeleton is maybe 2–4 days of that job, and it is the least differentiated and least enjoyable part. Releasing it does not remove the reason someone pays €3,000 — and it removes those days from the owner’s OWN delivery, so margin on the same €3,000 goes up.
The €3,000 eshop tier is therefore largely safe and probably helped. The €1,000 business-website tier is the one at genuine risk: a free skeleton plus a paid theme puts a competent developer within a weekend of it. The response is to stop selling €1,000 as a build and start selling it as €1,000 setup PLUS managed hosting. That converts the most-threatened line from one-time to recurring, which is an improvement disguised as a threat.
One more thing worth saying plainly: at €1,000/€3,000 the owner is not competing with agencies who would use his skeleton. He is competing with Wix, Shopify themes, and local freelancers. Agency-grade Magento storefront builds start around EUR 15,000 — a different market entirely. A skeleton barely moves his actual competitive set, while every agency that ships an AstroBaaS storefront with it grows the installed base that the paid vertical modules and the hosting sell into. The real cannibalisation risk is not the skeleton; it is being the second-best option for people who found him through it. The mitigation is to make the free skeleton genuinely good and the vertical themes visibly better.
All pricing figures above are ANCHORS, not recommendations, and they come from a May 2026 knowledge cutoff. Verify every one before it goes on a price page.
Work, in order
- Get the OptikiGwnia storefront into git with a remote, before anything else. docs/INSTANCE-FRONTEND-AUDIT.md found it is not a repository: the VPS holds a built standalone artifact and the 57 source files exist on one laptop. It is the extraction source for the free starter, so today a dead disk deletes the plan. Same check on gr-optics.com.
- Publish the client as its own npm package (@astrobaas/client), dependency-free bundle, CI publish on tag. INTEGRATION.md says plainly that
npm install astrobaas404s and the alternatives are a local tarball or raw REST. Nothing below is adoptable until a stranger can install it in one command — and a skeleton whose first instruction is “build a tarball from our monorepo” will not be used by anyone. - Land the OpenAPI contract test from BACKLOG §6 — assert every path in openapi.json resolves to a real route. The spec is hand-curated and can drift, and it is the codegen source in a later step, so it has to be trustworthy before anything is generated from it.
- PROBLEM: every storefront rewrites the same fetch layer, and the total on the cart page can disagree with the total the customer is charged. Extend astrobaas/client past content into commerce — products, orders, orders.quote, coupons, shipping methods, settings, search. The cart helper stores ONLY {product_id, variant_id, qty} and gets every subtotal, tax, shipping and discount line from POST /api/orders/quote, which already runs the same priceBasket() that placeOrder runs. The storefront performs no money arithmetic at all: that is the honest reading of “integer-only money arithmetic” on a client whose only number type is a float. Note the quote endpoint deliberately reserves no stock, so it is safe to call on every quantity change.
- PROBLEM: a developer writes price/100 and ships a rounding bug, and product pages earn no rich results because the schema.org code is unreachable. formatMoney/moneyPlain in src/lib/money-format.ts are exported from nowhere, and the pure schema.org builders in src/lib/structured-data.ts are exported only from astrobaas/core — the server package that drags in astro, libsql, lowdb and sanitize-html, which no Next.js storefront can import. Mirror all of them into the dependency-free client bundle, together with absoluteMediaUrl.
- PROBLEM: the shop either re-optimises every image or silently serves full-size originals. src/lib/media/derivatives.ts documents both failures on the owner’s own shops — 632 MB across 31,349 runtime-generated variants and 58,695 memory-ceiling hits on gr-optics.com, and weeks of unoptimised originals on optikigwnia.gr from a native module that failed with no user-visible symptom. Derivatives are a FIXED set (400/800/1600 WebP, generated at upload), so ship a srcset builder pinned to DERIVATIVE_WIDTHS plus a next/image recipe that requests exactly those widths with the optimiser off. Do NOT build image transformation URLs — that is the thing that already burned both shops.
- PROBLEM: an editor fixes a price and the shop shows the old one until someone redeploys. The emit side already ships — post., content., product., order., customer.created and subscriber.* fire HMAC-signed with a delivery log and redeliver. Build the receive side: a documented cache-tag vocabulary (product:
, products, post: , settings) and a drop-in Next App Router route handler that verifies with the existing verifyWebhookSignature and calls revalidateTag, with Nuxt and SvelteKit variants. Add a max-stale fallback sweep, because INTEGRATION.md states retries run on in-process timers and do not survive a restart. - PROBLEM: an editor cannot see how a draft will look on the actual shop. /admin/posts/preview renders drafts inside the CMS’s own PublicLayout — which on a headless install is a theme the storefront does not use, making it a truthful preview of the wrong site. Add a short-lived, single-post, read-only preview token; a storefront_preview_url setting; a Preview button that opens the shop rather than the CMS; and a draftMode() recipe in the SDK. Scope the token to one post id with a minutes-long expiry — handing a browser a full API key would be a draft-content leak, and preview URLs get pasted into Slack.
- PROBLEM: someone renames a field in the CMS and finds out from a blank product page in production. Add
astrobaas types— read GET /api/content-types (staff-only, so a build-time key, which is fine) and emit .d.ts for every admin-defined type, plus types generated from openapi.json now that the contract test makes it trustworthy. - PROBLEM: developing the storefront means either configuring CORS on a production CMS or running the whole CMS locally with a copy of the catalogue. Add
astrobaas dev-proxyto front a remote CMS on localhost, same-origining /api and /uploads and injecting the key server-side so it never reaches the browser. Pair it with an SDK guard that refuses to run with a bearer key in a browser context — a key in a NEXT_PUBLIC_ variable is a published credential, not a configured one. - Deliberately SKIP the React hooks and Vue composables in BACKLOG §5, except one useCart. App Router and Nuxt server components make most reads server-side, so hooks only earn their keep for cart state. Skip realtime/SSE (BACKLOG §4) for this toolkit too — webhooks plus revalidation cover storefronts, and realtime matters for admin dashboards and live stock counters, which is a different roadmap item.
- Extract the free starter from the two live Next.js 15 App Router shops once both are in git. Strip client content, branding and catalogue; keep the real fetching, cart, revalidation, preview, image and SEO wiring. Per the rules, no mock data — it ships pointed at a running AstroBaaS with a real seeded catalogue, or it does not ship. Licence it GPL-3.0 to match the core.
- Build the first PAID vertical theme on top of the free starter, for optics — the vertical that already exists and has two live shops to learn from. Price it against the anchor band, sell it with a stated upgrade window rather than implied perpetual maintenance.
- Ship
npx create-astrobaas-storefrontwith the managed-hosting deploy as the default happy path and a self-host path documented beside it with equal care. This is the step that turns the free skeleton into hosting conversions. - Rewrite the services page: €1,000 becomes setup plus managed hosting rather than a one-time build, €3,000 stays as-is (the skeleton does not replace catalogue migration, payments, shipping zones, VAT and launch), and publish fixed “starting from” prices for migration and configuration so skeleton users have an obvious upgrade path to paid help.
Risks. SOURCE-OF-TRUTH RISK, FIX FIRST: docs/INSTANCE-FRONTEND-AUDIT.md records that the OptikiGwnia storefront (Next.js 15, App Router, React Three Fiber, GSAP, 57 source files) is NOT a git repository — the VPS holds a built standalone artifact and the source exists on one laptop. That storefront is the extraction source for the free starter. A dead disk deletes the entire plan in this document. Nothing else here should start before that is in version control with a remote.
NPM IS A ONE-WAY DOOR: publishing @astrobaas/client claims a name permanently and opens a public support surface. STABILITY.md marks the core surface @alpha with breaking changes still expected, so the first published version needs an explicit stability statement or early adopters will be broken and will say so publicly.
PREVIEW TOKENS ARE A NEW CREDENTIAL IN A NEW PLACE: the preview handoff puts something in a browser URL that reads unpublished content. It must be scoped to one post id, read-only, and expire in minutes. A full API key reused for preview would be a draft-content leak, and preview URLs get pasted into Slack and email.
STALE-FOREVER FAILURE MODE: INTEGRATION.md states webhook retries run on in-process timers and do not survive a restart. A CMS restart during a delivery can leave a storefront permanently serving an old price with no signal that anything is wrong — the same silent-failure shape as the optikigwnia.gr image regression. The revalidation receiver needs a max-stale fallback sweep, not just the webhook path.
CODEGEN INHERITS SPEC DRIFT: BACKLOG §6 records that openapi.json is hand-curated and the contract test asserting every path resolves to a real route is unbuilt. Generating types from it before that test lands means generating confidently wrong types, which is worse than none.
THEME TREADMILL FUNDED BY ONE-TIME REVENUE: five vertical themes are five permanent maintenance obligations against Next.js majors and AstroBaaS API changes, each paid for once. This goes wrong in year two, not year one. Either price the themes with an upgrade window that expires, or bundle upgrades into the hosting subscription.
HOSTING IS AN UNSCOPED OPERATIONAL BUSINESS: the recommendation makes hosting the revenue line, but uptime, backups, TESTED restores, on-call and an incident process have not been scoped anywhere. One bad restore costs more trust than ten theme sales earn. This is the largest unknown in the recommendation and the owner should scope it before committing.
THE €1,000 TIER ERODES IF NOTHING CHANGES: the skeleton genuinely undercuts the business-website tier. If it is not repositioned as setup-plus-hosting before the skeleton is released, that revenue line degrades with no replacement.
PRICING FIGURES ARE ANCHORS FROM A MAY 2026 CUTOFF: Shopify plan prices, theme-store bands and Adobe Commerce licence ranges all move. Every number in this document must be re-checked against the vendor’s own page before it informs a real price.
SCOPE RISK ON THE SDK: extending astrobaas/client into commerce doubles its surface and therefore its compatibility promise. Each added method is something that cannot change freely afterwards. Add products, quote and orders first, and let the rest be pulled in by demand rather than pushed out speculatively.
Automations for AstroBaaS — merchant-authored trigger → condition → action, built on the sweep that already runs
Yes, and roughly 60% of the machinery already exists — a typed event system with wildcard subscription matching, a persisted delivery log with backoff retry and a manual re-send endpoint, a single-interval sweep with an explicit “never a second interval” doctrine, an audit trail, and an operator-editable template system that already enforces required placeholders at save time; what is missing is a rule store, a condition evaluator, an action executor, and about six events that ought to fire and do not.
Options
- Do nothing new — document the webhooks and point merchants at n8n / Make / Zapier — Publish the event catalogue (src/lib/webhook-util.ts WEBHOOK_EVENTS), the HMAC signing scheme, and a recipe page. The merchant runs the automation tool; AstroBaaS only emits.
- Cost: Two days of docs plus the six missing fireEvent calls. No new schema, no new screens, no ongoing obligation.
- For: Costs almost nothing and is honest about what the system is. Anyone technical already gets full automation today. Keeps the maintenance surface at zero and never turns a support call into ‘my flow did not fire’. A good stopgap that stays useful even after a real engine ships.
- Against: An optician or a taverna owner will not stand up n8n. It answers the developer’s question, not the owner’s — the merchant still cannot author anything. It also pushes the shop’s customer data through a third party, which for a self-hosted GPL product aimed at Greek SMBs is exactly the objection the product exists to answer.
- Plugin-authored rules only — no merchant UI — Expose a small registerRule() API on the existing plugin system; automations ship as TypeScript in a plugin, using PLUGIN_HOOKS.AFTER_ORDER_CREATE and friends.
- Cost: Roughly one PR. The hook catalogue and the plugin loader already exist.
- For: Almost free, fully general, no condition language to design, no injection surface. Fits the owner’s services business directly — a €3K storefront engagement can include three bespoke rules as billable work.
- Against: Every rule change is a deploy. It is not the thing the owner asked for. It also does not scale across five verticals: the hotel wants a different follow-up email from the frontistirio, and neither should require the owner to open an editor.
- Rules — flat trigger, typed condition list, ordered action list, executed on the existing sweep (RECOMMENDED) — One trigger from a fixed catalogue; conditions as a flat ANDed list of typed comparisons over a declared field manifest, with at most one ANY-of group; an ordered list of actions from a fixed catalogue. Persisted runs with a condition trace. Rides the sweep tick that publishDuePosts, sweepAbandonedOrders and maybeSendCampaign already share.
- Cost: Large — 6 to 10 PRs, realistically the biggest single item on the board. Two of them (missing events, tags on orders and customers) are independently useful and can land first. Ongoing: the field manifests must be kept in step with the payload shapes, which is a save-time validation problem, not a runtime one.
- For: Every part maps onto a primitive that already works and is already tested. A merchant can author it from dropdowns without typing an expression. The condition trace makes ‘why did nothing happen’ answerable without a support ticket. The registered-webhook action makes it infinitely extensible without shipping a single connector. And it is honestly implementable — no mock data, no aspirational hooks.
- Against: It is genuinely large, and half of it is admin UI, which is the slow half. The flat condition model will frustrate the one merchant in twenty who wants nested logic. Multi-replica execution needs a claim that may not be expressible on all three storage drivers.
- A full node-graph builder — branches, waits, parallel paths, a canvas — The literal Shopify Flow lookalike: drag nodes onto a canvas, branch on conditions, insert wait-3-days steps, fan out.
- Cost: Very large. A canvas editor, a persisted execution state machine with resumable waits, and a versioning story for a graph edited mid-run. Multiples of the recommended option.
- For: It is what the screenshots look like, and it genuinely handles the delayed-follow-up case elegantly. Demos extremely well.
- Against: Almost all of the value is in the first branch. The wait node alone drags in resumable timers that must survive restarts across three drivers, and editing a graph while runs are suspended inside it is a versioning problem with no cheap answer. Build the flat version first; a wait step can be added later as one action type without a canvas.
Recommendation. Build the flat rule engine (option 3). Below is the design, and it deliberately extends what is there rather than sitting beside it.
WHAT ALREADY EXISTS (verified in the tree, not assumed)
- src/lib/webhooks.ts — fireEvent(event, data): fire-and-forget, never throws, never blocks the request. Each delivery is a persisted WebhookDelivery row (status pending/success/failed, attempts, last_status, last_error) with backoff retry (30s/2m/10m, WEBHOOK_RETRY_DELAYS_MS) and a manual re-send at POST /api/webhooks/deliveries/{id}/redeliver. It refuses to fire at all when isStagingEnv(), because a refreshed staging clone carries production’s webhook rows and would call the real fulfilment partner. It also deny-by-defaults plugin-sourced subscriptions when the plugin table cannot be read.
- src/lib/webhook-util.ts — WEBHOOK_EVENTS plus webhookMatches(), which already supports exact names, ‘prefix.’ and ''. The subscription-matching problem is solved; reuse it verbatim for rule triggers.
- src/lib/scheduler.ts — ONE interval, one sweep at a time behind a
sweepingflag, idempotent start, in-memory SchedulerStatus deliberately not persisted. It already carries the doctrine in comments: “On the EXISTING tick, never a second interval”. maybeSendCampaign is the precedent for durable batch work — it keeps a cursor ON THE RECORD so a restart resumes rather than re-sending. - src/lib/plugin-system.ts — PLUGIN_HOOKS with AFTER_ORDER_CREATE, AFTER_ORDER_STATUS_CHANGE, BEFORE_ORDER_SAVE, PRODUCT_PRICE, ORDER_LINE_EXTRAS, PAYMENT_PROVIDERS, MANUAL_METHODS. Plugins register routes, admin pages and migrations.
- src/lib/audit.ts — recordAudit(), canonical AUDIT constants, admin-readable, with the standing rule that secrets never go in metadata.
- src/lib/email-templates.ts — operator-editable subject and body, a declared placeholder list,
requiredplaceholders enforced AT SAVE TIME with the reason, and a stored template that fails validation falling back to the built-in default rather than sending something broken. Plain text only, on purpose. - src/lib/rate-limit.ts (pluggable, sharedRateLimitStore()), src/lib/url-guard.ts (SSRF defence for operator-registered webhook targets), src/lib/capabilities.ts (role capability matrix with overrides; admin not reducible).
GAPS I FOUND THAT BLOCK THIS (all small, all independently worth fixing)
order.updatedis declared in WEBHOOK_EVENTS and is never fired anywhere. Fire it or delete it.content.submittedIS fired (src/pages/api/content/[type]/index.ts:313) but is NOT in WEBHOOK_EVENTS — only '' or ‘content.’ can subscribe. This is the review-moderation event, so it matters.- There is no event for the money arriving.
order.status_changedis FULFILMENT status; the Order model comments are explicit that conflating it with payment_status is how orders ship unpaid. So “when an order is paid” is currently unobservable. src/lib/payments/capture.ts is where it belongs. - No event for a refund (AUDIT.PAYMENT_REFUNDED is recorded, nothing fires), none for a failed payment, none for the abandonment sweep cancelling an order (scheduler.ts:339 records audit ‘order.abandoned’ and stops there).
- Order and Customer have NO tags field. Product does (
tags?: string[], models.ts:730). Tag-then-filter-on-tag is the backbone of every real Flow, so tags on order and customer are a prerequisite, not a nice-to-have. - Flow definitions must NOT be stored as custom_entities. models.ts:1112 says plainly that createCustomEntity writes into the content CHANGE FEED served publicly at /api/content/changes — a rule definition holds the shop’s internal logic and operator email addresses, and would be published. New first-class collections (
automations,automation_runs), the wayrevisionsgot its own for the same reason. That is a schema-version migration across three drivers.
TRIGGER CATALOGUE Tier 1, already firing: order.created, order.status_changed, customer.created, product.created/updated/deleted, post.created/updated/deleted, content.created/updated/deleted, content.submitted, subscriber.confirmed, subscriber.unsubscribed. A review is a moderated content type, so “a customer left a 1-star review” is content.submitted with type=review plus a condition on the rating — no new event needed. Tier 1b, one fireEvent call each at a site that already exists: order.paid, order.refunded, order.payment_failed, order.cancelled_abandoned, and order.updated resolved either way. Tier 2, scheduled: “every day, for each order matching X”. Rides the existing sweep tick, never a second interval. Be honest about granularity on the screen: cadence is the sweep interval (60s default), so “daily at 09:00” means the first tick after 09:00. Tier 3, manual: run this rule against this order, from the order screen. This is both a feature (the human decides, the machine does the tedious part) and the basis of testing. A “wait N days, then re-check” step can be added later as one action type without a canvas — it becomes a scheduled re-entry, and the scheduled trigger already provides the machinery.
THE CONDITION LANGUAGE — and how to stop it becoming a programming language The single rule: conditions are a flat list of typed comparisons over a DECLARED field manifest, ANDed, with at most one ANY-of group inside. No expressions, no variables, no arithmetic, no regex, no user-authored string ever evaluated as code.
- Each trigger declares a manifest of { path, type, label, operators }. Types: int_cents, int, string, enum, bool, datetime, string_list, country. The merchant picks from a dropdown and never types a path.
- Operators are per-type and closed. Numbers: = ≠ < ≤ > ≥. Enums: is / is not / is one of. Strings: is / is not / contains / starts with, case-folded — no regex, because a stored regex is a programming language with catastrophic backtracking attached. Lists: contains / does not contain / is empty. Datetime: before / after / older than N days.
- Money is int_cents throughout, entered in euros in the UI and stored as cents, validated as an integer at save time. Integer-only arithmetic, per the rules — a condition on a float price is a rounding bug the merchant will never diagnose.
- The things merchants actually need that look like queries are provided as PRECOMPUTED DERIVED FIELDS on the manifest, computed by core at evaluation time: customer.orders_count, customer.lifetime_total_cents, order.line_count, order.contains_product_id, order.contains_category, product.stock. Adding a derived field is a code change, deliberately. That is the escape valve that stops anyone needing joins.
- Templating exists ONLY in action bodies, and only as the same {{placeholder}} substitution email-templates.ts already does — from a declared list, with
requiredenforced at save. Do not invent a second templating story. - Save-time validation is the whole safety argument: a rule referencing a field its trigger does not have is REFUSED at save with the reason, exactly as an email template missing its reset link is. A rule that goes invalid later (content type deleted, product removed) is marked needs_attention and DOES NOT RUN — it must never silently pass, and must never silently match everything. Why not JSONLogic or CEL: a stored expression cannot be rendered back into a form the merchant can edit, cannot be validated against a manifest at save time, and cannot be migrated when a payload field changes. The flat model round-trips to UI perfectly, which is precisely what makes “why did my rule not fire” answerable. One level of grouping, full stop. A merchant who needs deeper logic writes two rules. State that as a product decision, not as a limitation to apologise for.
ACTION CATALOGUE — CORE (free, GPL, implementation entirely inside this repo)
- Send an email — recipient constrained to the triggering record’s own address, or a fixed operator list. Rendered through the EXISTING email-template system with placeholder validation and default fallback.
- Change order FULFILMENT status. Never payment_status — see the rails.
- Add an internal note to the record.
- Tag / untag an order, customer or product (needs the tags field on the first two).
- Publish / unpublish / trash content. Never delete.
- POST to an operator-registered webhook endpoint, signed with the existing HMAC. This is the universal escape hatch and it is what makes the free tier genuinely complete — any connector anybody wants can be built against it without asking permission.
- Create a task for a human — a row on a to-do list in the admin. This is the honest implementation of every action that should not be automated, and for a three-person optician it matters more than auto-refund does.
ACTION CATALOGUE — PAID PACK (separate repo) Slack / Teams / Viber / Telegram; SMS (Twilio and the Greek providers); Mailchimp / Brevo / Klaviyo list sync (the subscriber.confirmed and subscriber.unsubscribed events were added for exactly this — the comment in webhook-util.ts says so); accounting and ERP push (Softone, Entersoft, Epsilon); courier connectors (ACS, Geniki Taxydromiki, Speedex); Google Sheets append.
THE SEAM, stated in the owner’s own terms “Core owns the interface plus an honest hand-modelled implementation; a paid pack owns the credential, the per-country obligation, or the support commitment.” Applied here: core owns the engine, the run log, the condition evaluator, and every action whose implementation lives entirely in this codebase. A paid pack owns any action that needs a third party’s credential (Slack token, Twilio SID), a per-country obligation (a Greek courier’s manifest format, an ERP’s document schema), or a commitment to fix it within a day when their API changes. No licence gating anywhere — an install without the pack simply has fewer entries in the action dropdown, and the registered-webhook action still reaches every one of those services if someone wants to wire it themselves.
RUNS, LOGGING, RETRY Copy WebhookDelivery almost exactly; it already works and already has a screen and a re-send endpoint. A run record: { id, rule_id, rule_version, trigger_event, subject_type, subject_id, status pending|running|success|failed|skipped, matched, condition_trace, steps[{action, status, attempts, last_error, last_status, at}], created_at, updated_at }.
- condition_trace is the feature. For a run that did not match, record WHICH condition failed and WHAT the actual value was: “order.total_cents was 4500, condition required ≥ 5000”. One field, cheap to write, and it answers the only support question this feature ever generates. Shopify’s own log is worse at this.
- Volume control: always record matched runs; record non-matched runs only while the rule is in watch mode (first N evaluations after an edit, or toggled by hand), otherwise keep a per-rule per-day counter of evaluated / matched / skipped. A busy shop must not drown in rows.
- Retry is PER-ACTION and typed. Each action declares retry: ‘safe’ or ‘at-most-once’. Blindly retrying a failed sendEmail three times sends the customer three emails. An at-most-once action that fails marks the run failed and surfaces it for a human to re-run — the same affordance webhooks already give.
- Durability across restarts: webhook retries run on in-process timers and are honest that they do not survive a restart. Flows must do better, because the merchant believes they fired. So a run left pending past its next-attempt time is picked up by the EXISTING sweep tick. Restart-durable retry, no new infrastructure, and it obeys the scheduler’s own doctrine.
- Multi-replica: the sweeps are safe today because publishing an already-published post is a no-op. Flow actions are not idempotent. A run therefore needs a claim — conditional update to status=‘running’ where status=‘pending’, checking rows-affected. I could not verify that all three storage drivers can express that conditional update; check before writing the row. If one cannot, the honest answer is that automations require single-node and the screen says so, in the same voice schedulerStatus already uses.
DEBUGGING A FLOW THAT DID NOT FIRE — five answers, in the order the merchant asks
- Is the rule on? The list shows enabled/disabled and needs_attention.
- Did the trigger even happen? Add a shop-wide EVENT FEED — the last N events per type, independent of any rule. Today fireEvent returns early with no trace when nothing subscribes and when isStagingEnv(); that silence is exactly what makes this unanswerable. Record into the feed BEFORE the subscription check.
- Was the rule evaluated, and what did the conditions see? → condition_trace.
- Did the action run, and what did the other side say? → per-step status, last_error, HTTP status.
- Test it now: run the rule against a real existing record the merchant picks, in SIMULATE mode — evaluate conditions, render the email, show exactly what would be sent, execute nothing. This honours “no mock data ever”: the simulation runs against a real record and shows real rendered output. Plus a free win: a banner on any rule that has been enabled for 14 days with zero matches. That is almost always a mistake, and telling the merchant costs nothing.
SAFETY RAILS
Loop prevention, three layers: (a) every event carries caused_by_run_id and depth; a rule refuses at depth ≥ 3 and records skipped:loop_guard so it is visible rather than silent; (b) an event caused by a run of rule R never re-evaluates rule R; (c) a per-subject cap — at most N runs per rule per subject per day. “This customer received the same email forty times” is the failure that ends a merchant relationship.
Rate limits: reuse sharedRateLimitStore(). Per-rule buckets, per-host buckets for outbound HTTP, and one global email-per-hour bucket — the shop’s sending domain reputation is a single shared resource, and a runaway rule burning it is worse than the rule not running. Exhausted buckets queue (pending, retried on the sweep) rather than dropping.
A global kill switch on /admin/operations beside the scheduler status, plus per-rule pause.
Staging: automations MUST inherit the isStagingEnv() guard STRUCTURALLY, not by anyone remembering — evaluate, log the run, execute nothing external. That gives staging a genuinely useful “what would have happened” log for free.
Identity and audit: authoring is an admin-only capability (a new manage_automations entry in CAPABILITIES). A rule runs with a fixed narrow system identity, never with its author’s role, and every action it takes writes to the audit log with actor: automation:
WHAT MUST NEVER BE AUTOMATABLE — enforced in the action registry, not by convention
- Anything that moves money out: refunds, payouts, capture, void. Nobody can un-send €4,000.
- Setting payment_status. The provider says whether money arrived. The entire fulfilment/payment split in the Order model exists to stop orders shipping unpaid.
- Deleting anything — posts, products, customers, orders, media. A rule may unpublish, trash or flag; a human deletes.
- GDPR export or erasure. A rule that erases a subject on a trigger is a compliance incident wearing a feature’s clothes.
- Anything claiming FISCAL status. A rule may record external_receipt_no; it may never mint a fiscal document or transmit to myDATA. Per the standing rule, that is commercial and per-country and it is not a checkbox in a flow builder.
- Changing users, roles, capabilities, API keys, webhooks — or the automations themselves. A rule that can edit rules is a privilege-escalation primitive.
- Sending to an address taken from a free-text field. Recipients are derived from the triggering record or a fixed operator list, or it is an open relay.
- Arbitrary outbound HTTP to a URL computed from record data. Outbound goes only to endpoints an operator registered — which is what a Webhook row already is, carrying a secret and an active flag — and url-guard.ts already refuses internal targets.
PRICING ANCHORS (anchors, not recommendations; from memory as of the knowledge cutoff — verify every figure before it goes on a price list)
- Shopify Flow is FREE on every Shopify plan; it stopped being Plus-only in 2022. This is the load-bearing fact: the ENGINE is not a paid product at the largest competitor, and pricing one against free is a losing position.
- Mechanic (Shopify app): roughly USD 16–116/month, tiered by the store’s Shopify plan.
- Arigato Automation (Shopify): roughly USD 15–100/month by tier.
- Alloy Automation: a free tier, then roughly USD 99+/month.
- Klaviyo — what merchants actually buy for order-triggered email and SMS: free to about 250 contacts, then from roughly USD 20/month, rising steeply with list size.
- Magento Open Source has no merchant-facing flow builder at all; that market is one-off extensions (Amasty, Aitoc, Mageplaza order-status and follow-up-email modules), typically EUR 150–400 one-off plus an annual support renewal.
- Greek courier and ERP connectors are normally sold as a one-off integration fee plus annual support rather than SaaS — the same shape the owner’s services business already sells. Implication: price the CONNECTOR PACK and the managed hosting, not the engine. And it does not cannibalise the €1K/€3K frontstore work — a shop that can automate its own follow-ups is a shop with a reason to commission the storefront, and the pack earns its money by carrying the support commitment when ACS changes their API.
Work, in order
- Fix the event catalogue first — it is worth shipping on its own even if automations never follow. Add content.submitted to WEBHOOK_EVENTS (it fires at src/pages/api/content/[type]/index.ts:313 and nothing can subscribe to it by name). Resolve order.updated: it is declared in src/lib/webhook-util.ts and fired nowhere — either fire it or delete it.
- Add the events that make automation possible: order.paid in src/lib/payments/capture.ts (today the arrival of money is unobservable — order.status_changed is fulfilment, and the Order model comments are explicit that conflating them is how orders ship unpaid), order.refunded in src/lib/payments/refunds.ts, order.payment_failed, and order.cancelled_abandoned in sweepAbandonedOrders (src/lib/scheduler.ts:339 already records the audit line and stops there).
- Add a shop-wide event feed, written BEFORE the subscription check in fireEvent. Today an event with no subscriber leaves no trace, which makes ‘did it even happen’ unanswerable. This is the first debugging surface and it improves webhook support immediately.
- Add tags to Order and Customer (Product already has tags?: string[] at models.ts:730), plus an internal-notes affordance. Tag-then-filter-on-tag is the backbone of every real automation and this is a prerequisite, not a nice-to-have. Ships useful on its own for manual triage.
- Create first-class
automationsandautomation_runscollections with a schema-version migration across all three drivers. Do NOT use custom_entities: models.ts:1112 states that createCustomEntity writes into the content change feed served publicly at /api/content/changes, and rule definitions hold internal logic and operator addresses. Follow the precedent that gaverevisionsits own collection. - Build the field manifests and the condition evaluator as PURE functions with no I/O — one module, exhaustively unit-tested, mirroring how webhook-util.ts was split out from webhooks.ts. Typed comparisons only, one level of grouping, integer cents for money, closed operator sets per type, no regex.
- Implement save-time validation that refuses a rule referencing a field its trigger does not have, with the reason — the same doctrine as EmailTemplateDef.required. Add the needs_attention state for rules that go invalid later, and make invalid rules NOT RUN rather than pass or match everything.
- Build the action registry with each action declaring retry: ‘safe’ | ‘at-most-once’ and an explicit allow-list. Implement the core seven: email via the existing template system, fulfilment-status change, tag/untag, internal note, publish/unpublish/trash, POST to a registered webhook, create-a-task-for-a-human.
- Encode the never-automatable list in the registry itself, not as a convention: no money movement, no payment_status writes, no deletes, no GDPR export/erasure, no fiscal document, no changes to users/roles/keys/webhooks/automations, no free-text recipients, no computed outbound URLs. Add a negative test per line that fails without the guard.
- Wire execution into the EXISTING sweep tick in src/lib/scheduler.ts alongside publishDuePosts and maybeSendCampaign — never a second interval, per the doctrine already written in that file. Pending runs past their next-attempt time get picked up there, which gives restart-durable retry with no new infrastructure.
- Verify whether a conditional claim (UPDATE … WHERE status=‘pending’, checking rows-affected) is expressible on lowdb, SQLite and libSQL. If any driver cannot, say so on the screen and require single-node for automations — the same honesty schedulerStatus already practises about what this process is doing.
- Implement the safety rails: caused_by_run_id and depth on every event with a depth-3 refusal recorded as skipped:loop_guard, no self-retrigger, a per-rule-per-subject daily cap, per-rule and per-host and global-email rate buckets on sharedRateLimitStore(), a global kill switch on /admin/operations, and structural inheritance of the isStagingEnv() guard so staging logs runs and executes nothing.
- Add manage_automations to CAPABILITIES (admin-only), run rules under a fixed system identity rather than the author’s role, and write every action to the audit log as actor: automation:
with the run id. - Build the admin: a rule list, a dropdown-driven rule editor with no free text outside templated bodies, the run log modelled on the existing webhook deliveries screen, the condition trace rendered as a sentence, simulate-against-a-real-record, and the ‘enabled 14 days, never matched’ banner. Add one nav entry with a translation key, per the rule in admin-nav.ts.
- Ship three worked rules with the release, one per vertical, as documentation rather than seed data: an unpaid-order reminder for the optician, a booking-confirmation follow-up for the hotel, a low-stock task for the parts eshop.
- Only then start the paid connector pack in the separate repo — Slack, SMS, ESP sync, courier, ERP. Each one carries a credential, a per-country obligation or a support commitment, which is precisely what puts it on that side of the seam.
Risks. SIZE. This is the largest item on the board — 6 to 10 PRs, and roughly half of it is admin UI, which is the slow half. It should not be written as a single roadmap row. The first two steps (missing events, tags) are independently valuable and should be sequenced so that stopping after them still leaves the product better.
THE ENGINE CANNOT BE THE PRODUCT. Shopify Flow is free on every plan. If the pricing story depends on charging for trigger→condition→action, it is priced against free. The money is in the connectors and the managed hosting.
THE custom_entities TRAP. Storing rules or runs there would publish them at /api/content/changes. This is written down in models.ts and is easy to miss because custom_entities is otherwise exactly the right shape and works on all three drivers. Anyone who takes the obvious path leaks the shop’s internal logic and its operator addresses.
NON-IDEMPOTENT ACTIONS MEET AN IDEMPOTENT-BY-DESIGN SWEEP. Every existing sweep is safe under double execution; sending an email is not. The claim mechanism is load-bearing, and I could not verify it is expressible on all three storage drivers — check that before committing to multi-replica support rather than after.
RETRY THAT SENDS THREE EMAILS. The webhook retry loop is the natural thing to copy and it is wrong for at-most-once actions. If the retry typing is not built in from the first action, it will be retrofitted after a customer receives four order confirmations.
THE RAILS DECAY IF THEY ARE CONVENTION. “Never automate a refund” enforced by a code review is enforced until someone adds an action in a hurry. It has to be a property in the action registry with a negative test that fails when the property is removed — otherwise the guarantee quietly stops being true and nobody notices until it matters.
STAGING. fireEvent already refuses to fire on staging for a very concrete reason. If automations do not inherit that guard structurally, the first staging refresh emails real customers, and the report arrives as a phone call.
EMAIL REPUTATION IS A SHARED RESOURCE. One badly-written rule on one shop can burn a sending domain for every shop on the managed host. The global per-hour email bucket is not optional there — it protects tenants from each other.
MANIFEST DRIFT. Field manifests must stay in step with the event payload shapes. The mitigation is that they are validated at save time and that an invalidated rule stops rather than misfires — but the failure mode if this is skipped is a rule that silently matches everything, which is the worst possible failure for something that sends email.
WHAT I COULD NOT VERIFY, stated plainly: whether the three storage drivers support a conditional claim update; whether any admin to-do/task surface exists to host the create-a-task action (there is /admin/messages, but I did not confirm it is suitable); the exact current shape of the abandonment settings that a scheduled trigger would sit next to; and every pricing figure above, all of which are anchors from memory and need re-checking before they inform a real price.
POS for AstroBaaS: four options against a fiscal constraint that rules out three of them
Build the seam in core (option d) and sell the till integration (option b); treat the fiscal-printer bridge (a) as a customer-funded engagement and refuse the certified-POS path (c) outright — because in Greece the legally binding half of a retail sale is a device you are not licensed to be, and your actual problem is not that you lack a till (you own two, by law) but that the sales going through them are invisible to the stock the website sells from.
Options
- (a) Browser counter app + bridge to an existing certified fiscal printer — An admin ‘counter’ screen in AstroBaaS: search the catalogue, build lines, take tender, and hand the total to a certified ΦΗΜ over a small local bridge daemon on the shop PC. The ΦΗΜ prints the legal receipt and transmits it to myDATA itself; AstroBaaS records the sale, decrements shared stock, and stores the device’s document number. Critically, AstroBaaS never talks to the card terminal — under Α.1155/2023 the EFT/POS is driven by the ΦΗΜ (or a licensed provider), so the amount flows counter-app → ΦΗΜ → card terminal. Any design where our code sends an amount to the card machine is both non-conformant and technically broken.
- Cost: Estimate, not a quote: counter UI ~3–5 engineering-weeks (it is a real transactional UI — line editing, discounts, split tender, returns, cash drawer, receipt reprint). The bridge is where the money goes: ~3–6 weeks per ΦΗΜ vendor, and that is optimistic. Vendor protocols are per-manufacturer, largely Windows COM/DLL, documented in Greek if at all, frequently under NDA or dealer-only, and untestable without the physical device in hand. Budget procurement and on-site testing time you cannot compress. Ongoing obligation is the severe part: a per-vendor driver you maintain forever, plus a support commitment on the one screen in the business that stops all revenue when it fails.
- For: Best merchant experience by a wide margin: one catalogue, one stock pool, one customer record, one place to look at the day. Prescriptions, order history and web orders sit next to the counter sale. It is the only option that makes AstroBaaS the system the shop actually lives in, and it fits the existing plugin seams cleanly.
- Against: You inherit a hardware compatibility matrix and a firmware treadmill with no advance notice — ΑΑΔΕ changes the spec, the vendor ships firmware, your bridge breaks, and you find out when a shop cannot sell on a Saturday. Each new customer’s printer is a new project. Supporting a till is a different support tier (same-day, phone, during trading hours) than supporting a CMS, and pricing that honestly is hard. Solves your two shops and generalises badly.
- (b) Integrate with an existing certified Greek POS/ταμειακή vendor — AstroBaaS is catalogue and stock, their POS is the till — The vendor’s certified software stays the legal issuer and the myDATA reporter. AstroBaaS pushes catalogue, prices and (later) stock to it, and pulls completed sales back to decrement the same pool the website sells from. Two sub-shapes with very different costs: (b1) the vendor exposes a real API and you write an adapter; (b2) no API, so you reconcile from their sales export on a schedule — uglier, but it still solves the binding problem. Note the timing: Greek B2B e-invoicing becomes mandatory for essentially all remaining businesses from 1 October 2026, so your shops’ accountant is choosing a πάροχος right now anyway. That same provider is your warmest possible integration partner, and the conversation costs you nothing to have this month.
- Cost: Estimate: ~2–4 engineering-weeks for the first vendor adapter if documentation exists, plus a discovery phase measured in phone calls rather than code — the real unknown is whether the vendor will talk to you at all. (b2) reconciliation is cheaper, perhaps 1–2 weeks, and degrades to a nightly job. Ongoing obligation is version-tracking one API, which is an order of magnitude lighter than a hardware driver.
- For: The legal risk sits with the party that is licensed and paid to carry it. When ΑΑΔΕ changes something, the vendor absorbs it and you may not even notice. The till is a supported product the merchant already trusts, with a technician who visits. Cheapest path to the outcome that actually matters — stock stops lying. Fits the seam rule perfectly: core owns the adapter interface, the paid pack owns the credential.
- Against: Two systems and two logins; the merchant’s day is not unified. You depend on the goodwill of a company whose commercial interest is to own the catalogue too, and which may simply decline or may deprecate the API. Data ownership and mapping (SKUs, VAT categories, discounts) is fiddly and is where the real bugs will live. You are a junior partner in the relationship.
- (c) A full certified POS of our own — what certification actually entails — This is two different regulated things and they must not be conflated. (c1) A type-approved ΦΗΜ: you become a fiscal hardware manufacturer — sealed device, fiscal memory, model type-approval, an authorised dealer and technician network to install and seal units. That is a hardware business and is not open to a software UG. (c2) A Υ.ΠΑ.Η.Ε.Σ. licence — the ΑΑΔΕ software-suitability licence held by the company, under which clients formally authorise you through the ΑΑΔΕ application and you issue, authenticate, transmit and archive their tax documents on their behalf. That is the software route, and it makes you a regulated intermediary holding other businesses’ tax records.
- Cost: Not expressible in sprints. Legal, accounting, audit and certification lead time in quarters, followed by a permanent compliance function, a retention and availability commitment, security audit, and the insurance and liability posture of an intermediary. It is a company-strategy decision about what business Dimitriou eCommerceWebServices is in, not a roadmap row.
- For: The only option where you own the whole stack and capture the recurring fiscal revenue instead of paying it away. If the managed AstroBaaS hosting ever accumulates a large Greek retail base, provider margin becomes a real line item and this stops being absurd.
- Against: It contradicts your own stated rule that anything claiming fiscal status is commercial and per-country — this is the maximal version of that exposure, taken on before you have the customers to justify it. It is Greece-only while the product is being aimed at five verticals and a hosting business. It converts a GPL CMS company into a regulated financial-adjacent intermediary, with the support and liability that implies. And if it ever does become rational, the correct move is to partner with or acquire an existing licensed πάροχος, not to build one.
- (d) Do nothing in core beyond publishing the seam — order and stock APIs a partner can build against — Ship the small, honest primitives that any till needs and stop there: a way to record a sale that happened at a counter, a way to mark it ‘fiscalised outside AstroBaaS — do not transmit’, a way to store the ΦΗΜ document number against it, and a stock-adjustment path that does not require inventing a fake order. None of that exists today: Order has no channel field, there is no stock-adjustment endpoint, and external_receipt_no is modelled at src/core/models.ts:1063 but nothing anywhere writes it — TRUTH_PLAN.md:863 already says so explicitly.
- Cost: Days, not weeks. The hard parts are already built: atomic reserveStock/releaseStock (src/lib/localdb.ts:637), the order pipeline in src/lib/commerce-service.ts, the non-fiscal printable receipt (src/lib/commerce/receipt.ts), and plugin hooks including AFTER_ORDER_CREATE. Ongoing obligation is API stability only, which STABILITY.md already governs.
- For: Correct behaviour for a GPL core regardless of which other option you pick — (a) and (b) are both strictly cheaper once this exists, and neither can be built cleanly without it. Zero legal exposure. Zero support surface. It is genuinely useful on its own: the moment a counter sale is recorded, the website stops selling frames that walked out of the shop an hour ago.
- Against: On its own it does not give your two shops a till, so as a complete answer to what you asked it is a non-answer. It also depends on someone else showing up to build the other half, and for a young project that partner may never appear. Treat it as the foundation, not the destination.
Recommendation. Do (d) now, pursue (b) as the paid pack, sell (a) only when a specific customer funds their specific printer, and refuse (c).
Apply your own rule — “the free/paid line is drawn by SEAMS (core owns the interface + an honest hand-modelled implementation; a paid pack owns the credential, the per-country obligation, or the support commitment)” — and the line draws itself. Core gets the counter-sale interface, the fiscal-status marker, the external_receipt_no write path, the stock-adjustment endpoint, and a counter screen that prints the non-fiscal receipt that already ships. That is the honest hand-modelled implementation, and it is not a stub: a shop in a country with no ΦΗΜ obligation can genuinely run on it, and a Greek shop uses it to record what its ΦΗΜ issued. The paid pack then owns exactly the three things your rule names — the vendor credential, the Greek per-country obligation, and the same-day support commitment on a screen that stops the business when it breaks.
Two corrections to how this is currently framed internally.
First, the reframe that changes the sequencing. You do not have a missing-till problem. Both shops are legally required to have a certified ΦΗΜ and therefore already do. What you have is a stock-divergence problem: docs/ECOMMERCE-ROADMAP.md:91 states it correctly — the website can sell a frame that was sold over the counter an hour earlier. That is the problem worth money, and it is solvable without building a till at all.
Second, I disagree with the roadmap’s claim that this depends on multi-location inventory landing first. It does not, for the first slice. Recording a counter sale against the single existing stock pool is strictly better than today for the question “do we still own this,” even though it cannot answer “which shop is it in.” For optics that limitation is real — frames are often one-of-a-kind per shop — so say it plainly in the docs rather than letting it be discovered. But it converts an XL blocked row into a small unblocked one, and multi-location becomes slice three instead of a prerequisite.
Pricing anchors, and these are anchors rather than recommendations. Shopify POS Pro is $89 USD per location per month, or $79 billed annually, with POS Lite free on any plan and Pro included on Plus for the first 20 locations — so two shops bleed roughly $2,136 USD a year, forever, for the till layer alone. Magento has no native POS; third-party list prices run from Webkul at $249 (CE) / $448 (EE), PWA POS $199–$897 and app POS $499–$2,545, up to Magestore from $1,299 (CE) / $2,599 (EE) one-time or about $69/month, with support windows differing sharply (Magestore one year, Webkul three months). The useful shape for a self-hosted pack is a one-time purchase that undercuts two years of Shopify’s per-location rent, with support sold separately and explicitly — because support, not code, is your actual cost here.
I have deliberately not given you EUR figures for Greek ΦΗΜ hardware or Greek retail software subscriptions. I could not verify them, they are dealer-quote pricing rather than list pricing, and you run two optical shops in Greece and therefore know these numbers better than any source I could cite. Get two dealer quotes before costing (a).
FIRST SHIPPABLE SLICE — “record a counter sale against shared stock.” Not a till. An order created with channel ‘counter’, already marked paid, decrementing the same pool the website sells from, carrying the ΦΗΜ’s document number in external_receipt_no and a fiscal_status of ‘external’ meaning issued and transmitted elsewhere, do not transmit. It reuses the field you already reserved, ships in days, and is the thing both (a) and (b) are built on top of.
Work, in order
- Confirm the legal premise with your accountant before writing code, in writing. Article 12 ΕΛΠ (ν.4308/2014) with ν.1809/1980 requires retail documents to be issued through a ΦΗΜ, and the published exemption lists are dominated by service businesses (gyms, aesthetic centres, clinics). Retail sale of goods over a counter is the core ΦΗΜ case, so an optical shop is almost certainly obliged — but optics also renders services (eye exams) and has an ΕΟΠΥΥ dimension, so get the exemption question answered explicitly rather than assumed. Everything below assumes the answer is ‘ΦΗΜ required’.
- Add the two model fields and their migration: Order.channel (‘web’ | ‘counter’, defaulting to ‘web’ so every existing row is correct without backfill) and Order.fiscal_status (‘none’ | ‘external’). Order has no channel field today — verified. Document fiscal_status ‘external’ as load-bearing: it is the flag that stops any future myDATA work from double-reporting a receipt the ΦΗΜ already transmitted to ESEND.
- Write the external_receipt_no path that has never existed. src/core/models.ts:1063 defines the field with a correct docblock; nothing reads or writes it anywhere in src/, and TRUTH_PLAN.md:863 already documents that gap. Add it to the admin order editor and to the order API, validated as an opaque string (do not attempt to parse or validate ΦΗΜ document numbers — formats differ per vendor and you will be wrong).
- Add a stock-adjustment endpoint. There is none in src/pages/api — verified. Counter returns, breakage, stocktake corrections and supplier deliveries all need to move stock without fabricating an order. Route it through the existing atomic reserveStock/releaseStock in src/lib/localdb.ts:637 so it inherits the concurrency guarantee rather than racing checkout, and write it to the audit log.
- Ship the counter-sale recording path end to end: POST an order with channel ‘counter’, status paid, fiscal_status ‘external’, and the ΦΗΜ document number. It must decrement stock atomically through the same path as checkout, appear in the same order list and reports, and print the existing non-fiscal receipt from src/lib/commerce/receipt.ts. This is the first shippable slice and it stops the website overselling.
- Write the negative tests, per your own definition of done, and prove each fails without the fix: a counter order must never be picked up by the abandonment sweep in src/lib/commerce/abandonment.ts; a counter order must not send the customer-facing order-confirmation email; the non-fiscal receipt must not render for an order marked fiscal_status ‘external’ without displaying the ΦΗΜ document number, so nobody can mistake it for the legal receipt; and two concurrent counter sales of the last unit must leave stock at zero, not minus one.
- Document the position in COMMERCE.md alongside the existing myDATA section at line 266, which is already correct and should not be softened. State three things explicitly: AstroBaaS records, it does not issue; a counter sale’s legal receipt comes from the certified ΦΗΜ, which is also what transmits to myDATA; and the shared stock pool cannot yet tell you which of two shops holds the item.
- In parallel with the code, and costing only phone calls: ask the accountant which πάροχος the shops are adopting for the 1 October 2026 B2B e-invoicing deadline, then ask that provider and your ΦΗΜ dealer whether they expose an API for catalogue push and sales pull. Do not commit a single engineering-week to option (b) until one of them says yes in writing. If nobody will talk to you, fall back to (b2) — a scheduled reconciliation from their sales export — which needs no permission from anyone.
- Only after a vendor commits: build the adapter interface in core (the seam) with one honest reference implementation, and put the vendor credential, the Greek fiscal mapping and the support commitment in the paid pack. Multi-location inventory becomes the slice after that, at which point ‘which shop has it’ finally gets a real answer.
- Treat option (a) as a funded engagement, never as speculative roadmap work. Quote it per printer vendor, require the physical device on your desk before quoting, and price the ongoing firmware-breakage risk into a support contract rather than absorbing it. If no customer will fund it, that is the market telling you (b) was correct.
Risks. LEGAL, and the one that must not be hand-waved. Software alone cannot issue a legal Greek retail receipt. My web checks found no general abolition of the ΦΗΜ obligation — only category exemptions and deadline extensions (vending machines pushed to 31 December 2026). Anything AstroBaaS prints stays a commercial document, exactly as src/lib/commerce/receipt.ts already argues at length. Do not let a counter screen drift into looking like a till that issues receipts; that is a liability with your logo on it.
DOUBLE-TRANSMISSION. Since 1 April 2024 retail receipts issued through electronic cash registers are transmitted to myDATA automatically by the device. If AstroBaaS ever also transmits, the shop reports the same sale twice. This is why fiscal_status ‘external’ is a slice-one field and not a later refinement — build the flag before there is anything that could transmit.
CARD TERMINAL. Under Α.1155/2023 the EFT/POS is interconnected with the ταμειακό σύστημα, where the master is a ΦΗΜ or a licensed provider’s software. Never design a path where AstroBaaS sends an amount to the card terminal directly. Amounts flow through the ΦΗΜ.
SUPPORT TIER. A till that is down stops all revenue in a shop, immediately, during trading hours. That is a categorically different support commitment from a CMS with a broken admin page, and it is the single largest hidden cost in options (a) and (b). Price it explicitly and separately, or do not sell it.
VENDOR DEPENDENCE (option b). You would be integrating with companies whose commercial interest includes owning the catalogue themselves. They may decline, deprecate, or charge. Get the API commitment in writing before spending engineering time — and note that (b2), reconciliation from an export, needs nobody’s permission and is the fallback that always works.
SCOPE CREEP INTO (c). The pull toward ‘we may as well become the πάροχος’ will be strongest right after (b) works and the provider’s margin becomes visible. Resist it. It contradicts your own rule that fiscal status is commercial and per-country, it is Greece-only against a five-vertical product, and if it ever becomes rational the move is to partner or acquire, not to build.
WHAT I AM NOT SURE ABOUT, stated rather than assumed: whether optical shops fall under any exemption (your accountant, not me); current Greek ΦΗΜ hardware and retail-software pricing, which I did not verify and refuse to guess at; and whether any specific Greek POS vendor exposes a usable API, which no search will tell you and one phone call will. My legal reading is from public secondary sources and is not a substitute for professional advice on a matter with real penalties.
Sources: Shopify POS pricing 2026 · Shopify POS per-location pricing · Magestore vs Webkul POS pricing · Α.1155/2023 POS–ΦΗΜ interconnection, codified · POS–ΦΗΜ/ΥΠΑΗΕΣ interconnection: timeline and procedures · Businesses exempt from ΦΗΜ use · Issuing a retail receipt without a ΦΗΜ · Article 12 ΕΛΠ, retail documents · Greece mandatory B2B e-invoicing phases · Greece B2B e-invoicing second phase, October 2026 · ΑΑΔΕ extension for vending-machine ΦΗΜ to 31/12/2026
Customer accounts and expansion stores: where the seam goes
Build a customer principal in the GPL core (a fourth access gate alongside staff/admin/public, plus one honest free feature — guest order lookup) and sell the standing account on top of it; for expansion stores, run separate installs with one-way catalogue sync and refuse the store-dimension rewrite, which this codebase would not survive.
Options
- A1 — Customer principal in core, account pack on top (Q1, recommended) — Core gains four things and no more: (1) purpose-tagged customer tokens — new tags ‘cust-link’ (15 min, single-use) and ‘cust’ (session), minted through the existing signPurposeToken/verifyPurposeToken in /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/lib/auth.ts and exported from astrobaas/core, which today exports only signWebhook/newWebhookSecret; (2) a fourth access level in the middleware gate — ‘customer’ — resolving an X-Customer-Token header into locals.customer, a principal structurally separate from locals.user; (3) customerCanSee(order, customer) added to /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/lib/visibility.ts, the one place that decision is made; (4) Customer.session_version, mirroring User.session_version, so a session can actually be killed. The honest hand-modelled implementation core ships with it is guest order lookup: POST /api/orders/lookup takes order number + email, goes through the existing findOrderForEmail(), and returns that one order plus a token scoped to it. A free install still answers ‘where is my order’ without a human. The paid pack owns the standing identity: the emailed sign-in, the durable session, /api/account/me, /api/account/orders, /api/account/orders/:id, PATCH /api/account/me for addresses, the guest-order claim flow, self-service GDPR export via the existing collectSubjectData(), and admin screens to revoke a customer’s sessions or resend a link.
- Cost: Core seam: 5–8 days. The middleware gate is the delicate part — it is a fourth branch in a file whose comments document two past incidents from exactly this class of change. Paid pack: 2–3 weeks including the claim flow and the admin side. Ongoing: a login surface is the thing customers get locked out of, so this creates real support load, which is the point.
- For: Honest under the stated rule: the free tier does something real, and the paid tier is not a licence check. Puts the authorisation decision in one reviewed place rather than in every handler. Cookies never enter the picture, so nothing about the existing CSRF posture changes. Every guest order already carries customer_id, so order history works on day one with no new matching rule. Accounts make the storefront BIGGER (4–6 more screens), feeding the ~3K EUR eshop service rather than competing with it.
- Against: The GPL core publishes the seam, so a competent developer can rebuild the paid half in a week or two. Adding a fourth gate to the middleware is the highest-risk diff in this plan. There is no third-party credential and no per-country obligation here, so the only honest thing being sold is the support commitment — say that out loud rather than dressing it up.
- A2 — Plugin-only: account routes as self-guarding public endpoints (Q1) — No core change at all. The paid pack registers /api/plugin/customer-accounts/* with access: ‘public’ and does its own token check inside every handler. The reserved-prefix rule and the widen-access rule force this shape if core is not touched.
- Cost: 1–2 weeks less work up front. No core review needed.
- For: Ships fastest. Keeps the free/paid line trivially clean — nothing about accounts exists in core at all.
- Against: This is precisely the failure mode /Users/Theo/Projects/AstroCMS/AstroCMS_v1/src/lib/visibility.ts was written to close: ‘a handler that forgets its check returns EVERYTHING’. Eight or nine public routes each re-deciding who the caller is, over a storage layer with no row-level security, is eight or nine chances to leak another customer’s order. It also puts the storefront’s account URLs under /api/plugin/customer-accounts/, leaking the pack’s identity into the public API. Do not do this.
- A3 — Customers as User rows with a ‘customer’ role (Q1) — Reuse the existing staff auth wholesale: give each customer a User row, add ‘customer’ to the Role union, let the existing magic-link flow sign them in.
- Cost: 2–3 days. By far the cheapest.
- For: Zero new auth machinery. Sessions, 2FA, revocation and audit all work already.
- Against: Rejected. Role is consumed by canManageAllPosts, canReadCommerce, canWriteCommerce, canManageCatalog and roughly a dozen other gates, several of which are written as deny-lists rather than allow-lists. One of those defaulting open puts a shopper inside the admin. It also makes /api/auth/magic mint a staff SESSION_COOKIE for a customer, and it pollutes the staff user list with every buyer. The Customer entity already exists and is already linked to orders; there is no reason to reach for this.
- B1 — Separate installs plus one-way catalogue sync (Q2, recommended) — Each expansion store is its own AstroBaaS install — its own container, volume, database, domain, theme, Stripe account and order-number sequence, which is exactly the deploy shape docker-compose.yml already describes. One install is nominated catalogue master. Its existing signed webhooks (product.created / product.updated / product.deleted already exist in webhook-util.ts) push to the satellites, which upsert on SKU or GTIN — both fields already exist on Product and ProductVariant. Solves the merchant problem ‘I re-key the same 800 products into the second shop and they drift within a month’.
- Cost: There is no product export/import today — only the Woo importer and the content CSV transfer at /api/transfer/[collection]. So: a product export endpoint, an idempotent upsert-by-SKU import, conflict rules for locally-overridden fields (price is the one that always gets overridden), and a reconciliation sweep for missed webhooks. 2–3 weeks. Zero risk to existing installs.
- For: Nothing breaks, because nothing existing changes. Each store fails independently. A store can be sold, migrated or shut down by deleting a container. Per-store locales work, which they cannot in one install — locales are read from SITE_LOCALES at BUILD time into astro.config.ts. Maps directly onto the managed-hosting product being stood up next week.
- Against: No shared stock. Two installs selling one physical shelf will oversell, and no amount of sync fixes that — it needs a single source of truth for inventory, which is a different project. No single customer identity across stores, no consolidated reporting, no shared basket. Price overrides need an explicit rule or the sync will stomp them.
- B2 — One install, store dimension threaded through the schema (Q2) — Add store_id to products, prices, inventory, orders, customers, content and settings; thread it through every read; resolve the active store per request from the hostname.
- Cost: 8–14 weeks, and I want to be precise about why. There are 725 LocalDB.* call sites across 113 API route files and 116 test files. Every storage driver stores (id, data JSON) with no unique indexes and no query predicates — getOrders() returns the whole table and callers filter in memory — so a store filter is not a WHERE clause, it is a hand-written filter at every call site, and every list read stays O(all stores). Settings is a flat key/value table read wholesale on every request, so it needs a per-store-with-fallback resolution rule. The active theme is one row. Branding is settings. Payment credentials are settings, so one Stripe account. Order numbers come from a single nextSequence(‘order_number’) producing install-wide OG-1042. Locales are build-time env vars and cannot vary per store without a separate build. Media is one volume with one URL base. Then it becomes a permanent tax on every feature written afterwards.
- For: One admin, one login, one catalogue, shared stock, consolidated reporting. It is what Magento has, and it is genuinely what a merchant with two storefronts on one warehouse wants.
- Against: Recommend against. The failure mode is the exact one visibility.ts exists to prevent, scaled up: a handler that forgets the store filter returns every store’s orders — other people’s customers’ addresses — and the storage layer will not stop it. Magento can do this because store_id was in the schema on day one; retrofitting it onto a document store that was deliberately built single-tenant is not the same project. If it is ever wanted, it is a v2 with a new storage layer, not a feature branch.
- B3 — Channels: a catalogue-visibility tag, not a store (Q2) — An optional channel tag on products (and optionally orders) plus one filter in the existing visibility module, so one install can serve two storefronts that share stock, customers and admin but show different catalogues. Solves the narrower real problem: ‘the workshop site should show the parts catalogue and the retail site should not, but they sell off the same shelf.’
- Cost: 1–2 weeks, if it stays a tag. The filter goes in one place — the same module that already owns ‘who may see what’ — so the leak class stays closed.
- For: Cheap, and it is the honest answer for one merchant with two shopfronts on one stock pool, which B1 cannot serve. Sits inside the architecture rather than against it.
- Against: It is not multi-store and must not be sold as it. No per-channel pricing, no per-channel settings, no per-channel theme, no separate order numbering, no separate Stripe account. I have not verified how the search index or the /api/content/changes feed would treat a channel tag — that needs checking before committing.
- B4 — Managed hosting as the multi-store product (Q2) — Do not build multi-store into the software at all. Sell it: the managed platform provisions N installs, gives the merchant one control panel listing them, and runs the B1 catalogue sync between them as an operated service.
- Cost: Provisioning and a cross-install control panel — real work, but it is work already implied by launching managed hosting next week, not new architecture.
- For: Turns a schema problem into an ops problem already being solved. Matches how the market actually prices this: multi-store is an enterprise TIER everywhere, not a module. Recurring revenue on the exact customers most likely to want a second store.
- Against: Does nothing for a client self-hosting on their own premises, who will still ask. The honest answer to them is B1 plus the sync pack, run by themselves.
Recommendation. Take A1 for accounts and B1 (packaged with B4) for stores; hold B3 as the answer to the one-warehouse-two-storefronts case, and refuse B2 outright.
On the free/paid line for accounts, quoting the rule back: “core owns the interface + an honest hand-modelled implementation; a paid pack owns the credential, the per-country obligation, or the support commitment.” There is no credential here and no per-country obligation — so the only honest lever is the third one, and the owner should know that before pricing. Core gets the customer principal, the visibility rule, the token primitives, and guest order lookup, which is a real feature a free install can ship. The pack gets the standing identity and the support commitment behind it.
Three findings that should change the plan as written:
First, there is no third gate. The middleware knows staff, admin and public, and nothing else (src/lib/plugin-platform/routes.ts). A customer-authenticated route is a fourth thing that does not exist, so without a core change every account route must declare access: ‘public’ and guard itself — the shape src/lib/visibility.ts was written to eliminate. The core seam is not optional or cosmetic; it is what makes the paid pack safe.
Second, /api/auth/ is a reserved prefix no plugin may claim, and anything declaring access: ‘public’ is forced under /api/plugin/
Third, and this is the good news: the “link past guest orders” problem is already 90% solved in the data. placeOrder in src/lib/commerce-service.ts calls getCustomerByEmail and stamps customer_id on every order, including anonymous ones. Sign in with that address, list orders where customer_id matches, and the history is there with no new matching rule. Only orders placed under a DIFFERENT address need an explicit claim — and that claim must go through the existing findOrderForEmail(), which already refuses to distinguish “no such order” from “wrong email” and already refuses erased orders. Order numbers are sequential (OG-1042) and therefore guessable, so number-plus-email must never by itself attach an order to a standing account: send a confirmation link to that address first.
Session handling for a headless front end is already decided by code, not by preference. src/lib/security-headers.ts deliberately omits Access-Control-Allow-Credentials, with the reason written down: “auth is a bearer token in a header, never an ambient cookie — which keeps the API CSRF-safe even with a wildcard origin.” Using cookies would mean adding that header, dropping the wildcard origin, setting SameSite=None, and re-introducing CSRF on customer routes — a real regression for one convenience. So: bearer, in a NEW header (X-Customer-Token), not Authorization: Bearer, because that header is already the API-key channel and an API key authenticates as a staff role. The Next.js storefront should hold the token in an httpOnly cookie on its OWN domain and proxy calls server-side, which keeps it out of JavaScript on both origins. The sign-in link should land on the storefront, which POSTs the token to the CMS — so the credential never appears in the CMS’s own access logs or Referer headers.
Pricing anchors — these are anchors, not recommendations, and current listed prices should be verified before quoting anyone:
Customer accounts are table stakes everywhere, and this cuts against charging for them. Shopify includes customer accounts in every plan, Basic listed around USD 39/month. Magento Open Source includes customer accounts free, and includes multi-store (websites/stores/store views) free too. So the market anchor for “accounts” as a paid module is roughly zero. What IS sold: third-party Shopify customer-portal and order-history apps, commonly around USD 5–20/month per shop — the 100-app sample supplied separately should be checked for the current spread. That maps to roughly EUR 60–240/year per shop, which against a ~3K EUR eshop build is 2–8% and does not cannibalise the service. For multi-store the anchor is a tier, not a module: expansion stores live on Shopify Plus, listed from roughly USD 2,300/month. WooCommerce official extensions, the closest self-hosted comparable, sit around USD 49–249/year per site.
The commercial read: price the accounts pack low and recurring (the EUR 60–240/year band), because the software is GPL and the seam is public — what is actually being sold is somebody answering the phone when a customer cannot sign in. Price catalogue sync higher, because an unattended sync that corrupts a satellite’s prices is a support event with a real cost. And put multi-store behind managed hosting, where the market already puts it.
Work, in order
- Core, step 1 — the token primitives. Add signCustomerToken / verifyCustomerToken to src/lib/auth.ts using the existing signPurposeToken with two NEW purpose tags: ‘cust-link’ (15 min, carries uid + sv + a random jti) and ‘cust’ (session, carries uid + sv + an audience claim). Never reuse the ‘magic’ tag: src/pages/api/auth/magic.ts turns a ‘magic’ token into a staff SESSION_COOKIE with a role, so a customer token verifying as ‘magic’ would be a staff session. Export both from src/core/index.ts, which today exports only signWebhook and newWebhookSecret.
- Core, step 2 — a revocable identity. Add session_version?: number to the Customer interface in src/core/models.ts, and bump it on sign-out-everywhere and on GDPR erasure. Without it a stateless customer session cannot be killed. Give the session token a short TTL (30 min) refreshed against the stored sv, mirroring how User.session_version already kills outstanding staff magic links.
- Core, step 3 — the fourth gate. Teach src/middleware.ts to resolve an X-Customer-Token header into locals.customer (id + email only). It must never write to locals.user, and locals.customer must never carry a Role. Add ‘customer’ to PluginRouteAccess so a plugin route can declare it and be gated by the middleware rather than by its own handler. Add X-Customer-Token to the Access-Control-Allow-Headers list in src/lib/security-headers.ts. Do NOT add Access-Control-Allow-Credentials.
- Core, step 4 — one place decides. Add customerCanSee(order, customer) to src/lib/visibility.ts. It must require BOTH customer_id match AND normalised-email match, and must return false for any order where isErased(order) is true. Every account read goes through it.
- Core, step 5 — the honest free feature. POST /api/orders/lookup: order number + email, routed through the existing findOrderForEmail() in src/lib/commerce/order-lookup.ts, returning that one order through a field allow-list plus a short-lived token whose audience claim is that single order id. Add it to PUBLIC_API_WRITE in the middleware, throttled per IP+email like the forgot and magic-link routes.
- Core, step 6 — the serialiser, and treat this as security-critical. Write one publicOrderView(order) that builds the response from an explicit ALLOW-LIST of fields. Never JSON.stringify an Order: it carries payment_reference, payment_events, the refund ledger, external_receipt_no and — on an optician’s install — prescription data. An allow-list fails closed when a field is added later; omission fails open. Add a test that a new Order field does not appear in the response by default.
- Core, step 7 — auth endpoints, in core because /api/auth/ is a reserved prefix no plugin may claim. POST /api/auth/customer/request: copy the rules of src/pages/api/auth/magic-link.ts line for line — always the same neutral 200, per-IP+email throttle, gated on emailChannelActive(), timing equalised with a dummy PBKDF2 — plus one rule the staff route does not need: the response must be identical whether or not the address has ever placed an order, because otherwise it is a ‘has this person shopped here’ oracle, and for an optician that is health-adjacent. POST /api/auth/customer/session: exchange the link token for a session token, marking the jti single-use through sharedRateLimitStore().consumeOnce exactly as src/pages/api/auth/magic.ts does.
- Paid pack — the account surface, declared ‘customer’ so the middleware gates it: GET /api/account/me, GET /api/account/orders, GET /api/account/orders/:id (404 when it is not theirs, never 403 — 403 confirms existence), PATCH /api/account/me for name, phone, billing and shipping address, and GET /api/account/export wrapping the existing collectSubjectData() in src/lib/gdpr.ts so the shop stops answering Article 15 requests by hand. Deliberately NOT included: order editing, order cancellation, and any stored payment method.
- Paid pack — the claim flow for guest orders placed under a different address. POST /api/account/orders/:number/claim takes number + email, verifies through findOrderForEmail(), then emails a confirmation link to THAT address before writing the link. Never match on name or shipping address: a shared household address would hand one person another’s orders.
- Paid pack — admin and audit. A customer detail screen showing their orders and sessions, a revoke-sessions button that bumps Customer.session_version, and a resend-link button. Write recordAudit events for customer sign-in and for every claim, the way staff logins already do.
- Negative tests before shipping either half — each must fail without its fix: a customer token rejected on a staff route; a staff API key rejected on an account route; order N+1 fetched with customer N’s token returning 404; an erased order unreachable through the account; the sign-in request returning byte-identical bodies and comparable timings for a known and an unknown address; a link token replayed twice succeeding once; a session token surviving a session_version bump; and a newly added Order field absent from the public serialisation.
- Stores, step 1 — write down which problem is being solved, because B1 and B3 answer different ones. Two separate businesses, or one business with two warehouses: separate installs. One business, one shelf, two storefronts: channels. Getting this wrong costs the whole build.
- Stores, step 2 — the sync pack. A product export endpoint (none exists; only the Woo importer and the content CSV transfer at /api/transfer/[collection]), an idempotent upsert keyed on SKU then GTIN, an explicit rule for locally-overridden fields with price defaulting to local-wins, driven by the existing product.created/updated/deleted webhooks, plus a scheduled reconciliation sweep because webhook delivery is at-least-once and sometimes none. Ship a dry-run planner that shows what WOULD change before it writes, matching the shape planContentImport already uses.
- Stores, step 3 — say plainly in the docs that separate installs do not share stock, and that two installs selling one physical shelf will oversell. Do not let the sync pack imply otherwise.
Risks. The fourth access gate in src/middleware.ts is the single riskiest diff in this plan. That file’s own comments document two incidents from this exact class of change — a plugin declaring csrf: ‘exempt’ silently disabling CSRF on a core handler, and the D2-2 scope-gate note about public GETs establishing an identity. A new branch that resolves a principal before routing needs the same paranoia: locals.customer must be provably unable to reach any staff gate, and there should be a test asserting that a customer token produces locals.user === null on every route.
Sequential order numbers are the standing enumeration hazard. OG-1042 tells an attacker OG-1041 exists and roughly how many orders a day the shop takes. Every current path through findOrderForEmail() is safe because it refuses to distinguish “no such order” from “wrong email” — but every NEW path is a fresh chance to write the friendly error message. Route everything through that function; do not add a second copy of the rule.
The response serialiser is where a leak will actually happen. An Order carries payment_reference, the payment_events idempotency ledger, the refund history, external_receipt_no, and on an optical install the prescription. A future field added to the model lands in the customer’s response automatically unless the serialiser is an allow-list. This is the classic sibling-gap shape.
The paid seam is thin and the owner should decide with that in front of them. Core will publish the token primitives, the principal, and the visibility rule — because it must, for the free order-lookup feature to work and for the pack to be safe. Given that, rebuilding the account pack against the seam is a week or two for a competent developer, and GPL guarantees they may. What is being sold is the support commitment, not scarcity. Pricing it as though it were scarce will not hold.
The market anchor points the other way on accounts. Shopify includes them in every plan; Magento Open Source includes them free. A self-hosted CMS charging separately for customer accounts will be asked about it, and the answer has to be the support commitment and the maintained login surface, not the feature.
Erasure and accounts interact and it is easy to miss. eraseSubject() in src/lib/gdpr.ts anonymises orders and writes erased_at. It must also bump Customer.session_version, or someone who asked to be forgotten keeps a live session over their own anonymised orders for the rest of the token’s TTL.
On stores: locales are the hard structural blocker on B2 that is easy to overlook until late. SITE_LOCALES is read at BUILD time into astro.config.ts for routing. A per-store locale set is not expressible in one install without a separate build, so the “one install, many stores” design cannot deliver a Greek store and a German store from the same process. That alone probably decides the question.
Two things I did not verify and would check before committing: how the search index (src/lib/search/) and the /api/content/changes feed would treat a channel tag under B3 — the change feed is served publicly, and a channel-scoped record appearing in it would be the same class of bug the PluginDataRecord comment describes; and whether any storage driver enforces slug uniqueness in a way that a multi-store or multi-channel catalogue would violate (SlugTakenError exists, but I did not trace its scope).
WCAG 2.2 AA and the EAA: who owes what, what ships already, and what is actually sellable
The EAA binds the MERCHANT, not AstroBaaS — and most of this customer base (small optical shops, frontistiria, restaurants) is inside the micro-enterprise exemption for services — so accessibility is not a compliance product here: it is a disqualifier if absent and worth €0 as a module if present. Build it into GPL core, gate it in CI, sell the audit-with-a-name-on-it as a service, and never ship an overlay. The repo is also materially closer than its own docs claim: the proof-of-work CAPTCHA is the best available answer to WCAG 2.2 SC 3.3.8, <html lang>/dir are correct with RTL checked in CI, prefers-reduced-motion is honoured in every plugin that animates, and the consent banner deliberately does not trap focus. The real gap is concentrated in four places, not everywhere.
Options
- A. Honest silence — document the narrow truth, build nothing — Keep the existing WCAG AA contrast gate and the two ARIA landmark assertions, write down exactly what they cover and what they do not, and make no accessibility claim anywhere. This is what docs/ECOMMERCE-ROADMAP.md:335-341 already tells the owner to do.
- Cost: Roughly a day of documentation. Ongoing obligation: every future release note has to keep resisting the temptation to say ‘accessible’.
- For: Zero risk of a false claim, which is the failure mode TRUTH_PLAN.md was written to catch. Costs nothing. Stays true.
- Against: The first German enterprise, public-sector, or agency RFP that asks ‘is your admin keyboard operable’ ends the conversation. It also leaves real Level A failures shipping — there is no skip link anywhere in src/layouts/AdminLayout.astro or src/layouts/PublicLayout.astro, which is WCAG 2.4.1 Bypass Blocks, Level A, on all 28 admin screens behind a full sidebar. That is not a claim problem, it is a defect.
- B. Conformance-by-gate — make WCAG 2.2 AA a CI gate in core (FREE) — axe-core inside the Playwright suite that already exists, driven by the machine-readable ADMIN_NAV list in src/lib/admin-nav.ts plus reservedSlugs(), landed against a frozen baseline; a scripted keyboard walk (Tab reaches the primary action, focus is always visible, Escape closes every dialog, focus is restored); the contrast gate at tests/theme-tokens.test.mjs:177-184 extended past body-on-background to links, button fills, muted text and the 3:1 focus ring; plus crude grep-shaped static gates in the house style (every
in src/**/*.astro has alt, every icon-only button has an accessible name, both layouts contain a skip link).
- Cost: One new devDependency (@playwright/test is already there, so @axe-core/playwright is the only add). ~2 weeks to land the gates plus the baseline; ~3-4 weeks to burn the baseline down. Ongoing obligation: the baseline file must shrink, never grow, or it becomes a graveyard.
- For: It is the only option that makes the honest claim earnable. It fits the repo’s existing verification culture exactly — tests/reserved-slugs.test.mjs already proves a hand-kept list against src/pages/, so the anti-drift pattern is written. It catches regressions at build time, where they are cheap.
- Against: axe finds roughly a third of WCAG issues and none of the ones that matter most here — it cannot judge focus order inside a contenteditable, whether alt text is meaningful, or whether an error message makes sense. It also lands red, and a red gate that blocks unrelated PRs gets disabled within a week unless the baseline goes in with it on day one.
- C. Merchant compliance kit in core (FREE) + the audit sold as a service (PAID) — Core gets: a three-state alt model (described / marked decorative / undescribed — today an empty alt_text is ambiguous between decorative and nobody-filled-it-in, which is exactly why imagesMissingAlt has to guess), an optional publish policy that blocks on undescribed images, a heading-outline check in src/lib/content-analysis.ts (it counts headings and measures the run between them but never checks the outline for a skipped level or a second h1),
langadded to the sanitizer allow-list at src/lib/sanitize.ts:108-130 so an author can mark a German quotation inside a Greek article — WCAG 3.1.2 is literally impossible for the author today on a trilingual product — and an accessibility STATEMENT generator built on the existing src/lib/legal/templates.ts shelf. The paid half is not a module: it is the audit, the EN 301 549 clause mapping, and the signed statement for a tender.- Cost: Core: ~3 weeks (the alt model is a migration; the statement template is a day since the legal-template machinery, placeholder filling, [fill in:] markers and draft-not-published behaviour are all built). Paid: no build at all — it is billable hours plus a subcontracted auditor. Ongoing obligation: the statement template must be re-read whenever the national transpositions move.
- For: The free/paid line falls exactly where the seam rule puts it. Core owns the interface and an honest hand-modelled implementation; the paid side owns the support commitment and the signature. It also sells: ‘the platform generates your statement, we audit and sign it’ is a real service ladder on top of the ~3K EUR eshop, and it does not cannibalise a theme skeleton because it is hours, not code.
- Against: An accessibility statement generated by the vendor that overclaims is a liability with the owner’s company name on it, so the template must report status — tested what, when, by whom, known gaps — and never blanket conformance. That discipline has to hold every time someone edits the template.
- D. A paid ‘EAA compliance pack’ module (REJECT) — Put the alt policy, the statement generator, the accessibility report and an accessible admin behind a paid plugin in the separate repo.
- Cost: Similar build to C, plus the packaging.
- For: Recurring revenue on a legal deadline, which is the easiest thing in the world to sell in 2025-2026.
- Against: It breaks the seam rule as written: there is no credential and no per-country obligation involved in making your own admin keyboard-operable — the seam here is a SUPPORT COMMITMENT, and a support commitment is a service, not a module. It is also mispriced by the market: Shopify charges nothing for theme accessibility (its Theme Store imposes accessibility requirements on themes at no cost to the merchant), so the merchant’s baseline expectation for ‘the platform does not obstruct me’ is €0. And selling keyboard operability as an upsell means shipping a core that is deliberately unusable by a disabled staff member, which is indefensible for a GPL project and would be the first thing a hostile reviewer wrote about.
- E. An accessibility overlay widget (REJECT, explicitly) — The Shopify app-shelf answer: a JS widget that claims to remediate a site at runtime from a toolbar.
- Cost: A week to build a bad one. Ongoing: reputational.
- For: It is what the market visibly buys. ANCHOR, NOT A RECOMMENDATION — Shopify App Store list prices for this category run roughly $19–$49/month USD (accessiBe, UserWay and AudioEye all sit around $49/mo USD at their small-site tiers; lighter apps such as Accessibly sit nearer $19–$39/mo USD). Re-check current pricing before quoting; these are the figures the category advertises, not verified quotes.
- Against: Overlays do not fix the underlying failures, they are publicly opposed by accessibility practitioners, and several vendors in this category have been the subject of US litigation over exactly the claims they sell. For a project whose entire positioning is TRUTH_PLAN-grade honesty about what it does and does not do, shipping one would trade a few hundred euros a month for the only asset the product actually has.
Recommendation. Run B then C, with A’s discipline applied to every sentence either of them produces; refuse D and E in writing so they do not come back. Sequence matters: land the CI gates and the frozen baseline BEFORE touching the merchant-facing features, because a statement generator built on top of an unmeasured platform is the false claim this repo keeps catching itself making. Be precise in all external copy about who owes what. The EAA (Directive (EU) 2019/882) has applied since 28 June 2025 to service providers, e-commerce services among them, and it binds the merchant operating the shop — not AstroBaaS, which is a tool. Micro-enterprises providing services are exempt (under 10 staff and turnover or balance-sheet total at or under €2m), which covers most optical shops, frontistiria and restaurants in the target verticals, so the honest pitch is never ‘you must’ — it is ‘you will be asked, by a tender, a bank, an insurer, or a customer with a complaint, and the exemption evaporates the day you hire your tenth person’. Judge merchants against the NATIONAL transposition, not the Directive: Germany’s BFSG and its BFSGV are what a German client is measured by, and Greece has its own transposing law whose reference I have not verified and which counsel should supply. Build to WCAG 2.2 AA rather than to a standard version number: it is a superset of what EN 301 549 currently cites for web, so it covers both regimes without betting on which EN 301 549 revision is OJ-cited when a client asks. Note also that the education vertical can pull in a DIFFERENT regime — a public school or municipality buyer falls under the Web Accessibility Directive (2016/2102), which mandates a prescribed-form accessibility statement plus a feedback mechanism; do not let the EAA template be reused there unchecked. Two structural corrections to the framing in the question. First, the admin’s accessibility is NOT an EAA obligation — staff tools are not a consumer e-commerce service. It matters for hiring (a merchant who takes on a blind bookkeeper and cannot give them the orders screen has a reasonable-accommodation problem under national law transposing Directive 2000/78/EC) and for procurement (public buyers score internal tools against EN 301 549 too). Sell it on those grounds and it stays true. Second, ‘accessible names in structured data’ rests on a false premise: JSON-LD is not exposed to assistive technology and does nothing for accessible names. What a headless front end actually needs from the API is alt text and a decorative flag beside every image URL — src/lib/content-refs.ts:143 already emits ${name}_alt, so the pattern exists and only needs generalising — plus the heading outline, the per-field locale so the storefront can emit lang= on a foreign-language passage, and link text that is not ‘read more’. Fix the API contract, not the JSON-LD. Finally, the bundled-themes half of the plan deserves resizing. The rule that an SSR-only feature does not exist cuts hard here: the three bundled themes are not what either live shop renders, so theme accessibility is demo-and-future-client value, not merchant-compliance value. But the platform-rendered HTML that DOES reach a headless storefront’s visitors is real and consumer-facing — the consent banner plugin, the popups plugin, the assistant widget served from /assistant-widget.js, the public submission-form runtime, /receipt.astro, /legal/withdrawal.astro, the print-styles plugin, and the order-confirmation email HTML. That set is the merchant’s EAA surface inside this repo, and it is where the (b) budget belongs. Everything already good there should be claimed rather than rebuilt.
Work, in order
- Write the scope note first, before any code: who is bound (the merchant, as e-commerce service provider), who is exempt (micro-enterprises providing services, under 10 staff and ≤€2m), which national text applies per client (BFSG/BFSGV in Germany; the Greek transposing law, reference to be supplied by counsel — do not guess it), and the separate Web Accessibility Directive regime that a public-sector education buyer would bring. Also settle whether the managed hosted AstroBaaS launching next week is sold to consumers or businesses only — B2B-only sits outside the e-commerce-service definition, but if a sole trader can buy it online the owner’s own company is the service provider, and that is a decision to make deliberately rather than discover.
- Fix the Level A failure that is already shipping: add a skip link to src/layouts/AdminLayout.astro and src/layouts/PublicLayout.astro. Tailwind’s sr-only is already in use (src/pages/404.astro:64),
already exists in both layouts (AdminLayout.astro:63, PublicLayout.astro:73), so this is roughly three lines each plus a static test asserting both layouts contain one. WCAG 2.4.1, Level A, 28 admin screens behind a full sidebar. - Add @axe-core/playwright to the existing suite in tests/e2e/. Drive the page list from ADMIN_NAV in src/lib/admin-nav.ts plus reservedSlugs() so it cannot silently stop covering a new screen — the same anti-drift pattern tests/reserved-slugs.test.mjs already uses. Land it in the SAME commit as a frozen baseline file recording today’s violations: new violations fail, existing ones are listed and burned down. A gate that lands red gets disabled.
- Add the keyboard walk axe cannot do: Tab through each admin screen asserting focus is always visible, the primary action is reachable, Escape closes every dialog, and focus returns where it came from. This is where the concrete defect lives — src/layouts/AdminLayout.astro:70 declares role=“dialog” aria-modal=“true” and correctly moves focus in, restores it, and closes on Escape (lines 104-139), but there is no focus trap, so Tab walks out of a dialog that has just told assistive technology the rest of the page is hidden.
- Extend the contrast gate at tests/theme-tokens.test.mjs:177-184 past body-on-background to link colour on background, button text on button fill, muted text, and the focus ring at 3:1 (SC 1.4.11). While there, fix src/lib/theme-tokens.ts:218: readableOn() picks black or white by a luminance threshold (> 0.45) instead of computing a ratio, so nothing proves the pair it returns reaches 4.5:1 — replace the heuristic with the ratio the test file already knows how to compute.
- Fix the editor, which is the worst surface in the product and the one no scanner will judge for you. src/components/admin/RichTextEditor.astro:100 is a bare contenteditable with no accessible name and no role/aria-multiline; its placeholder is CSS :empty:before content (line 121) so it is never announced; the toolbar buttons carry title only; and .toolbar-btn.active (line 117) conveys pressed state by colour alone with no aria-pressed (SC 4.1.2 and 1.4.1). Target size is fine — min-w-[28px] h-7 clears the 24×24 of SC 2.5.8 — so do not churn it.
- Change the alt data model before touching the media UI: add an explicit decorative flag alongside alt_text so described / decorative / undescribed are three distinguishable states. Today an empty string means both ‘correctly decorative’ and ‘nobody wrote one’, which is precisely why the audit in src/pages/admin/insights.astro:157-170 has to guess and why its comment has to explain the guess. Migration plus the ingest path in src/lib/media/ingest.ts:337 and the patch route in src/pages/api/media/update.ts.
- Then replace the alt-text prompt() in src/components/admin/MediaLibrary.astro:605-630 with a real labelled field that shows the image being described and offers the decorative checkbox — the comment there says a modal was skipped precisely to avoid building a focus trap, and step 4 will have produced a reusable one. Do NOT bulk-replace the other 84 alert/confirm/prompt call sites across the admin: native browser dialogs are focus-managed and announced, so they PASS, and hand-built replacements fail until each gets a trap. Replace only where the dialog must show content the native one cannot.
- Add
langto the sanitizer allow-list at src/lib/sanitize.ts:108-130 (currently ’*’: [‘class’,‘id’]), with a test. Without it an author on a trilingual el/de/en product cannot mark a German quotation inside a Greek article, which makes SC 3.1.2 Language of Parts unachievable by the author — a platform making compliance impossible, which is the one thing the framing says must not happen. - Add a heading-outline check to src/lib/content-analysis.ts. It already counts headings and measures the longest run between them (lines 152-160, 280-289) but never checks the outline itself: a skipped level (h2 to h4), or a second h1 in the body colliding with the theme’s. Surface it in the same panel authors already read, and expose the outline on the content API so a Next.js storefront can render it.
- Generalise the API contract for headless consumers: alt plus decorative beside every image URL (src/lib/content-refs.ts:143 already emits ${name}_alt — make it universal, not one call site), the heading outline, and the locale of each field so the storefront can emit lang= correctly. Document it in INTEGRATION.md as the accessibility contract, because a headless front end can only be accessible if the API tells it these things.
- Add the accessibility STATEMENT template to src/lib/legal/templates.ts in en/el/de, reusing the machinery already there — placeholders filled from settings, visible [fill in: …] markers for gaps, created as a DRAFT. It must state status, never conformance: what was tested, on what date, by whom, which known gaps remain, and how to report a barrier (a feedback mechanism is mandatory under the Web Accessibility Directive regime and is good practice under the EAA). Core, free, on the same shelf as privacy/terms/imprint.
- Add the crude static gates in the repo’s existing style, which never flake and catch things at build time: every
in src/**/*.astro has an alt; every icon-only
- Book the human audit that automation cannot replace — a screen-reader run (NVDA on Windows, VoiceOver on macOS and iOS) of login → new post → insert image → publish, plus a full checkout on a real headless storefront; a focus-and-reading-order review of the editor; a plain-language review of error messages; and a look at the 3D showcase island. ANCHOR, NOT A RECOMMENDATION: EU agencies commonly quote roughly €3,000–€10,000 for a WCAG 2.2 AA audit of a storefront plus admin, and a US VPAT/ACR commonly runs roughly $5,000–$15,000 USD; get two real quotes rather than trusting these figures.
- Only after the audit, update the public claims — README.md:391, TRUTH_PLAN.md:444-446 and 1273-1277, docs/ECOMMERCE-ROADMAP.md:335-341, BACKLOG.md:212 — and add the sellable service to the price list: audit, remediation, statement with the company’s name on it, and (for the managed hosting) recurring monitoring.
- Claim what already ships, in the same pass. The proof-of-work CAPTCHA in src/lib/captcha.ts is the strongest available answer to WCAG 2.2 SC 3.3.8 Accessible Authentication — no cognitive test, no puzzle, no third party — and it is on /login, where Shopify’s reCAPTCHA is a cognitive test. and dir are correct (src/layouts/BaseLayout.astro:133) with RTL generated and verified in CI (scripts/gen-rtl.mjs —check). prefers-reduced-motion is honoured in the popups, assistant and consent-banner plugins and on /showcase. The consent banner explicitly refuses to trap focus and ships :focus-visible outlines (src/plugins/consent-banner/index.ts:21,47). Breadcrumbs and the language switcher carry aria-label and aria-current, and CI asserts both (tests/smoke.mjs:2557,3291). The sanitizer preserves table semantics including scope and headers, and figure/figcaption (src/lib/sanitize.ts:58-125). None of this is new work — it is a page of true claims that most competitors cannot make.
Risks. “The claim risk is the big one, and it is the repo’s own known failure mode. The moment a statement generator or a README says ‘WCAG 2.2 AA’, the project has made a claim it has not earned, on the owner’s company letterhead, about a legal obligation — the exact shape TRUTH_PLAN.md exists to catch, and it already caught this once (docs/ECOMMERCE-ROADMAP.md:335 warns against claiming compliance on the strength of the contrast gate). Every generated statement must be dated, scoped, and list known gaps. Second and most under-appreciated: THE LARGEST EAA SURFACE IS NOT IN THIS REPO. Both live storefronts are headless Next.js, so the merchant’s actual e-commerce service is the front-end code the owner ships as a services business at ~1K/~3K EUR. Making AstroBaaS accessible does not make those storefronts accessible — it only makes it possible. If the front-store accessibility work is not budgeted into the service price, the exposure lands on the owner’s own agency, on projects already sold. Third, the micro-enterprise trap: most of the target verticals are exempt today, so ‘you must comply’ is a claim a client can check and find false, which poisons every other claim; sell the trigger events (a tender, a bank, an insurer, a complaint, the tenth hire) instead. Fourth, the red-gate risk: axe will land with real violations, and a gate that blocks unrelated PRs gets disabled within a week — the frozen baseline must ship in the same commit, not as a follow-up. Fifth, standards drift: EN 301 549 revisions and the OJ citation move, so build to WCAG 2.2 AA and cite the national transposition (BFSG/BFSGV for Germany; the Greek transposing law, whose reference I have NOT verified and must come from counsel, not from this plan) rather than the Directive. Sixth, the overlay temptation will return every time a competitor’s $49/mo widget wins a deal; the refusal should be written down with its reasons so it does not have to be re-argued. Seventh, a regression risk specific to step 8: replacing the 85 native alert/confirm/prompt call sites wholesale would make accessibility WORSE, because native dialogs already pass and hand-rolled ones do not until each is trapped and labelled — treat that as a rule, not a preference. Finally, two things I could not verify and should not be reported as fact: whether every admin data table carries th scope (I checked the sanitizer’s content path, not the admin’s own tables), and whether /admin/products’ one role=“dialog” manages focus — both need reading before anyone writes a status statement.”