All projects
CupidFaces Live

Ethliam

Quiet luxury e-commerce store for Nigeria: curated fashion, electronics, fragrance, and home goods with a full admin panel and Paystack checkout

Fashion, Electronics, Fragrance, Home
Product categories
Cloudflare R2 (zero egress)
Image storage
Paystack (NGN)
Payments
Lagos, ships Nigeria-wide
Market
E-commerceNext.jsSupabaseNigeriaRetailAdmin Panel

Overview

Ethliam is a quiet luxury e-commerce store based in Lagos, shipping across Nigeria. The store's curation spans fashion, electronics, fragrance, and home goods, with every product personally reviewed before listing. The brand positioning is deliberate: this is for people who want fewer, better things rather than volume. Pricing is in Naira with no hidden conversion.

The platform is a full Next.js application backed by Supabase for the database, Cloudflare R2 for product image storage, and Paystack for Nigerian payment processing. It includes a customer-facing store with product pages, category browsing, a shopping bag, and checkout, plus an admin panel for product management, order tracking, and inventory.

Alongside the shop, the site has a Journal section for editorial content, a customer account area, and brand pages (Our Story, Returns, Shipping, Privacy).

My Role

  • Designed and built the full platform from scratch: data model, storefront, checkout flow, admin panel, and editorial system
  • Built the product catalogue with category filtering, featured product slots, individual product pages with image galleries, and stock tracking
  • Built the shopping bag as client-side state synced to Supabase for logged-in users, with guest bag support
  • Integrated Paystack for Naira payments: order creation, payment initialisation, and webhook-based confirmation to update order status
  • Built the admin panel: product creation and editing with R2 image upload, order management, inventory control, and published/draft states
  • Built the Journal: editorial posts with rich content, cover images via R2, and individual post pages
  • Set up Cloudflare R2 for product and journal image storage with a server-side upload API route

Architecture

Next.js App Router with Supabase for PostgreSQL and auth. Product images and journal covers are stored in Cloudflare R2 and served via public R2 URLs. The admin panel is a protected route segment gated by the is_admin flag on the users table. Supabase Row Level Security handles data access policies at the database level.

Orders flow through Paystack: the checkout page creates a pending order in the database, initiates a Paystack transaction, and redirects the customer to Paystack's hosted payment page. On successful payment, a Paystack webhook hits the confirmation route which verifies the transaction reference and marks the order as paid. The order items and shipping address are stored with the order record.

The bag is managed in Supabase for logged-in users (cart_items table scoped by user_id) so it persists across devices. Product prices are stored in kobo (the smallest Naira unit) to avoid floating point precision issues on payment amounts.

Key Decisions

Cloudflare R2 for image storage instead of Supabase Storage
Supabase Storage works fine for small projects but egress costs at scale add up. R2 has zero egress fees on data served to end users, which matters for an image-heavy product catalogue. The trade-off is a slightly more complex upload path: a server-side API route handles the upload to R2 rather than going directly from the client to Supabase Storage.
Prices stored in kobo to avoid floating point issues
Storing prices as floats (e.g. 25000.50 NGN) introduces rounding errors in payment calculations. Storing in kobo as an integer (e.g. 2500050) means all arithmetic is exact. The display layer divides by 100 for presentation. Paystack also accepts amounts in kobo, so there is no conversion needed when initiating a payment.
Webhook-based order confirmation rather than redirect-based
Relying on the redirect back from Paystack's payment page to confirm an order is fragile: the customer might close the tab before the redirect completes. Using Paystack webhooks means the order is confirmed by Paystack's server-to-server call, which happens regardless of what the customer's browser does after payment.
Quiet luxury brand positioning as a product constraint
The brand brief was not just a design direction but a curation policy. Every product decision on what to list, how to describe it, and how to present it runs through the same filter: does this earn its place? That constraint keeps the catalogue tight and intentional rather than growing into a generic everything-store over time.

Challenges

Keeping the bag consistent for both guest and logged-in users
A logged-in user's bag lives in Supabase and persists across devices. A guest's bag lives in client-side state. The tricky part is the transition: when a guest logs in, their local bag needs to be merged with whatever is already in their Supabase cart. The merge strategy is to add the guest items to the existing cart, incrementing quantities where the same product already exists, and then clear the local state.
Admin panel security on a public-facing Next.js app
The admin panel is a route segment within the same Next.js application as the public store. Keeping it secure means checking the is_admin flag on every admin route, not just at the layout level. A layout-level check can be bypassed by direct route access if the per-route check is missing. Every admin API route checks the session and the admin flag independently.

Stack & Integrations

Next.js 16React 19 + TypeScriptSupabase + RLSCloudflare R2PaystackTailwind CSSVercel