| 74 | Separation of concerns: platform site vs AGPL open source core | 73 | TBD | planned |
See [css-migration.md](docs/plans/css-migration.md) for full plan with architecture, visual regression testing strategy, and acceptance criteria per phase.
## Usability fixes (16/18 done)
Issues from hands-on testing of the deployed prod site (Feb 2025). 16 of 18 complete. The remaining 2 are tracked as features in the task list above (#16 variant refinement, #18 shipping costs).
## Roadmap
### Tier 1 — MVP (can take real orders and fulfil them)
1. ~~**Order management admin**~~ — ✅ Complete (02cdc81). Admin UI at `/admin/orders` with status filter tabs, streamed order table, and detail view showing items, totals, and shipping address.
2. ~~**Orders & fulfilment**~~ — ✅ Complete. Submit paid orders to Printify, track fulfilment status (submitted → processing → shipped → delivered), webhook-driven status updates with polling fallback, admin UI with submit/refresh actions.
3. ~~**Transactional emails**~~ — ✅ Complete. Plain text order confirmation (on payment via Stripe webhook) and shipping notification (on dispatch via Printify webhook + polling fallback). OrderNotifier module, 10 tests.
4. ~~**Default content pages**~~ — ✅ Complete (5a43cfc). Generic `ShopLive.Content` LiveView handles about + 3 policy pages (delivery, privacy, terms) via `live_action`. Rich text with list blocks, footer links updated, theme editor preview. 10 tests (575 total).
### Tier 2 — Production readiness (can deploy and run reliably)
5. ~~**Hosting & deployment**~~ — ✅ Complete. Alpine Docker image (131 MB), Fly.io config, release overlays, health check endpoint, hardcoded path fixes for releases. Observability: LiveDashboard in prod behind admin auth, ErrorTracker for exception capture, JSON structured logging, Oban/LiveView telemetry metrics, os_mon for CPU/disk/memory.
6. **Litestream / SQLite replication** — Litestream for continuous SQLite backup to S3-compatible storage. Point-in-time recovery. Simple sidecar process, no code changes needed, works with vanilla SQLite. For the hosted platform (Tier 5), evaluate [Turso](https://turso.tech/) (libSQL fork of SQLite) with embedded read replicas via [ecto_libsql](https://github.com/ocean/ecto_libsql) adapter — gives multi-node reads without a separate replication daemon, but adds a dependency on the libSQL fork.
7. ~~**CI pipeline**~~ — ✅ Complete. `mix ci` alias: compile --warning-as-errors, deps.unlock --unused, format --check-formatted, credo, dialyzer, test. Credo configured with sensible defaults. Dialyzer with ignore file for false positives (Stripe types, Mix tasks, ExUnit internals). 612 tests, 0 failures.
8. ~~**PageSpeed in CI**~~ — ✅ Complete. `mix lighthouse` task runs Google Lighthouse against the shop with configurable thresholds. Builds production assets (minified + digested + gzipped), waits for image variant cache, checks all 4 categories. Mobile: 99-100, Desktop: 97-100 across all pages. Unconditional gzip on Plug.Static.
9. **End-to-end & accessibility tests** — Wallaby browser tests for critical flows (browse → add to cart → checkout → order confirmation) with A11yAudit assertions baked into each test. Covers the happy path, key error cases, and WCAG 2.1 AA compliance in one pass. Wallaby drives a headless Chrome, A11yAudit wraps axe-core for automated a11y checks within ExUnit. Focus management, ARIA labels, keyboard navigation, colour contrast — all verified as part of the e2e suite rather than a separate audit.
### Tier 3 — Compliance & quality
10. ~~**Privacy-respecting analytics**~~ — ✅ In progress. Custom cookie-free analytics with pageviews, e-commerce funnel, device/browser/OS/country tracking. Period comparison deltas on stat cards, dashboard filtering. Next: CSV export, entry/exit pages.
11. **AGPL licensing & code hosting** — Currently AGPL-3.0. Decide on GitHub vs Codeberg vs self-hosted Forgejo. Set up proper LICENSE file, contribution guidelines, and release process.
12. **Security (Paraxial.io)** — Runtime application security monitoring for Elixir. Bot detection, rate limiting, vulnerability scanning. Evaluate whether it fits the self-hosted model.
13. **No-JS support** — Audit and fix all key user flows for no-JS clients. Browse, search, cart, checkout, and analytics should all work without JavaScript. The analytics pipeline already supports no-JS (Plug records pageview, only superseded if JS connects). Cart and checkout use LiveView but should degrade gracefully.
14. **SEO** — Best-in-breed SEO to match or exceed Shopify/Squarespace/WordPress plugins. Page titles with site name separator across all pages. Open Graph and Twitter Card meta tags for products (with images, prices), collections, and home. JSON-LD structured data (Product with offers/availability, BreadcrumbList, Organization). Canonical URLs, robots.txt, sitemap.xml (auto-generated from products/collections). Per-page meta descriptions with auto-generated fallbacks from product/collection data.
15. **Favicon & site icons** — Upload one source image, get a complete best-practice icon setup. "Use logo as icon" toggle (on by default) with fallback to a separately uploaded icon image if the logo is wide/unsuitable at small sizes. Auto-generates PNG variants at 32×32 (browser fallback), 180×180 (iOS home screen), 192×192 and 512×512 (Android/PWA) via an Oban job using the existing `image` library pipeline. If the source is an SVG, the SVG itself is served as `favicon.svg` with a `prefers-color-scheme: dark` media query injected so the icon adapts to dark mode automatically. A `FaviconController` serves all variants from the DB at the expected root paths. Dynamic `site.webmanifest` served as JSON, pulling shop name, theme colour, and background colour from settings. Customisation: short name (shown under home screen icon), icon background colour (Android circle/squircle fill), theme colour (defaults to active theme primary). `.ico` fallback is a pre-baked static file — libvips can't output ICO, and it's only needed for very old browsers anyway. See [plan](docs/plans/favicon.md).
16. **URL redirects & dead link monitoring** — Preserve link equity and UX when product URLs change. Three layers: (1) automatic redirect creation when a product slug changes during sync — detected in `upsert_product/2`, no admin involvement needed; (2) ETS-cached Plug in the request pipeline that checks redirects before routing, 301s and halts; (3) analytics-powered 404 monitoring — when a 404 fires on a path with prior analytics history, it's a real broken URL that matters (not a bot scanner). FTS5 search attempts auto-resolution; low-confidence cases surface in an admin broken URLs queue sorted by prior traffic (highest impact first). Manual redirect creation for anything else. Redirect chains flattened on creation (A→B, B→C becomes A→C). **Dead link monitoring** catches broken outgoing links in your own content (social URLs, custom pages, product descriptions): internal links validated instantly via `Phoenix.Router.route_info` — no HTTP needed; external links checked asynchronously via Oban HEAD requests. Event-driven — product deletion/rename triggers a scan of stored links. Admin "Dead links" tab with one-click "Update link" for moved URLs. Weekly re-check cron for external links. See [plan](docs/plans/url-redirects.md).
### Tier 3.5 — Business tools
17. **Profit-aware pricing (inc. tax)** — Complete cost visibility for shop owners. Fix Printful cost sync (catalog API cross-reference for variant costs). Snapshot `unit_cost` on order items at time of sale. **Stripe does the heavy lifting:** use Stripe Tax for automatic tax calculation at checkout (50+ countries, correct rates, one line of config — far better than maintaining our own rate tables); fetch exact Stripe fees from the Balance Transaction API after payment rather than estimating. **Tax as a toggle:** most small POD sellers aren't VAT registered (UK threshold: £90k), so by default there's no tax — prices are prices and all revenue is profit. When they flip the toggle, Stripe Tax activates at checkout and the shop shows "inc. VAT" (UK/EU/AU) or "+ tax" (US/CA) based on shop country. Profit calculations use actual post-payment data: exact Stripe fee + tax amount from Stripe session. Admin profit dashboard showing per-product margins, per-order P&L, and overall business health. Price editor shows live margin as you set prices. The goal: shop owners always know exactly what they're making, with no hidden costs. See [plan](docs/plans/profit-aware-pricing.md).
18. **Sales & promotions** — Transparent, honest alternative to discount codes (no empty "enter code" box at checkout — that's a dark pattern). Create time-limited sales scoped to the entire catalogue, specific categories, or individual products. Percentage or fixed-amount discounts. Scheduled start/end dates with automatic activation via Oban or date-bounded queries. Original price shown struck through with sale price. **Margin guard**: sales cannot breach a configurable minimum profit threshold — the system prevents shop owners from accidentally selling at a loss. Announcement bar (dismissable banner across the shop) to promote active sales. Later ties into newsletter for sale email blasts.
19. **Activity log & order timeline** — A single `activity_log` table records every meaningful event: order created, confirmation email sent, submitted to provider, in production, shipped (with tracking), delivered, errors and retries. Two views on the same data: (1) **order timeline** on the order detail page — a chronological feed showing the complete lifecycle of that specific order, replacing the current scattered key/value cards. Errors are never overwritten — if submission failed and retried, both entries are visible. Email sends are recorded, so you can see if the confirmation actually reached the customer. (2) **global activity feed** at `/admin/activity` — reverse-chronological stream of all system events: orders, syncs, emails, abandoned carts. Two tabs: all activity and "needs attention" (unresolved errors/warnings). Count badge on the admin nav when attention is needed. 90-day pruning via Oban cron. See [plan](docs/plans/activity-log.md).
20. **Order status lookup** — The UI stub already exists: a "Track your order" card on the contact page sidebar with an email input and Send button, but it's purely static HTML with no backend. Two possible approaches: (a) email-only lookup — customer enters their email, receives a signed magic-link showing all their orders (nicer UX, requires sending an email); (b) email + order number — inline lookup, no email sending needed, slightly more friction but simpler to build. Either way, no customer accounts required.
### Tier 4 — Growth & content
21. **Page editor** — Database-driven page builder where every page is a list of blocks. Generic renderer, portable + page-specific blocks, block data loaders, ETS cache, mobile-first admin editor. See [plan](docs/plans/page-builder.md).
22. **Legal page generator** — Replace the hardcoded `PreviewData` placeholder content on `/privacy`, `/delivery`, and `/terms` with generated content that's factually accurate for each shop. Berrypod already knows which providers are connected (each with different lead times), which countries it ships to (from the shipping rates table), whether VAT is enabled, whether abandoned cart recovery is on, the shop country (drives jurisdiction language). Privacy policy cites correct statutes (UK GDPR, PECR) and includes conditional sections only for features that are actually enabled. Delivery policy quotes real shipping destinations from DB and correctly applies the Consumer Contracts Regulations Regulation 28(1)(b) exemption for POD (made-to-order goods are exempt from the 14-day right to cancel — most generic templates get this wrong). Terms cites governing law from shop country. Phase 2 (after page editor): "Regenerate from settings" button and auto-regeneration when settings change. See [plan](docs/plans/legal-page-generator.md).
23. **Newsletter & email marketing** — Email list collection (signup forms). Campaign sending for product launches, sales. Can be simple initially (collect emails, send via Swoosh) or integrate with a service. Ties into sales & promotions for sale announcement emails.
24. **Abandoned cart recovery** — Privacy-respecting, GDPR-compliant recovery for customers who started Stripe Checkout but didn't complete payment. Triggered by `checkout.session.expired` webhook (Stripe fires this after 24h). Only possible for customers who entered their email on the Stripe Checkout page — anonymous cart sessions with no email are never contacted. Single plain-text email, no tracking pixels, one-click unsubscribe (suppression list honoured for all future emails). Abandoned cart records deleted after 30 days. Stripe Checkout footer text notifies customers at collection time. Lawful basis: UK PECR soft opt-in (email obtained during negotiation of a sale, single follow-up for similar products). EU: legitimate interests with documented LIA. See [plan](docs/plans/abandoned-cart.md).
25. **Product page improvements** — Pre-checkout variant validation (verify Printify availability). Cost change monitoring/alerts. Better image gallery (zoom, multiple angles). **Product reviews system** to replace the hardcoded `PreviewData.reviews()` on the PDP template.
### Tier 5 — Platform vision
26. **Platform/marketing site** — Berrypod.com as the public face: brochure pages (features, pricing, comparison), sign-up/subscribe flow, demo store showcase. Needs clear separation between the platform site (commercial, hosted by us) and the open source AGPL core (self-hostable, community-driven). The platform site should position Berrypod as fast, capable, and valuable — competing on speed, privacy, and simplicity against Shopify/Squarespace. Consider: separate Phoenix app or separate layout/router scope within the same app? The AGPL core ships without any platform branding — just the store engine.
27. **Hosted platform infrastructure** — Multi-tenancy with per-tenant databases. **OAuth connect flows** for providers and payments: register as a Printify/Printful OAuth app so hosted users get a one-click "Connect with Printify" button on the setup page; use Stripe Connect so merchants authorise via OAuth redirect instead of pasting API keys. The setup UI already supports this — check for OAuth credentials and show the connect button when available, fall back to the API key form on self-hosted installs where no OAuth app is configured. Provider metadata (`connect_mode: :oauth | :api_key`) drives which form renders.
28. **Migration & export** — Let shop owners export their data (products, orders, customers, theme settings). Import from other platforms (Shopify, WooCommerce). Portable data as a selling point for the self-hosted story.
29. **Internationalisation (i18n)** — Multi-language support via Gettext (already in Phoenix). Currency formatting. RTL layout support. Per-shop locale configuration. **Note:**`ex_money`/`ex_cldr` are currently used *only* for `Cart.format_price/1` (a single GBP formatting call) but add ~13 MB to the release (ex_cldr 9.5 MB, digital_token 3.7 MB, ex_cldr_numbers, ex_cldr_currencies). Consider replacing with a simple `format_price/2` function that handles GBP/EUR/USD directly — all three use 2 decimal places and are trivial to format. Re-add `ex_money` later if proper locale-aware number formatting is needed (e.g., German `12.345,67 €`).
- Filters to only published variants (not full catalog)
- Price updates on variant change
- Startup recovery for stale sync status
- [x] Per-colour product images (0fe48ba)
- `color` column on product_images, tagged during sync (both providers)
- PDP gallery filters by selected colour (hero gets all, others front+back)
- Printify options filtered to enabled variants only (not full blueprint)
- Hero/default colour ordered first in swatch list
- MockupEnricher generates per-colour mockups for Printful
- Printful catalog API fetched for hex colour codes
#### Future Enhancements (post-MVP)
- [ ] Print provider insights — fetch provider name/location via `get_print_providers/1` during sync, store in `provider_data`. Show "Ships from UK/US" on product pages. Admin dashboard showing which providers are used, their locations, and shipping cost analysis to help optimise product selection for domestic fulfilment and combined postage savings
- [ ] Pre-checkout variant validation (verify availability before order)
- Mox provider mocking for test isolation, 33 new tests (555 total)
- [x] Transactional emails (Roadmap #3)
- OrderNotifier module with plain text emails via Swoosh
- Order confirmation sent from Stripe webhook after payment + address/email updates
- Shipping notification sent from Printify shipment webhook + polling fallback
- Guards for missing customer_email, graceful tracking info handling
- 10 tests (565 total)
See: [docs/plans/products-context.md](docs/plans/products-context.md) for schema design
### DRY Refactor
**Status:** Complete
All 8 items from the plan done. Key wins: ThemeHook eliminated mount duplication, shop_layout saved ~195 lines, shop_components split into 5 focused modules (largest file dropped from 4,487 to ~1,600 lines), Settings repo lookups consolidated via `fetch_setting/1`, secrets loading made scalable via registry pattern.
See: [docs/plans/dry-refactor.md](docs/plans/dry-refactor.md) for full analysis and plan
### Shop Page Integration Tests
**Status:** Complete
All shop pages now have LiveView integration tests (612 total):
- **Product detail page** (15 tests) — rendering, breadcrumbs, variant selection, price updates, add-to-cart, related products, fallback for unknown IDs
- **Cart page** (10 tests) — empty state, item display with DB fixtures, order summary, increment/decrement, remove, checkout button
- [x] HTML/CSS bar chart with hourly today view and readable labels (08fcd60)
- [x] Period comparison deltas on stat cards (6eda1de)
- [x] 2-year demo seed data with growth curve (6eda1de)
- [x] Dashboard filtering (click referrer/country/device to filter all panels) (7ceee9c)
- [x] CSV export (ZIP with 12 CSVs, period + filter aware)
- [x] Entry/exit pages panel
See: [docs/plans/analytics-v2.md](docs/plans/analytics-v2.md) for v2 plan
### Media Library
**Status:** Complete
Admin media library at `/admin/media` with image grid, type/search/orphan filters, upload with alt text, detail panel (metadata editing, usage tracking, delete with protection). Image schema extended with `alt`, `caption`, `tags` fields and `"media"` type. `find_usages/1` scans product_images, theme settings, favicon variants, and page blocks. `delete_with_cleanup/1` refuses deletion of in-use images and cleans up disk variants. `:image` field type in block editor with image preview and ID input. Page renderer resolves `image_id` to responsive variant URLs, falling back to legacy `image_url`/`image_src` strings. Alt text backfill mix task. 1398 tests.
See: [docs/plans/media-library.md](docs/plans/media-library.md) for full plan
### Page Editor
**Status:** Complete — all stages done, 1516 tests
Database-driven page builder. Every page is a flat list of blocks stored as JSON — add, remove, reorder, and edit blocks on any page. One generic renderer for all pages (no page-specific render functions). Portable blocks (hero, featured_products, image_text, etc.) work on any page. Page-specific blocks (product_hero, cart_items, etc.) are restricted to their native page. Block data loaders dynamically load data based on which blocks are on the page. ETS-cached page definitions. Mobile-first admin editor with live preview, undo/redo, accessible reordering (no drag-and-drop), inline settings forms, and "reset to defaults". CSS-driven page layout (not renderer-driven). Custom CMS pages with 4 utility block types (spacer, divider, button/CTA, video embed), page templates (blank/content/landing) for new page creation, duplicate page action, and mobile sidebar fix for the block picker.
**Stages:**
1. ~~Foundation — data model, cache, block registry~~ ✅ (`35f96e4`)
A beautiful, customisable e-commerce storefront built with Phoenix LiveView. Designed for print-on-demand sellers who want professional shops without design expertise.
A customisable e-commerce storefront for print-on-demand sellers, built with Phoenix LiveView. Professional shops without design expertise, privacy-respecting by default, fully self-hostable.
## Features
### The Shop
A complete storefront with all the pages you need:
- **Home** - Hero banner, category navigation, featured products, testimonials
- **Products** - Grid layout with hover effects and filtering
- **Product Detail** - Image gallery, variants, reviews, related products
- **Cart** - Full shopping cart with order summary
- **Checkout** - Stripe-hosted checkout with order confirmation
- **About** - Rich content with your brand story
- **Contact** - Contact form with business details
- **Error pages** - Themed 404/500 pages
### Shop
Complete storefront with all the pages you need:
- **Home** — hero banner, category navigation, featured products, newsletter
- **Products** — grid layout with hover effects, sorting, filtering by collection
- **Product detail** — image gallery with per-colour filtering, variant selector, related products
> Vision and future features. For current status, see [PROGRESS.md](PROGRESS.md).
> Forward-looking vision and planned features. For current status, see [PROGRESS.md](PROGRESS.md).
## What's done
Tiers 1-3 are complete. The shop handles real orders end-to-end: browse products, add to cart, Stripe Checkout, order submission to Printify/Printful, fulfilment tracking, transactional emails. Full admin with theme editor, page builder, analytics, media library, activity log, URL redirects, and dead link monitoring. Tailwind-free CSS, 99-100 PageSpeed, 1679+ tests.
See [PROGRESS.md](PROGRESS.md) for the full list.
---
## Core MVP: Cart & Checkout ✅
## Profit-aware pricing & sales
Session-based cart, Stripe-hosted Checkout, order persistence, and webhook handling are all complete. See [PROGRESS.md](PROGRESS.md) for details.
Complete cost visibility for shop owners. [Plan](docs/plans/profit-aware-pricing.md).
### Orders & Fulfillment (next up)
- Submit paid orders to Printify for fulfillment
- Track fulfillment status updates via webhook
- Display order status to customers
- Fix Printful cost sync (catalog API cross-reference for variant costs)
- Snapshot `unit_cost` on order items at time of sale
- Exact Stripe fees from Balance Transaction API (not estimated)
- Tax as a toggle: Stripe Tax at checkout, "inc. VAT" / "+ tax" display based on shop country
- Admin profit dashboard: per-product margins, per-order P&L, overall business health
- Price editor shows live margin as you set prices
- Sales & promotions: time-limited, scoped to catalogue/category/products, % or fixed
- Margin guard: prevent discounts that breach minimum profit threshold
- Announcement bar for active sales
### Cost Verification at Checkout
Verify Printify costs haven't changed before completing checkout to prevent selling at a loss.
## Production hardening
- **Litestream / SQLite replication** — continuous backup to S3-compatible storage with point-in-time recovery. For the hosted platform, evaluate Turso/libSQL with embedded read replicas
- **End-to-end & accessibility tests** — Wallaby browser tests for critical flows with A11yAudit (axe-core) assertions. WCAG 2.1 AA compliance
- **AGPL licensing & code hosting** — proper LICENSE file, contribution guidelines, release process. Decide on GitHub vs Codeberg vs self-hosted Forgejo
## Newsletter & email marketing
Email list collection (signup forms on shop pages). Campaign sending for product launches and sales. Simple to start: collect emails, send via Swoosh. Ties into sales & promotions for sale announcement blasts.
- Cost change monitoring/alerts (warn if provider costs increased)
- Better image gallery (zoom, multiple angles)
- Product reviews system
## Platform vision
### Marketing site
Berrypod.com as the public face: brochure pages, pricing, comparison with Shopify/Squarespace, sign-up flow, demo store. Clear separation between the commercial platform site and the AGPL open source core.
### Hosted platform infrastructure
Multi-tenancy with per-tenant databases. OAuth connect flows for providers (Printify, Printful) and payments (Stripe Connect). The setup UI already supports this: check for OAuth credentials and show the connect button, fall back to API key form on self-hosted.
### Migration & export
Let shop owners export their data (products, orders, theme settings). Import from Shopify, WooCommerce. Portable data as a selling point for the self-hosted story.
### Internationalisation
Multi-language via Gettext, currency formatting, RTL layout support. Note: `ex_money`/`ex_cldr` currently only used for GBP formatting but add ~13 MB to the release. Consider replacing with a lightweight `format_price/2` until proper locale-aware formatting is needed.
---
## Medium Features
## Design philosophy
### Page Builder
Database-driven pages with drag-and-drop sections:
- Hero, Featured Products, Testimonials, Newsletter
The current first-run experience is disjointed: `/users/register` is a standalone page that closes after one user, the login page still links to it, and `/admin/setup` is a 3-step wizard hardcoded to Printify that bundles "go live" into the initial setup. Research into Shopify, WooCommerce and Squarespace shows that every successful platform separates initial setup (get the plumbing working) from launch readiness (guide the owner to a shop worth opening).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.