update docs and progress tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-03-13 13:34:36 +00:00
parent f2e9960303
commit 255912af73
5 changed files with 818 additions and 1 deletions

11
.envrc.example Normal file
View File

@ -0,0 +1,11 @@
# direnv configuration for Berrypod development
# Copy to .envrc and run `direnv allow` to activate
# SQLCipher build flags (required after installing libsqlcipher-dev)
# These tell exqlite to use system SQLCipher instead of bundled SQLite
export EXQLITE_USE_SYSTEM=1
export EXQLITE_SYSTEM_CFLAGS="-I/usr/include/sqlcipher"
export EXQLITE_SYSTEM_LDFLAGS="-lsqlcipher"
# Optional: enable database encryption in dev (omit for unencrypted)
# export SECRET_KEY_DB="dev-only-key-not-for-production"

View File

@ -103,6 +103,52 @@ Extend the existing page editor (PageEditorHook + editor_sheet) to include theme
|---|---|---|---| |---|---|---|---|
| 1 | Fix double radio button dots in theme editor | 30m | done | | 1 | Fix double radio button dots in theme editor | 30m | done |
### Competitive gaps ([plan](docs/plans/competitive-gaps.md))
Close critical gaps identified in the competitive analysis. Phased approach: core commerce first, then retention/growth, then scale.
**Phase 1: Core commerce gaps**
| # | Task | Depends on | Est | Status |
|---|------|------------|-----|--------|
| 75 | Customer authentication schema | — | 2h | planned |
| 76 | Customer auth flows (login, register, reset) | 75 | 3h | planned |
| 77 | Link orders to customers | 75, 76 | 1.5h | planned |
| 78 | Customer account dashboard | 76 | 2h | planned |
| 79 | Saved addresses | 76 | 1.5h | planned |
| 80 | Guest checkout linking | 75, 76 | 1h | planned |
| 81 | PayPal SDK integration | — | 2h | planned |
| 82 | PayPal checkout flow | 81 | 3h | planned |
| 83 | PayPal webhooks | 82 | 1.5h | planned |
| 84 | Reviews schema | — | 1.5h | planned |
| 85 | Review submission | 84 | 2h | planned |
| 86 | Review moderation | 84 | 1.5h | planned |
| 87 | Reviews display | 84 | 1.5h | planned |
| 88 | Review schema markup | 87 | 1h | planned |
| 89 | Provider stock sync | — | — | done (already existed) |
| 90 | Availability display | 89 | 1h | done |
**Phase 2: Retention & growth**
| # | Task | Depends on | Est | Status |
|---|------|------------|-----|--------|
| 91 | Returns schema | — | 1.5h | planned |
| 92 | Return request flow | 91, 78 | 2h | planned |
| 93 | Return admin | 91 | 2h | planned |
| 94 | Return policy settings | 91 | 1h | planned |
| 95 | Email sequence schema | — | 2h | planned |
| 96 | Sequence triggers & sending | 95 | 3h | planned |
| 97 | Sequence admin | 95 | 2h | planned |
| 98 | Customer data export (GDPR) | 75 | 1.5h | planned |
| 99 | Customer data deletion (GDPR) | 75 | 2h | planned |
**Phase 3: Scale**
| # | Task | Depends on | Est | Status |
|---|------|------------|-----|--------|
| 100 | Blog post type | — | 3h | planned |
| 101 | Staff accounts & RBAC | — | 4h | planned |
### Editor panel reorganisation ([plan](docs/plans/editor-reorganisation.md)) ### Editor panel reorganisation ([plan](docs/plans/editor-reorganisation.md))
Restructure the 3-tab editor panel for better discoverability. Replace Settings tab with Site tab for site-wide content (announcement bar text, social links, nav items, footer content). Move branding from Theme to Site. Merge page settings inline into Page tab. Restructure the 3-tab editor panel for better discoverability. Replace Settings tab with Site tab for site-wide content (announcement bar text, social links, nav items, footer content). Move branding from Theme to Site. Merge page settings inline into Page tab.
@ -195,3 +241,4 @@ All plans in [docs/plans/](docs/plans/). Completed plans are kept as architectur
| [editor-reorganisation.md](docs/plans/editor-reorganisation.md) | Planned | | [editor-reorganisation.md](docs/plans/editor-reorganisation.md) | Planned |
| [seo-enhancements.md](docs/plans/seo-enhancements.md) | Planned | | [seo-enhancements.md](docs/plans/seo-enhancements.md) | Planned |
| [competitive-gap-analysis.md](docs/plans/competitive-gap-analysis.md) | Reference | | [competitive-gap-analysis.md](docs/plans/competitive-gap-analysis.md) | Reference |
| [competitive-gaps.md](docs/plans/competitive-gaps.md) | Planned |

View File

@ -30,6 +30,7 @@ Complete storefront with all the pages you need:
### Technical highlights ### Technical highlights
- Hand-written CSS with three-layer architecture (9.8 KB gzipped shop, 17.8 KB admin) - Hand-written CSS with three-layer architecture (9.8 KB gzipped shop, 17.8 KB admin)
- SQLite with BLOB storage, IMMEDIATE transactions, WAL, mmap - SQLite with BLOB storage, IMMEDIATE transactions, WAL, mmap
- SQLCipher encryption at rest (AES-256, optional for dev, required for prod)
- Image optimisation pipeline (AVIF/WebP/JPEG responsive variants via Oban) - Image optimisation pipeline (AVIF/WebP/JPEG responsive variants via Oban)
- ETS caching for CSS, pages, redirects, favicons - ETS caching for CSS, pages, redirects, favicons
- 99-100 PageSpeed mobile, no-JS support across all key flows - 99-100 PageSpeed mobile, no-JS support across all key flows
@ -102,6 +103,59 @@ assets/css/
└── theme-layer3-semantic.css # component styles └── theme-layer3-semantic.css # component styles
``` ```
## Database encryption
Berrypod uses SQLCipher to encrypt the entire SQLite database at rest. Two independent secrets provide defence in depth:
| Secret | Purpose |
|--------|---------|
| `SECRET_KEY_BASE` | Phoenix sessions, Cloak field encryption |
| `SECRET_KEY_DB` | SQLCipher whole-database encryption |
### Development
Encryption is optional for development. To test locally with encryption:
```bash
# Generate a key (hex-only recommended)
openssl rand -hex 32
# Set environment variable
export SECRET_KEY_DB="your-hex-key"
# Recreate database with encryption
mix ecto.reset
mix phx.server
```
Without `SECRET_KEY_DB`, the database is unencrypted.
### Production
Both secrets are required. Generate them:
```bash
mix phx.gen.secret # → SECRET_KEY_BASE
openssl rand -hex 32 # → SECRET_KEY_DB (or mix phx.gen.secret)
```
For Fly.io deployment:
```bash
fly secrets set SECRET_KEY_BASE="..." SECRET_KEY_DB="..."
```
### Backup and restore
Admin > Backup provides:
- Database stats (size, encryption status, table breakdown)
- Download backup (encrypted with same key)
- Restore from backup (validates key matches)
**Key management:**
- Lost key = lost data. No recovery possible.
- Store keys securely (password manager, secrets manager).
- Backups are portable — copy file + set same key = working shop.
## Stripe setup ## Stripe setup
1. Create a [Stripe account](https://dashboard.stripe.com/register) 1. Create a [Stripe account](https://dashboard.stripe.com/register)

View File

@ -1,6 +1,6 @@
# Competitive Gap Analysis # Competitive Gap Analysis
Status: Reference Status: Reference (implementation plan: [competitive-gaps.md](competitive-gaps.md))
## Overview ## Overview

View File

@ -0,0 +1,705 @@
# Competitive gaps
> Status: Planned
> Tasks: #75101 in PROGRESS.md
> Tier: 4.5 (Core commerce gaps)
## Goal
Close the critical gaps identified in the competitive analysis that block Berrypod from competing with Shopify, Squarespace, and established POD platforms.
## Phasing
The gaps are grouped into three phases, ordered by impact:
1. **Phase 1 (Core commerce)** — unblocks repeat customers and essential e-commerce features
2. **Phase 2 (Retention & growth)** — keeps customers coming back and scales operations
3. **Phase 3 (Scale)** — differentiators and advanced features
Items already planned elsewhere (profit-aware-pricing, SEO enhancements) are not duplicated here.
---
## Phase 1: Core commerce gaps
### Customer accounts (#7580)
The biggest unlock. Returning customers can't see past orders, save addresses, or leave reviews without accounts.
#### #75 — Customer authentication schema (2h)
New table and auth infrastructure for customer accounts (separate from admin users).
**Schema:**
```elixir
create table(:customers, primary_key: false) do
add :id, :binary_id, primary_key: true
add :email, :citext, null: false
add :hashed_password, :string, null: false
add :confirmed_at, :utc_datetime
add :name, :string
timestamps()
end
create unique_index(:customers, [:email])
```
**Files:**
- Migration
- `lib/berrypod/customers/customer.ex` — schema + registration/password changesets
- `lib/berrypod/customers.ex` — context (register, authenticate, get_by_email)
**Tests:**
- Registration with valid/invalid data
- Authentication with correct/wrong password
- Email uniqueness
---
#### #76 — Customer auth flows (3h)
Login, register, password reset, email confirmation for shop customers.
**Routes:**
- `POST /account/register` — create account
- `POST /account/login` — log in
- `DELETE /account/logout` — log out
- `GET /account/confirm/:token` — email confirmation
- `POST /account/reset-password` — request reset
- `PUT /account/reset-password/:token` — set new password
**Sessions:**
- Customer session separate from admin session (different cookie)
- `@current_customer` assign in shop live_session
**UI:**
- Register/login forms (modal or page TBD)
- Email templates for confirmation and reset
- "Already have an account?" / "Create account" links
**Files:**
- `lib/berrypod_web/controllers/customer_session_controller.ex`
- `lib/berrypod_web/controllers/customer_registration_controller.ex`
- `lib/berrypod_web/live/shop/customer_auth_live.ex` — optional LiveView forms
- Email templates
- Router updates
**No-JS:** Forms work as standard POST.
---
#### #77 — Link orders to customers (1.5h)
Associate orders with customer accounts.
**Migration:**
```elixir
alter table(:orders) do
add :customer_id, references(:customers, type: :binary_id, on_delete: :nilify_all)
end
create index(:orders, [:customer_id])
```
**Flow:**
- At checkout, if `@current_customer` exists, set `customer_id` on order
- Guest checkout still works (customer_id = nil)
- "Create account?" prompt after guest checkout (pre-fill email from order)
**Files:**
- Migration
- `lib/berrypod/orders/order.ex` — add field
- Checkout controller — set customer_id
- Post-checkout prompt component
---
#### #78 — Customer account dashboard (2h)
`/account` page for logged-in customers.
**Features:**
- Order history list (status, date, total, tracking link)
- Click into order detail (items, addresses, timeline)
- Account settings (name, email, password)
**Files:**
- `lib/berrypod_web/live/shop/account_live.ex`
- `lib/berrypod_web/live/shop/account_orders_live.ex`
- Router — `/account`, `/account/orders/:id`
---
#### #79 — Saved addresses (1.5h)
Address book for returning customers.
**Schema:**
```elixir
create table(:customer_addresses, primary_key: false) do
add :id, :binary_id, primary_key: true
add :customer_id, references(:customers, type: :binary_id, on_delete: :delete_all), null: false
add :label, :string # "Home", "Work", etc.
add :name, :string, null: false
add :line1, :string, null: false
add :line2, :string
add :city, :string, null: false
add :state, :string
add :postal_code, :string, null: false
add :country, :string, null: false # ISO code
add :is_default, :boolean, default: false
timestamps()
end
```
**Features:**
- Add/edit/delete addresses in account settings
- Address selector at checkout (for logged-in customers)
- "Save this address" checkbox at checkout
- Prefill Stripe Checkout with selected address
**Files:**
- Migration + schema
- `lib/berrypod/customers.ex` — address CRUD
- Account settings component
- Checkout integration
---
#### #80 — Guest checkout linking (1h)
After guest checkout, prompt to create account and link the order.
**Flow:**
1. Order confirmation page shows "Create account to track orders"
2. Email pre-filled from order
3. On registration, link order to new customer
**Files:**
- Order confirmation component
- Registration flow — accept order_id param, link on create
---
### PayPal integration (#8183)
~30% of buyers expect PayPal. Critical for conversions.
#### #81 — PayPal SDK integration (2h)
Set up PayPal JavaScript SDK and server-side API.
**Dependencies:**
- PayPal REST API (use `:req` for HTTP)
- Client ID + Secret in encrypted settings
**Files:**
- `lib/berrypod/payments/paypal.ex` — API client (create order, capture payment)
- Settings — PayPal credentials
- Admin settings UI — PayPal section
---
#### #82 — PayPal checkout flow (3h)
Add PayPal as payment option alongside Stripe.
**Flow:**
1. Cart page shows "Pay with PayPal" button (PayPal JS SDK)
2. Customer clicks → PayPal modal opens
3. Customer approves → returns to site
4. Server captures payment
5. Order created/confirmed
**Approach:**
- PayPal handles the entire payment UI (like Stripe Checkout)
- Server creates PayPal order, client approves, server captures
- Same order creation flow as Stripe, different payment capture
**Files:**
- `lib/berrypod_web/controllers/paypal_controller.ex` — create order, capture
- Cart/checkout UI — PayPal button
- JS — PayPal SDK integration
- Order creation — handle PayPal payments
---
#### #83 — PayPal webhooks (1.5h)
Handle PayPal webhooks for refunds and disputes.
**Events:**
- `PAYMENT.CAPTURE.REFUNDED` — mark order refunded
- `CUSTOMER.DISPUTE.CREATED` — flag order
**Files:**
- `lib/berrypod_web/controllers/paypal_webhook_controller.ex`
- Router — `/webhooks/paypal`
- Orders context — refund/dispute handling
---
### Product reviews (#8488)
Reviews block exists but has no backend. Critical for social proof.
#### #84 — Reviews schema (1.5h)
**Schema:**
```elixir
create table(:reviews, primary_key: false) do
add :id, :binary_id, primary_key: true
add :product_id, references(:products, type: :binary_id, on_delete: :delete_all), null: false
add :customer_id, references(:customers, type: :binary_id, on_delete: :nilify_all)
add :order_id, references(:orders, type: :binary_id, on_delete: :nilify_all)
add :rating, :integer, null: false # 1-5
add :title, :string
add :body, :text
add :status, :string, default: "pending" # pending, approved, rejected
add :verified_purchase, :boolean, default: false
add :author_name, :string # for guest reviews or if customer deleted
timestamps()
end
create index(:reviews, [:product_id])
create index(:reviews, [:customer_id])
create index(:reviews, [:status])
```
**Files:**
- Migration
- `lib/berrypod/reviews/review.ex`
- `lib/berrypod/reviews.ex` — context
---
#### #85 — Review submission (2h)
Customers can leave reviews on purchased products.
**Routes:**
- `POST /products/:id/reviews` — submit review
**Rules:**
- Must have purchased the product (order with this product, status paid+)
- One review per customer per product
- Optional: allow guest reviews with email verification
**UI:**
- Review form on product page (only for purchasers)
- Star rating selector
- Optional title and body
**Files:**
- `lib/berrypod_web/live/shop/product_live.ex` — review form component
- Review creation with validation
---
#### #86 — Review moderation (1.5h)
Admin can approve/reject reviews before display.
**Admin UI:**
- `/admin/reviews` — pending reviews queue
- Approve/reject buttons
- Bulk actions
**Moderation:**
- `status: pending` by default
- Only `approved` reviews shown on product pages
- Email notification to admin on new review (optional)
**Files:**
- `lib/berrypod_web/live/admin/reviews_live.ex`
- Router
- Admin nav
---
#### #87 — Reviews display (1.5h)
Show approved reviews on product pages.
**Features:**
- Average rating + count in product header
- Review list below product details
- "Verified purchase" badge
- Sort by date/rating
- Pagination
**Files:**
- Product page — reviews section
- `lib/berrypod/reviews.ex` — queries (avg rating, list for product)
- Shop components — review card, star display
---
#### #88 — Review schema markup (1h)
Add JSON-LD `Review` and `AggregateRating` schema.
**Schema:**
```json
{
"@type": "Product",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"reviewCount": "12"
},
"review": [...]
}
```
**Files:**
- Product page — extend existing JSON-LD
- `lib/berrypod_web/components/seo_components.ex`
---
### Stock status improvements (#8990) — Complete
Accurate availability status from providers. No artificial urgency — POD products are made to order. Stock limits reflect provider availability, not scarcity marketing.
#### #89 — Provider stock sync — Already exists
The `is_available` field already exists on `ProductVariant` and is synced from both providers:
- Printify: `var["is_available"]`
- Printful: `sv["availability_status"] == "active"`
Product-level `in_stock` is computed from variant availability in `Products.recompute_cached_fields/1`.
#### #90 — Availability display — Complete
**Implemented:**
- Unavailable variants are visually disabled in the variant selector (opacity 0.3, dashed border)
- "This option is currently unavailable" message shown when an unavailable variant is selected
- Add-to-cart button disabled for unavailable variants
- Cart shows "This item is currently unavailable" warning for affected items
- Checkout blocked if cart contains unavailable items
**No "X left" urgency** — that's a dark pattern for made-to-order products.
---
---
## Phase 2: Retention & growth
### Returns management (#9194)
No RMA system currently. Customers can't request returns.
#### #91 — Returns schema (1.5h)
**Schema:**
```elixir
create table(:return_requests, primary_key: false) do
add :id, :binary_id, primary_key: true
add :order_id, references(:orders, type: :binary_id, on_delete: :delete_all), null: false
add :customer_id, references(:customers, type: :binary_id, on_delete: :nilify_all)
add :status, :string, default: "requested" # requested, approved, rejected, completed
add :reason, :string, null: false
add :notes, :text
add :refund_amount, :integer # approved refund amount
timestamps()
end
create table(:return_request_items, primary_key: false) do
add :id, :binary_id, primary_key: true
add :return_request_id, references(:return_requests, type: :binary_id, on_delete: :delete_all)
add :order_item_id, references(:order_items, type: :binary_id, on_delete: :delete_all)
add :quantity, :integer, null: false
end
```
**Files:**
- Migration
- Schemas
- Context
---
#### #92 — Return request flow (2h)
Customers can request returns from their account.
**Flow:**
1. Customer goes to order detail in account
2. Clicks "Request return" (within return window)
3. Selects items and reason
4. Submits request
5. Email sent to admin
**Files:**
- Account orders — return request button/form
- `lib/berrypod/returns.ex` — create request
- Email notification
---
#### #93 — Return admin (2h)
Admin can process return requests.
**Admin UI:**
- `/admin/returns` — list of requests (pending, processed)
- Approve/reject with refund amount
- Status tracking
**Actions:**
- Approve → triggers refund (Stripe or PayPal)
- Reject → email customer with reason
**Files:**
- `lib/berrypod_web/live/admin/returns_live.ex`
- Stripe/PayPal refund integration
- Activity log entries
---
#### #94 — Return policy settings (1h)
Configure return window and reasons.
**Settings:**
- `return_window_days` — default 30
- `return_reasons` — list of selectable reasons
**Files:**
- Settings
- Admin settings UI
- Return form uses configured reasons
---
### Email sequences (#9597)
Only single campaigns exist. Need automated flows.
#### #95 — Email sequence schema (2h)
**Schema:**
```elixir
create table(:email_sequences, primary_key: false) do
add :id, :binary_id, primary_key: true
add :name, :string, null: false
add :trigger, :string, null: false # "welcome", "post_purchase", "browse_abandon"
add :active, :boolean, default: true
timestamps()
end
create table(:email_sequence_steps, primary_key: false) do
add :id, :binary_id, primary_key: true
add :sequence_id, references(:email_sequences, type: :binary_id, on_delete: :delete_all)
add :position, :integer, null: false
add :delay_hours, :integer, null: false # hours after trigger/previous step
add :subject, :string, null: false
add :body, :text, null: false
timestamps()
end
create table(:email_sequence_sends, primary_key: false) do
add :id, :binary_id, primary_key: true
add :step_id, references(:email_sequence_steps, type: :binary_id, on_delete: :delete_all)
add :customer_id, references(:customers, type: :binary_id, on_delete: :delete_all)
add :email, :string, null: false
add :sent_at, :utc_datetime
add :opened_at, :utc_datetime
add :clicked_at, :utc_datetime
end
```
**Files:**
- Migration
- Schemas
- Context
---
#### #96 — Sequence triggers & sending (3h)
Oban jobs to trigger and send sequence emails.
**Triggers:**
- `welcome` — on customer registration
- `post_purchase` — on order confirmation
- `browse_abandon` — on product view without purchase (24h later)
**Flow:**
1. Trigger event → enqueue first step job with delay
2. Job runs → send email → enqueue next step
3. Track opens/clicks (pixel + link tracking)
**Files:**
- `lib/berrypod/workers/email_sequence_worker.ex`
- Trigger hooks in registration/order/analytics
- Email sending integration
---
#### #97 — Sequence admin (2h)
Admin can create and manage sequences.
**Admin UI:**
- `/admin/sequences` — list of sequences
- Create/edit sequence with steps
- Preview emails
- Analytics (sent, opened, clicked)
**Files:**
- `lib/berrypod_web/live/admin/sequences_live.ex`
- Router
- Admin nav
---
### GDPR data export/deletion (#9899)
Right to erasure not fully implemented.
#### #98 — Customer data export (1.5h)
Customers can export their data.
**Export includes:**
- Account info (email, name)
- Orders (with items, addresses)
- Reviews
- Newsletter subscription status
**Format:** JSON download
**Files:**
- Account settings — "Download my data" button
- `lib/berrypod/customers.ex` — export function
- Controller endpoint
---
#### #99 — Customer data deletion (2h)
Customers can delete their account.
**Process:**
1. Customer requests deletion
2. Confirmation email sent
3. After confirmation, anonymise data:
- Remove name, email (replace with "deleted-{id}")
- Keep order records for accounting (anonymised)
- Delete reviews or anonymise
- Remove from newsletter
**Files:**
- Account settings — "Delete my account"
- Deletion flow with confirmation
- Anonymisation logic
---
---
## Phase 3: Scale
### Blog functionality (#100)
Page builder exists but no blog post type.
#### #100 — Blog post type (3h)
Extend page builder for blog posts.
**New fields on pages:**
```elixir
add :page_type, :string, default: "page" # "page" or "post"
add :published_at, :utc_datetime
add :author, :string
add :category, :string
add :excerpt, :text
```
**Features:**
- `/blog` index page (posts sorted by date)
- `/blog/:slug` individual posts
- Category filtering
- RSS feed
- Previous/next navigation
**Files:**
- Migration
- Page schema updates
- Blog index LiveView
- RSS controller
- Admin pages — post type option
---
### Staff accounts (#101)
Single admin only. Need team access.
#### #101 — Staff accounts & RBAC (4h)
Multiple staff with role-based permissions.
**Schema:**
```elixir
alter table(:users) do
add :role, :string, default: "owner" # owner, admin, staff
add :permissions, :map, default: %{}
end
```
**Permissions:**
- `orders` — view/manage orders
- `products` — view/manage products
- `pages` — edit pages
- `settings` — access settings
- `analytics` — view analytics
**Admin UI:**
- `/admin/team` — list staff
- Invite staff (email with setup link)
- Assign role/permissions
- Activity log shows who did what
**Files:**
- Migration
- User schema update
- Permission checks in LiveViews
- Team management LiveView
- Invite flow
---
## Dependencies
| Task | Depends on |
|------|------------|
| #77 (Link orders to customers) | #75, #76 |
| #78 (Account dashboard) | #76 |
| #79 (Saved addresses) | #76 |
| #80 (Guest checkout linking) | #75, #76 |
| #85 (Review submission) | #84, #75 (optional) |
| #92 (Return request flow) | #91, #78 |
| #96 (Sequence triggers) | #95 |
| #98, #99 (GDPR) | #75 |
---
## Already covered elsewhere
These gaps are addressed in other plans:
| Gap | Covered in |
|-----|-----------|
| Discount/coupon codes | profit-aware-pricing.md (#69) |
| Tax management | profit-aware-pricing.md (#66) |
| Profit dashboard | profit-aware-pricing.md (#67) |
| Margin warnings | profit-aware-pricing.md (#68, #70) |
| Announcement bar | profit-aware-pricing.md (#71) |
| SEO enhancements | seo-enhancements.md |
| Multiple print providers | ROADMAP.md |
| Buy-now-pay-later (Klarna) | Future (dependent on demand) |