A full-stack e-commerce web application built for the Thai market. Rose Γ deep plum brand identity, JWT-based authentication, Stripe payments, and a complete admin panel.
Author: Hermann Nβzi Ngenda
Live Site: https://abona-3ve.pages.dev





Abona/
βββ frontend/ # All client-side code
β βββ index.html # Main shop page
β βββ login.html
β βββ register.html
β βββ forgot-password.html
β βββ reset-password.html
β βββ checkout.html
β βββ orders.html
β βββ tracking.html
β βββ product.html
β βββ wishlist.html
β βββ settings.html
β βββ images/ # Product images, logos, icons
β βββ styles/
β β βββ shared/ # general.css, amazon-header.css
β β βββ pages/ # Per-page stylesheets
β βββ scripts/
β β βββ amazon.js # Homepage product grid + filters
β β βββ auth.js # Auth state, header rendering
β β βββ checkout.js # Checkout page entry
β β βββ checkout/
β β β βββ orderSummary.js
β β β βββ paymentSummary.js
β β βββ utils/
β β βββ api.js # API_BASE constant
β β βββ money.js # formatCurrency helper
β β βββ darkmode.js
β βββ data/
β βββ cart.js # Cart state + API calls
β βββ deliveryOptions.js
β
βββ backend/ # Node.js / Express API
β βββ server.js # App entry point
β βββ .env # Environment variables (not committed)
β βββ routes/
β β βββ auth.js
β β βββ products.js
β β βββ cart.js
β β βββ orders.js
β β βββ payment.js
β β βββ reviews.js
β β βββ wishlist.js
β β βββ coupons.js
β β βββ users.js
β β βββ uploads.js
β β βββ admin.js
β βββ middleware/
β β βββ auth.js # JWT verification middleware
β βββ db/
β β βββ connection.js # MySQL pool
β β βββ schema.sql # Full database schema + triggers
β β βββ seed.js # Sample data seeder
β βββ utils/
β β βββ email.js # Brevo HTTP API email templates
β βββ admin/ # Admin panel HTML + assets
β
βββ tests/ # Jasmine unit tests
βββ README.md
| Tool | Version |
|---|---|
| Node.js | 18+ |
| MySQL / MariaDB | 8.0+ (XAMPP works) |
| VS Code + Live Server | Any |
mysql -u root -p < backend/db/schema.sql
cd backend
cp .env.example .env
npm install
npm start # http://localhost:3000
Open frontend/index.html with VS Code Live Server (configured via .vscode/settings.json to serve from frontend/).
Site runs at http://127.0.0.1:5500. Make sure CLIENT_ORIGIN=http://127.0.0.1:5500 in .env.
PORT=3000
# MySQL
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=abona_shop
# JWT
JWT_SECRET=long_random_string
JWT_ADMIN_SECRET=different_long_random_string
# Arcjet
ARCJET_KEY=ajkey_...
ARCJET_ENV=development
# CORS
CLIENT_ORIGIN=http://127.0.0.1:5500
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
# Brevo (email)
BREVO_API_KEY=xkeysib_...
BREVO_SENDER_EMAIL=you@gmail.com
ADMIN_EMAIL=admin@yourdomain.com
βββββββββββββββββββββββ HTTPS βββββββββββββββββββββββ
β Cloudflare Pages β ββββββββββββββΊ β Render Backend β
β (Frontend) β β (Express API) β
βββββββββββββββββββββββ ββββββββββββ¬βββββββββββ
β SSL
ββββββββββββΌβββββββββββ
β Aiven MySQL β
β (Database) β
βββββββββββββββββββββββ
mysql --ssl-ca=ca.pem -h your-host -P port -u user -p dbname < backend/db/schema.sql
backendnpm installnode server.jsPORT=3000
DB_HOST=your-aiven-host
DB_PORT=your-aiven-port
DB_USER=your-aiven-user
DB_PASSWORD=your-aiven-password
DB_NAME=defaultdb
DB_SSL=true
JWT_SECRET=...
JWT_ADMIN_SECRET=...
ARCJET_KEY=...
ARCJET_ENV=production
CLIENT_ORIGIN=https://your-project.pages.dev
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...
BREVO_API_KEY=...
BREVO_SENDER_EMAIL=...
ADMIN_EMAIL=...
https://abona-backend.onrender.comfrontend. (dot)frontend/scripts/utils/api.js:
export const API_BASE = 'https://abona-backend.onrender.com';
https://abona.pages.devCLIENT_ORIGIN=https://abona.pages.devAdd SSL support to backend/db/connection.js:
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: true } : false
In Cloudflare Pages β Custom Domains β add abona-shop.com.
DNS is managed automatically since youβre already on Cloudflare.
Browser Backend Database
β β β
ββββ POST /api/auth/register βββββΊβ β
β { name, email, password } βββ bcrypt.hash(password) ββββββΊβ
β βββ INSERT INTO users βββββββββββΊβ
β βββββββββββββββββββββββββββββββββ€
ββββ Set-Cookie: token (JWT) ββββββ€ β
β β β
ββββ GET /api/auth/me ββββββββββββΊβββ jwt.verify(cookie) βββββββββΊβ
ββββ { user } βββββββββββββββββββββ€ β
{ id, email, name, role }, expires 7 daysverifyToken middlewareAdd to Cart βββΊ POST /api/cart
Checkout βββΊ POST /api/payment/create-intent (Stripe PaymentIntent)
βββΊ stripe.confirmPayment()
βββΊ POST /api/orders (saves order, clears cart)
Event Customer Admin
βββββββββββββββββββββββββββββββββββββββββββββββββββββ
Order placed β
Confirmation β
Alert
Order paid (admin) β
Notification β
Order shipped (admin) β
Notification β
Order delivered (admin) β
Notification β
Order cancelled by customer β β
Alert
Order cancelled by admin β
Notification β
Low stock after order β β
Alert
Password reset β
Reset link β
<script>
if (localStorage.getItem('darkMode') === 'true')
document.documentElement.classList.add('dark');
</script>
Runs before CSS loads β prevents flash of unstyled content.
users βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βββ sessions (multi-device login tracking) β
βββ addresses (saved shipping addresses) β
βββ cart_items ββββββββ products ββββ product_images β
βββ wishlists ββββ product_variants β
β βββ wishlist_items ββββ product_categories β
βββ reviews βββ categories β
βββ orders ββββββββββββββββββββββββββββββββββββββββββββββββββΊ β
βββ order_items β
βββ order_status_logs β
βββ payments β
β
coupons ββ coupon_uses ββββββββββββββββββββββββββββββββββββββββββ
password_resets
| Table | Purpose |
|---|---|
users |
Account data, role (user/admin) |
products |
Catalog with denormalised star rating |
product_variants |
Size/color SKUs with individual stock |
cart_items |
Per-user persistent cart |
orders |
Order header with frozen shipping snapshot |
order_items |
Line items with product data snapshot |
payments |
Stripe PaymentIntent records |
coupons |
Discount codes (percentage or fixed) |
reviews |
One review per user per product |
password_resets |
Secure tokens for password reset (1hr expiry) |
Fetch cart:
SELECT c.*, p.name, p.image, p.price_cents
FROM cart_items c
JOIN products p ON c.product_id = p.id
WHERE c.user_id = ?
Place an order (transaction):
BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO order_items (...) VALUES (...);
UPDATE products SET stock = stock - ? WHERE id = ?;
DELETE FROM cart_items WHERE user_id = ?;
COMMIT;
Rating trigger (auto on review change):
UPDATE products
SET stars = (SELECT ROUND(AVG(stars), 2) FROM reviews
WHERE product_id = NEW.product_id AND is_approved = TRUE),
rating_count = (SELECT COUNT(*) FROM reviews
WHERE product_id = NEW.product_id AND is_approved = TRUE)
WHERE id = NEW.product_id;
Validate coupon:
SELECT * FROM coupons
WHERE code = ?
AND is_active = TRUE
AND (expires_at IS NULL OR expires_at >= NOW())
AND (max_uses IS NULL OR uses_count < max_uses)
Incoming Request
β
βΌ
βββββββββββββββββββββββββββββββ
β Arcjet Shield β β blocks bots, scrapers, attack patterns
βββββββββββββββββββββββββββββββ€
β Sliding Window β β rate limiting per IP
β (10 req/hr on register, β
β 10 req/15min on login, β
β 3 req/15min on reset) β
βββββββββββββββββββββββββββββββ€
β Email Validation β β rejects disposable / invalid emails
βββββββββββββββββββββββββββββββ
β
βΌ
Route Handler
Frontend Backend Stripe
β β β
βββ GET /api/config βββββββββΊβ β
ββββ { publishableKey } ββββββ€ β
β β β
βββ POST /api/payment/ β β
β create-intent βββββββββββΊβββ paymentIntents.create() βΊβ
β ββββ { client_secret } ββββββββ€
ββββ { clientSecret } ββββββββ€ β
β β β
β stripe.confirmPayment() β β
βββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββΊβ
ββββ status: succeeded βββββββΌβββββββββββββββββββββββββββββ€
β β β
βββ POST /api/orders ββββββββΊβββ saves order to DB β
ββββ { orderId } βββββββββββββ€ β
return_url handles 3D Secure redirects| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
β | Create account |
| POST | /api/auth/login |
β | Sign in, set cookie |
| POST | /api/auth/logout |
β | Clear cookie |
| GET | /api/auth/me |
Cookie | Get current user |
| POST | /api/auth/forgot-password |
β | Send reset email |
| POST | /api/auth/reset-password |
β | Set new password |
| GET | /api/products |
β | List all products |
| GET | /api/products/:id |
β | Single product |
| GET | /api/cart |
β | Get user cart |
| POST | /api/cart |
β | Add item |
| PATCH | /api/cart/:id |
β | Update qty / delivery |
| DELETE | /api/cart/:id |
β | Remove item |
| GET | /api/orders |
β | Order history |
| POST | /api/orders |
β | Place order |
| GET | /api/orders/:id |
β | Single order |
| PATCH | /api/orders/:id/cancel |
β | Cancel order |
| POST | /api/payment/create-intent |
β | Stripe PaymentIntent |
| GET | /api/wishlist |
β | Get wishlist |
| POST | /api/wishlist/:id |
β | Add to wishlist |
| DELETE | /api/wishlist/:id |
β | Remove from wishlist |
| POST | /api/coupons/validate |
β | Validate coupon |
| GET | /api/reviews/:productId |
β | Product reviews |
| POST | /api/reviews/:productId |
β | Submit review |
Local: http://localhost:3000/admin/login
Production: https://abona-backend.onrender.com/admin/login
Protected by JWT_ADMIN_SECRET. Features:
| Layer | Technology |
|---|---|
| Frontend | Vanilla HTML, CSS, ES Modules |
| Backend | Node.js, Express.js |
| Database | MySQL / MariaDB |
| Auth | JWT (httpOnly cookies) + bcryptjs |
| Payments | Stripe (Payment Element) |
| Security | Arcjet (shield + rate limit + email validation) |
| Brevo HTTP API | |
| Hosting β Frontend | Cloudflare Pages |
| Hosting β Backend | Render |
| Hosting β Database | Aiven MySQL |
Built by Hermann Nβzi Ngenda