Projects

05 / 05Case study

Delicaté 4.0

E-commerce and brand site

Handmade soaps from the Dominican Republic, live on their own domain: a Django-managed catalog, an account-free cart that stays up to date and orders that close over WhatsApp.

Delicaté home page with the headline “Cuidado hecho con amor” (Care made with love) and handmade soaps with coconut, orange and oatsCart on a phone with three products and the green Finish on WhatsApp button
My role
Requirements gathering, UX/UI, full-stack development and deployment
Stack
React 19 · Vite 8 · Django 5.2 · Django REST Framework · PostgreSQL · Docker · Railway
Status
Live on its own domain · commercial launch in validation

Scope

  • 18automated tests, run in CI on every push
  • 0accounts needed to buy
  • 68 KBof JavaScript in production, gzipped

The challenge

The business needed to present its catalog with an identity of its own and receive well-formed orders, without taking on online payments or a logistics operation it doesn't have.

What I built
I redesigned and rebuilt the whole store — a Django REST catalog API, inventory management and a responsive React storefront — and took it to production on Railway with its own domain, continuous integration and 18 tests.
Key technical decision
I removed customer sign-up, the server-side cart and the checkout from the previous version, and turned the cart into a structured WhatsApp order: the same channel where the business already confirms, collects and delivers.

Design decisions

  1. Delicaté home page with the headline “Cuidado hecho con amor” (Care made with love) and handmade soaps with coconut, orange and oats
    Problem
    The price of a handmade soap is carried by the brand, not by a product list.
    Decision
    The home page presents the brand before the store, with a single main action.
  2. Cacao & Almendras product view with price, stock, benefit, skin type and ingredients
    Problem
    Before buying, customers ask about benefits, skin type and ingredients.
    Decision
    The product view answers in a native dialog, without leaving the catalog.
  3. Cart with Avena Calma, Corazón de Lavanda and Café Despierto and an estimated total of RD$1,575
    Problem
    A checkout that can't take payment is a broken promise.
    Decision
    The cart builds the order, makes clear it doesn't charge and closes it on WhatsApp.
  4. Cart with a price and availability update notice and Avena Calma capped at 12 units
    Problem
    A cart saved days ago can send old prices or quantities that are out of stock.
    Decision
    When you come back, the cart updates against the real catalog and says what changed.
  5. Arcilla & Avena card on mobile with the Botanicals filter active and the Add button visible
    Problem
    On a touch screen there's no hover to reveal the buy button.
    Decision
    On touch, “Add” is always visible and the filters scroll in a single row.
  6. Catalog filtered by Gifts with Corazón de Lavanda and Flor de Ámbar marked as sold out
    Problem
    If a sold-out soap disappears, whoever was looking for it can't tell if it'll be back.
    Decision
    It stays visible as “Sold out” with buying disabled, on the card and in the product view.
  7. Catalog unavailable message with a Try again button
    Problem
    If the API fails, showing made-up products misleads the buyer.
    Decision
    Production shows an error with a retry; the demo catalog only exists in development.
  8. Django admin with ten products and editable price, stock, featured and active fields
    Problem
    The business had to change its catalog without touching code or rebuilding.
    Decision
    Price, stock, featured and visibility are edited right in the admin list.

System

Pick a module: its path lights up.Pick a module: its decision appears below.

Architecture18 modules · 19 connections

Brand before store: a single main action leads to the collection.

React 19 · Custom CSS

Read in full from the API, following every page, and filtered in the browser; filters declare their state with aria-pressed.

fetch with AbortController · React 19

A native dialog answers benefit, skin, ingredients and stock without leaving the catalog.

Native <dialog> · React 19

No account. On return it's checked against the catalog: it takes the current price, caps each quantity at the stock and says what changed.

useReducer · Custom useCart hook

Built for the phone: on touch, “Add” is always visible and the cart takes the whole screen.

@media (hover: none) · Custom CSS

The form stores nothing on the server: it turns the question into a WhatsApp message.

React 19

Public and read-only: a paginated, filterable list and a product view by slug. The catalog is only written from the admin.

Django REST Framework 3.17 · Pages of up to 100

Price, stock, featured and visibility are edited in the list itself; photos are validated (JPG, PNG or WebP, up to 5 MB).

Django Admin · Pillow 12.3

CSP and the other headers on every response, one-year HSTS and rate limits that count the real IP behind the proxy.

Custom middleware · DRF throttling

The cart lives in the browser; the server never stores orders or buyer data.

localStorage

Four models, down from eight in 2024: products, messages, subscriptions and the team user.

PostgreSQL · psycopg 3.3 · dj-database-url 3.1

Uploaded photos live on a persistent volume; Django won't start if its path is relative.

Railway volume · 7-day cache

On every push: Django check, pending migrations, the 18 tests, lint and build.

GitHub Actions

A multi-stage image: Node builds the store and Python serves it, with DEBUG off by default.

node:24-alpine · python:3.13-slim

Migrates before release and only sends traffic to the new version if the health check, which queries the database, returns 200.

Railway · railway.json

A single process serves the store, API, admin and photos from the same origin: no CORS.

Gunicorn 23 · WhiteNoise 6.12

The order goes out as a pre-written wa.me message, on the channel where the business already confirms, collects and delivers.

wa.me link

When the link is shared, a 1200×630 image and the canonical URL come from the build configuration.

Open Graph · Twitter Card

Inspector18 decisions

Client

Home page

Brand before store: a single main action leads to the collection.

Technologies

43 tools across 10 areas. In the system, each module lists its own.

Languages

  • Python 3.13
  • JavaScript (JSX)
  • CSS
  • HTML

Frontend

  • React 19.2
  • Vite 8.2
  • Rolldown 1.2
  • @vitejs/plugin-react 6.0

Interface

  • Custom CSS without a framework
  • Lightning CSS 1.33
  • System fonts
  • 8 inline SVG icons
  • Native <dialog>

Browser state

  • useReducer and a custom hook
  • localStorage
  • fetch with AbortController

Backend

  • Django 5.2 LTS
  • Django REST Framework 3.17
  • Django Admin
  • django-cors-headers 4.9
  • Pillow 12.3
  • python-dotenv 1.2

Data

  • PostgreSQL
  • SQLite
  • psycopg 3.3
  • dj-database-url 3.1

Server and deployment

  • Multi-stage Docker
  • Gunicorn 23
  • WhiteNoise 6.12
  • Railway
  • Persistent volume
  • Custom domain with TLS

Integrations

  • WhatsApp wa.me link
  • Open Graph and Twitter Card

Quality and CI

  • Django test runner
  • DRF APITestCase
  • ESLint 10.8
  • eslint-plugin-react-hooks 7.1
  • GitHub Actions

Tooling

  • Node.js 24
  • npm
  • Git
  • Railway CLI

Verifiable results

  • Live at delicate.jonasjavier.dev as a single Docker container on Railway, with PostgreSQL and a volume for photos
  • A catalog managed from Django, with no code changes or storefront rebuilds
  • An account-free cart saved in the browser that, when you come back, updates to real prices and stock
  • A complete order, with quantities and an estimated total, pre-written for WhatsApp; the site doesn't charge
  • Migrations before release, a database-backed health check, CSP and rate limits
  • 18 automated tests and a CI that checks migrations, lint and build on every push

Tour by module

30 screens across 7 modules.

Home and brand3 screens

  • Delicaté home page with the headline “Cuidado hecho con amor” (Care made with love) and handmade soaps with coconut, orange and oats

    The home page sells the brand before the product, with a single main action.

  • Our story section with a photo of soaps being cut by hand and the brand's text

    The brand story backs up the price of something handmade.

  • Green block with a line about the time, care and love that go into every bar

    An editorial pause reinforces the brand's tone.

Catalog and product6 screens

  • Delicaté catalog with category filters and three soaps marked as favorites

    The catalog loads from Django and filters instantly.

  • Catalog filtered by Botanicals with Arcilla & Avena, Café Despierto and Jardín Botánico

    Each card sums up price, skin type and weight before you open the product.

  • Cacao & Almendras card with the Add button over the photo on hover

    On desktop, “Add” appears when you hover over the photo.

  • Cacao & Almendras product view with price, stock, benefit, skin type and ingredients

    The product view covers benefit, skin, ingredients and stock without leaving the catalog.

  • Catalog filtered by Gifts with Corazón de Lavanda and Flor de Ámbar marked as sold out

    An out-of-stock product stays visible, but can't be added.

  • Flor de Ámbar product view with a Temporarily sold out label and a disabled button

    The product view says it's sold out and disables the purchase.

Cart and order3 screens

  • Cart with Avena Calma, Corazón de Lavanda and Café Despierto and an estimated total of RD$1,575

    The cart builds the order and makes clear the site doesn't charge.

  • Empty side cart with a message inviting you to pick something made with love

    The empty cart invites you back to the collection.

  • Cart with a price and availability update notice and Avena Calma capped at 12 units

    When you come back, the cart updates to today's prices and stock.

How ordering works3 screens

  • Ordering-is-easy section with three numbered steps

    Three steps explain how to order without a checkout.

  • Frequently asked questions with the first answer expanded

    The FAQ settles doubts before anyone writes on WhatsApp.

  • Contact form with name and question and a Send via WhatsApp button

    The form turns a question into a WhatsApp message.

Catalog states2 screens

  • Catalog unavailable message with a Try again button

    If the API fails, it offers a retry, never a made-up catalog.

  • Classics filter with no products, a message and a See all link

    An empty category offers a way back to every product.

Admin2 screens

  • Django admin with ten products and editable price, stock, featured and active fields

    Price, stock and visibility change right from the list.

  • Admin product list filtered by Botanicals with three results

    The admin filters by category, featured and status.

On the phone11 screens

  • Cart on a phone with three products and the green Finish on WhatsApp button

    The order goes out over WhatsApp from the same phone.

  • Cart on mobile with a notice that the order was updated to today's prices and availability

    The notice explains the change before the order is sent.

  • Arcilla & Avena card on mobile with the Botanicals filter active and the Add button visible

    On touch screens, “Add” is always visible and filters scroll sideways.

  • Cacao & Almendras product view on mobile with photo, price and benefits stacked

    On the phone the product view stacks and scrolls inside the dialog.

  • End of the product view on mobile with ingredients and the Add to cart button

    The purchase action closes the product view at full width.

  • Delicaté home page on a phone with the headline and the Discover the collection button

    The headline and the main action sit within thumb's reach.

  • Mobile drop-down menu with Collection, Our story, Process, FAQ and Let's talk

    The menu closes with Escape or when you pick a destination.

  • Our story section on mobile with the photo of soaps being cut

    The story reads top to bottom.

  • Steps to order from Delicaté stacked on a phone

    The three ordering steps, stacked.

  • Catalog unavailable message on mobile with a Try again button

    The error is just as clear, and just as fixable, on the phone.

  • Empty category message on mobile with a See all link

    The empty state is handled on the phone too.

8 min read

The full case

Context

Delicaté is a brand of handmade soaps made in small batches in the Dominican Republic, and I built the site for it. It sells through conversation: the customer asks, chooses and arranges delivery over WhatsApp. The site had to present the brand and the catalog with an experience of its own and bring the customer into that conversation with the order already put together.

The repository keeps two generations. The 2024 one was a conventional store: sign-up and Google sign-in, a server-side cart, reviews, order history and a blog. Version 4.0, rebuilt in August 2026 and in production since September, starts by asking what the business actually needed.

The problem

A store with accounts, checkout, payments and inventory promises an operation this business doesn't have. Requiring sign-up to buy a bar of soap adds friction and gives nothing back, and a checkout that can't take payment is a broken promise. What it did need was to show the products well, help people choose and receive a clear order, because confirmation, payment and delivery would keep happening over WhatsApp.

My role

I gathered the requirements with the business, defined the information architecture and the user journey, designed the interface and developed the store, the API and the admin. I also deployed it on its domain and wrote the technical documentation, the launch checklist and the spec for a future line of custom soaps.

Users

The shopper browses the collection, filters by category, opens each soap's details and builds an order without creating an account. The business team maintains products, prices, stock, photos and featured items from the Django admin, and receives on WhatsApp a readable message with each product, its quantity and the total.

Constraints

  • No payments, no logistics and no promise of real-time stock. That's why the cart total is "estimated".
  • A catalog that changes without rebuilding anything. The store reads it from the API on every visit.
  • Phone first. That's where the brand's audience shops, and where WhatsApp lives.
  • No shopper data. The store keeps no profiles, addresses or orders.

How I built it

I started from the real sales journey — discover the brand, find a product, understand its benefits, build the order and open the conversation — and removed everything that didn't serve it. The rebuild dropped the previous version's models with explicit migrations (cart, cart lines, reviews and user profiles) and added a data migration to give every product a unique slug. It went from eight models to four, and from thirty runtime dependencies on the frontend to two: React and React DOM.

The backend is three Django apps: shop (catalog, API and admin), contact (messages and subscriptions) and accounts (the team user, who signs in with an email). The frontend is six React components, a cart hook and custom CSS, with no state or component libraries. In September 2026 I got it ready for production: I hardened Django, made the cart sync with the catalog and deployed it on its domain.

Architecture decisions

  • No customer accounts. The custom Django user exists only for the team; the shopper never signs up.
  • A public, read-only API. The catalog is read through the API and written only from the admin. The store requests every page of the list, so a bigger catalog never leaves products out.
  • A cart that doesn't lie. It lives in localStorage and survives a reload. When the catalog arrives, each line takes the current price, its quantity is capped at the stock and discontinued products drop out of the cart; if something changed, the drawer says so.
  • A single container in production. Django serves the API, the admin, the photos and the React build from the same domain, with WhiteNoise: no CORS needed. In development, Vite proxies to Django.
  • Demo data never replaces real data. The fallback catalog only shows up in development, or when explicitly enabled, with a visible notice. In production, if the API fails, the customer sees an error with a retry, not made-up products.

The WhatsApp order

"Finish on WhatsApp" opens a wa.me link with the order already written. There's no WhatsApp API integration and nothing to pay per message: the server doesn't record the order, and the conversation continues on the business's phone. This is the message the cart in the screenshots generates, in Spanish, as the business receives it:

¡Hola, Delicaté!
Quiero realizar este pedido:

• 2 × Avena Calma — RD$700
• 1 × Corazón de Lavanda — RD$425
• 1 × Café Despierto — RD$450

Total estimado: RD$1,575

Mi nombre es:
Prefiero: entrega / recoger

¿Me confirman disponibilidad y forma de entrega? Gracias.

The greeting used to carry an emoji that WhatsApp's web page turned into "�" when it redirected the link; now it goes without it. The contact form does the same with the name and the question: it doesn't store anything on the server either.

Production and security

  • Ship only what works. Every push to main builds the image, applies migrations before release and only sends traffic to the new version if /api/health/, which queries the database, returns 200. If anything fails, the previous one stays up.
  • Headers on every response, including the ones WhiteNoise serves: a Content Security Policy, Permissions-Policy and X-Frame-Options; mandatory HTTPS, one-year HSTS and secure cookies.
  • Rate limits that count the real IP behind the proxy: a thousand requests per hour per visitor, with tighter caps on the write endpoints.
  • A safe startup. With DEBUG off, Django refuses to start with the development key or with a relative photo path.
  • Validated photos by format (JPG, PNG, WebP) and size (5 MB), served with a seven-day cache.

Challenges

The main one was building a convincing shopping experience without faking a payments or logistics infrastructure. The second was keeping the managed catalog, the filters, the product view and a cart that survives between visits consistent when prices or stock change. And the third was cutting down: going from an oversized store to a maintainable product of about 2,600 lines.

What deployment taught me

  • Railway didn't apply railway.json to the service created from the command line, and the first version went out without migrations. The configuration is now pinned on the service itself.
  • Git Bash turned the /data/media path into a Windows path, and the photos were lost in a redeploy. I fixed it and added the check that now refuses to start with a relative path.
  • When the custom domain was added, Railway stopped exposing the *.up.railway.app address; both hosts are now explicitly allowed.

Design and UX

The visual direction uses a warm palette of cream, forest green and terracotta, an editorial serif for headlines and plenty of breathing room to communicate craft, without downloading web fonts. The page tells a story in order: home, values, collection, brand story, how to order in three steps, FAQ and contact. When you add a product, the cart opens to confirm the gesture.

The states are all handled: loading, error with retry, empty category and sold-out product. For accessibility, the product view is a native <dialog>, the cart traps and returns focus, the mobile menu closes with Escape, there's a skip-to-content link, filters declare their state with aria-pressed and prefers-reduced-motion is respected.

What it doesn't have

So nobody assumes otherwise: there are no payments, no in-store search (only the API accepts ?search=), no product variants, no more than one photo per product and no dedicated URL for each soap: the product view is a dialog. There's no order dashboard either, because orders never go through the server. The API keeps two rate-limited endpoints for contact and newsletter that no store screen uses today.

About the screenshots

The screenshots were taken on September 25, 2026, on the store's production build, served locally with a throwaway database and the catalog's ten products. Some states were triggered on purpose to show them: the sold-out product, the catalog that's slow or fails, the empty category and the saved cart with old prices. What you see in each one is production code. The product photos are the original project's; the lifestyle images on the home page and in the story were generated for this version. Screens with any visible defect were left out.

What I learned

A good product doesn't need to fake more infrastructure than the business can run. Removing accounts, the server-side cart and checkout made the system smaller, safer and easier to adopt, and turned a real limitation into a coherent flow. The version that delivers the most wasn't the one that did the most, but the one that best fit how the business sells.

Current status

Live at delicate.jonasjavier.dev since September 25, 2026: a Docker service on Railway with PostgreSQL, a volume for photos and its own domain over HTTPS. The repository is public for evaluation, under a proprietary license, with continuous integration green and 18 automated tests passing. Before announcing it, the checks that depend on the business are still on its launch list: delivery and privacy policies, a full order tested on Android and iPhone, backups and monitoring. I don't publish sales or traffic because I don't measure them.

The product's next line is already specified: a guided custom-soap configurator whose result is a WhatsApp request, not an automatic purchase. It isn't part of the system yet.