Website Link: clearup.skin!
Clearup is a skincare organizer platform. Users can browse skincare product catalogs, compare seller prices, build step by step routines in a visual builder, and share them to the their friends and family.
Clearup is basically a database of products. Its product information is gathered via a data scraper (same developer is working on it so, thank you for your patience!). It holds important information that makes it easily visible for users, like what skin types a product supports, the ingredients, the formulation, and more. Users can also filter specific information like the brands they enjoy or the skin type the product supports.
A service Clearup provides is an builder for your routine, where you can create a routine that works for you from the product catalog. It saves the information so you can click off and continue to update it until it's ready to be saved! You can also add the order in which the product are used.
Once you save your routine, you have a link carry that information (if you're logged in, I would do logged out too but I'm still a local developer). This will automatically go into the community routine tab where anyone can see and learn from what you use (or what you want to use).
ClearUp/
├── frontend/ # Next.js app
├── backend/ # Express API
│ ├── docker-compose.yml # Local Postgres only
│ ├── docs/modules/ # backend module notes (for legacy)
│ └── testREADME.md # API testing & troubleshooting
└── LICENSE # MIT
ClearUp is a split-stack monorepo: a Next.js frontend talks to a standalone Express API over HTTP. Both run on Bun. PostgreSQL is the single source of truth for products, routines, merchants, and auth.
| Layer | Stack | Hosting |
|---|---|---|
| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS v4 | Prod: Vercel, Staging: Vercel Preview, Dev: http://localhost:3000 |
| Backend | Express 5, TypeScript, Sequelize 6 | Prod: AWS EC2 t3.micro (Ubuntu, Caddy), Staging: Render, Dev: http://localhost:5050 |
| Database | PostgreSQL via Sequelize + Umzug migrations | Prod: Supabase pooler, Staging: Supabase pooler, Dev: Docker Postgres |
| Auth | Better Auth (sessions + Google OAuth) | Runs on the API; frontend is a cookie-aware client |
| AWS SES v2 | Verification, password reset; SNS webhook for bounces/complaints |
Key dependencies
- Frontend:
better-auth,radix-ui/ shadcn,framer-motion,@vercel/analytics,@vercel/speed-insights - Backend:
better-auth,sequelize,pg,umzug,@aws-sdk/client-sesv2,express-rate-limit,compression
How the pieces connect
- The frontend calls
NEXT_PUBLIC_API_URLwithcredentials: "include"so Better Auth session cookies flow cross-origin (CORS + trusted origins must align). - Catalog reads use Next.js Data Cache (
revalidatetiers insrc/lib/products.ts); mutations usecache: "no-store"and a customx-clearup-client: 1header for CSRF mitigation on routine writes. - On API boot:
sequelize.authenticate→ model associations →sequelize.sync()→ Umzug migrations. - Auth is application-level (
requireAuth,requireAdmin, ownership checks), not just Postgres RLS.
flowchart
subgraph client["Browser"]
UI["Next.js App"]
end
subgraph vercel["Vercel"]
FE["clearup.skin / Preview"]
end
subgraph api["API host"]
BE["Express + Better Auth"]
end
subgraph data["Data"]
DB[("PostgreSQL")]
SES["AWS SES"]
end
UI --> FE
FE -->|"HTTPS + cookies\nNEXT_PUBLIC_API_URL"| BE
BE --> DB
BE --> SES
SES -->|"SNS bounce/complaint"| BE
Regarding design: this frontend is generally inspired by PCPartPicker and various skincare websites. Since I am the sole developer, I decided to keep this application strict to its few services, but do them very well.
Shoutout to:
- Nich Wilson for UI, marketing, and admin management
- My mom and sister, and Kelly Marks for UI/UX testing
- Stella Ngo for logo design
Next.js App Router with route groups and a @/* path alias:
frontend/src/
├── app/
│ ├── (main)/ # Header + footer shell — catalog, builder, routines, profile
│ ├── (auth)/ # login, register, verify-email, forgot/reset password
│ ├── (info)/ # about, FAQ, privacy, TOS, contact
│ └── admin/ # role-gated dashboard (server layout check)
├── components/ # ui/, product/, routine/, guides/, home/, admin/, auth/
├── hooks/ # useBuilderRoutine, useEffectiveRole, catalog hooks
├── lib/ # API clients (products, routines, auth, merchants)
├── types/ # shared TypeScript types
└── constants/ # filters, mail, country mapping
- Server Components for data-heavy pages (catalog, routines, PDP).
- Client Components for interactivity (builder, auth forms, filters).
loading.tsxskeletons on major async routes.
| Concern | Approach |
|---|---|
| Auth session | better-auth/react via authClient.useSession() |
| Admin role | useEffectiveRole → GET /api/auth/me |
| Builder draft | localStorage (builder-routine) via useBuilderRoutine |
| Server cache | fetch with revalidate tiers in lib/products.ts / lib/routines.ts |
| Cross-tab sign-out | BroadcastChannel in useCrossTabSignOut |
Hooks + Better Auth + Next.js cache only.
Loading: Each important async page has skeleton loaders (loading.tsx), inspired by apps like YouTube.
Reduced API calls + speed: Tiered revalidate caching in src/lib/products.ts:
| Constant | TTL | Use |
|---|---|---|
CATALOG_REVALIDATE_SEC |
1h | Category grids + PDP product body |
PRODUCT_MERCHANTS_REVALIDATE_SEC |
30m | Per-product merchant offers on PDP |
BATCH_MERCHANT_OFFERS_REVALIDATE_SEC |
6h | Batched offers on routine pages |
Mutations always use cache: "no-store" and mutationHeaders() (x-clearup-client: 1).
Progress saver: The builder stores a slim JSON snapshot in localStorage so grid slots and notes survive navigation before save.
Images: Product images are not hosted natively; they load from retailer CDNs (YesStyle, Sephora, etc.) via next/image remotePatterns. next/image optimization applies to those remotes. I do want to implement an
profile picture + image comments for routines in the future.
Styling — Tailwind CSS, shadcn/ui primitives (components/ui/), Inter + Geist fonts, framer-motion for scroll reveals and auth-page motion. I want to formalize all the CSS variables that I use though (will refractor!).
routes → controllers → services → repositories → models
Each HTTP concern stays at the controller; business logic lives in services, and SQL stays in repositories. Controllers validate input and map responses to the repositories, being the only layer that talks to Sequelize.
backend/src/
├── index.ts # Express bootstrap, middleware, route mounting
├── db.ts # Sequelize singleton
├── associations.ts # model relationships
├── config/ # auth, pagination, product categories
├── routes/ # product, routine, merchant, user, webhook
├── controllers/ # HTTP handlers
├── services/ # business logic
├── repositories/ # DB queries
├── models/ # Sequelize models
├── middleware/ # requireAuth, rate limits, CSRF header, CSV parse
├── migrations/ # Umzug 001–009
└── lib/ # dbConfig, SES, CSV import, security helpers
- CORS (
TRUSTED_ORIGINS,credentials: true) +trust proxy - Webhook route (raw body) before JSON parser
- Global rate limit (300 / 15 min), compression
- Auth stack on
/api/auth(rate limits + audit logging) - Feature routes:
/api/users,/api/products,/api/routines,/api/merchant GET /health— DB connectivity probevalidateSecurityConfig→sequelize.authenticate→ associations →sync()→runMigrations()→listen
| Domain | Models |
|---|---|
| Auth (Better Auth) | User, Session, Account, Verification |
| Catalog | Product, Merchant, ProductMerchant |
| Routines | Routine, RoutineProduct, FeaturedRoutine |
Schema evolves via Umzug migrations (stored in sequelize_meta); sequelize.sync() on boot creates missing tables non-destructively.
- Better Auth on Express handles sign-up, sign-in, Google OAuth, and sessions. The frontend never holds a JWT, it forwards cookies.
requireAuthresolves the session, hydrates the user from DB if needed, and promotesADMIN_EMAILSwhitelist entries torole: "admin".requireAdmingates admin routes and CSV imports.requireMutationHeaderon routine POST/PUT/PATCH/DELETE, blocks cross-origin form CSRF.- Rate limits: global 300/15 min; routine POST 10/hr; routine mutations 60/15 min; merchant POST 10/hr; dedicated auth brute-force limiters.
- Email: SES for verification and password reset;
/api/webhooks/aws-ses(SNS) updatesuser.emailStatuson bounces/complaints.
OAuth note: redirect URIs must point at the API host (e.g. http://localhost:5050/api/auth/callback/google), not the Next.js port. When frontend and API are on different origins in production, set BETTER_AUTH_CROSS_SITE_COOKIES=1.
cd backend
docker compose up -dPostgres listens on localhost:5432 (skincare / postgres / password123 per docker-compose.yml).
Create backend/.env using backend/.env.example (if in NODE_ENV=development, set DATABASE_URL= nothing):
cd backend
bun install
bun devMigrations apply on boot. Health check: GET http://localhost:5050/health.
Run bun run seed:merchants for sellers (will add a admin endpoint eventually).
Create frontend/.env using frontend/.env.example:
cd frontend
bun install
bun dev Open http://localhost:3000.
Port note: The API defaults to
5000in code ifPORTis unset; the frontend defaults to5050. SetPORT=5050locally so docs, OAuth redirects, andNEXT_PUBLIC_API_URLstay aligned.
| Prefix | Auth | Role |
|---|---|---|
/api/auth/* |
Better Auth | Sign-up, sign-in, OAuth, sessions |
GET /api/auth/me |
Session | Current user { id, email, role } |
/api/products |
Mixed | Catalog CRUD; admin for writes/imports |
/api/routines |
Mixed | Routines, guides, featured, admin stats |
/api/merchant |
Admin writes | Merchant master data |
/api/users/admin |
Admin | User listing |
/api/webhooks/aws-ses |
SNS | Bounce/complaint handling |
/health |
Public | DB connectivity probe |
Rate limits: global (300 / 15 min), stricter caps on routine/merchant POST and auth brute-force paths.
| Path | Description |
|---|---|
/ |
Landing: hero, featured routines, how it works |
/products/category/[slug] |
Category product grid |
/product/id/[slug] |
Product detail + merchant offers |
/builder |
Routine builder (local + save) |
/routine/[id] |
Shared routine view |
/routines |
Community routines with filters (/guides redirects here) |
/profile/* |
Saved/created routines, preferences |
/login, /register, … |
Auth flows |
/admin/* |
Admin dashboard (role-gated) |
Backend (backend/):
| Command | Description |
|---|---|
bun dev |
Watch mode API |
bun run build / bun run start |
Production compile + run |
bun run migrate:up / migrate:down |
Umzug migrations |
bun run seed:merchants |
Seed merchant data from CSV |
bun run test:ses |
Send test email via SES |
Frontend (frontend/):
| Command | Description |
|---|---|
bun dev |
Run development frontend |
bun run build / start |
Production build |
bun run analyze |
Bundle analysis (ANALYZE=true) |
Backend module notes remain under backend/docs/modules/ for controller/service/repository detail.
MIT — see LICENSE. Copyright (c) 2025 Anthony Pham.
Anthony Pham — phamanthony47@gmail.com
