Duka Platform
Multi-vendor marketplace for Tanzania & Zanzibar
Duka is a multi-vendor e-commerce platform built for the Tanzanian and Zanzibar markets. It connects customers, sellers, and delivery agentsthrough four separate web applications backed by a single Laravel REST API and Firebase authentication.
Customer App
app.duka.supplies
Browse products, cart, orders, AI chatbot
Seller Portal
seller.duka.supplies
Manage products, orders, analytics
Delivery App
delivery.duka.supplies
Accept deliveries, track earnings
Admin Panel
admin.duka.supplies
Platform management, KYC, approvals
Architecture
Duka uses a monorepo structure managed by Turborepo. Each web application is an independent Next.js project deployed on Vercel. All apps share the same backend API and Firebase project.
duka-web/ <span class="comment"># Turborepo monorepo</span> ├── apps/ │ ├── admin/ <span class="comment"># Next.js — admin panel</span> │ ├── customer/ <span class="comment"># Next.js — customer app</span> │ ├── seller/ <span class="comment"># Next.js — seller portal</span> │ ├── delivery/ <span class="comment"># Next.js — delivery app</span> │ └── docs/ <span class="comment"># Next.js — this docs site</span> └── package.json <span class="comment"># root workspace config duka-api/ # Laravel 11 REST API ├── Modules/ │ ├── Auth/ │ ├── Product/ │ ├── Order/ │ ├── Seller/ │ ├── Delivery/ │ └── Admin/ └── routes/api.php
Request flow
- User authenticates with Firebase (email/password)
- Client exchanges Firebase ID token for a Laravel Sanctum token via
POST /api/auth/login - Subsequent requests use
Authorization: Bearer <sanctum_token> - Role middleware (
role:admin,role:seller, etc.) guards each route group
Tech Stack
Laravel 11
REST API, Sanctum auth, modular architecture (nwidart/laravel-modules)
Next.js 16
App Router, Server Components, Tailwind CSS v4, deployed on Vercel
Firebase
Authentication (email/password). Firestore not used — all data in MySQL
MySQL
Primary database via Laravel Eloquent ORM
Cloudflare
DNS management for duka.supplies (DNS-only, no proxy for Vercel)
Gemini AI
Google Gemini 2.0 Flash for customer chatbot — free tier, server-side only
Platforms
Customer App — apps/customer
app.duka.supplies- Sign in / Create account (Firebase + API)
- Home feed, categories, product search & browse
- Product detail, add to cart, checkout flow
- Order tracking with real-time status
- Profile, saved addresses, order history
- AI-powered chatbot (Gemini 2.0 Flash) at bottom-right
Seller Portal — apps/seller
seller.duka.supplies- Multi-step KYC registration (Account → Shop Info → pending approval)
- Dashboard with sales analytics
- Product management (create, edit, variants, images)
- Order management (accept, fulfill, track)
- Shop settings & payout info
Delivery App — apps/delivery
delivery.duka.supplies- Multi-step agent registration (Account → Vehicle Info → pending approval)
- Available orders queue & accept/decline
- Active delivery tracking & status updates
- Earnings dashboard & history
Admin Panel — apps/admin
admin.duka.supplies- Platform overview dashboard
- Seller KYC review & approval
- Delivery agent approval
- Product & category management
- Order monitoring
- User management (customers, sellers, agents)
API Reference
Base URL: https://api.duka.supplies/api (production) · http://localhost:8000/api (local)
Authentication
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/auth/login | POST | — | Exchange Firebase ID token for Sanctum token |
/auth/register | POST | — | Create new user account (role: customer) |
/auth/me | GET | Bearer | Get authenticated user profile |
Products
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/products | GET | — | List products (filterable by category, seller, search) |
/products/:id | GET | — | Get product detail |
/seller/products | GET/POST | Bearer (seller) | Seller's product management |
/seller/products/:id | PUT/DELETE | Bearer (seller) | Update or delete own product |
/admin/products | GET | Bearer (admin) | All products with moderation tools |
Orders
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/orders | POST | Bearer (customer) | Place an order |
/orders | GET | Bearer (customer) | Customer's order history |
/seller/orders | GET | Bearer (seller) | Orders for seller's products |
/delivery/orders | GET | Bearer (delivery) | Available & assigned deliveries |
/admin/orders | GET | Bearer (admin) | All orders platform-wide |
Registration (KYC)
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/seller/register | POST | Bearer (customer) | Submit seller application |
/delivery/register | POST | Bearer (customer) | Submit delivery agent application |
/admin/sellers/:id/approve | POST | Bearer (admin) | Approve seller (role → seller) |
/admin/delivery/:id/approve | POST | Bearer (admin) | Approve delivery agent (role → delivery) |
Authentication
All apps use Firebase Authentication (email/password) paired with Laravel Sanctum. Firebase handles credential storage and token generation. Laravel validates the Firebase ID token and returns its own Sanctum token for subsequent API calls.
Login flow
<span class="comment">// 1. Sign in with Firebase</span>
<span class="kw">const</span> cred = <span class="kw">await</span> signInWithEmailAndPassword(auth, email, password)
<span class="kw">const</span> idToken = <span class="kw">await</span> cred.user.getIdToken()
<span class="comment">// 2. Exchange for Sanctum token</span>
<span class="kw">const</span> res = <span class="kw">await</span> api.post(<span class="str">'/auth/login'</span>, { firebase_token: idToken })
<span class="kw">const</span> { token, user } = res.data
<span class="comment">// 3. Store and use for all API calls</span>
localStorage.setItem(<span class="str">'duka_admin_token'</span>, token)Token storage keys
| App | localStorage key |
|---|---|
| Customer | duka_customer_token |
| Seller | duka_seller_token |
| Delivery | duka_delivery_token |
| Admin | duka_admin_token |
Firebase lazy initialization
All apps/*/lib/firebase.ts files use a Proxy pattern to defer Firebase initialization to client-side only. This prevents auth/invalid-api-key errors during Next.js server-side prerendering at build time.
<span class="comment">// lib/firebase.ts — lazy proxy pattern</span>
<span class="kw">let</span> _auth: Auth | undefined
<span class="kw">function</span> getInstance(): Auth {
<span class="kw">if</span> (!_auth) {
<span class="kw">const</span> app = getApps()[0] ?? initializeApp(firebaseConfig)
_auth = getAuth(app)
}
<span class="kw">return</span> _auth
}
<span class="kw">export const</span> auth = <span class="kw">new</span> Proxy({} <span class="kw">as</span> Auth, {
get(_, prop) {
<span class="kw">const</span> a = getInstance()
<span class="kw">const</span> val = Reflect.get(a, prop, a)
<span class="kw">return typeof</span> val === <span class="str">'function'</span> ? val.bind(a) : val
},
})Environment Variables
Never commit .env.local files. All environment variables must be added via the Vercel dashboard for each project, and locally in apps/[app]/.env.local.
Admin — apps/admin
| Variable | Example value |
|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | AIzaSy... |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | duka-app.firebaseapp.com |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | duka-app |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | duka-app.appspot.com |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | 1234567890 |
NEXT_PUBLIC_FIREBASE_APP_ID | 1:123:web:abc |
NEXT_PUBLIC_API_URL | https://api.duka.supplies/api |
Seller — apps/seller
Same Firebase variables as admin, plus:
| Variable | Example value |
|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | AIzaSy... |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | duka-app.firebaseapp.com |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | duka-app |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | duka-app.appspot.com |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | 1234567890 |
NEXT_PUBLIC_FIREBASE_APP_ID | 1:123:web:abc |
NEXT_PUBLIC_API_URL | https://api.duka.supplies/api |
Delivery — apps/delivery
Same Firebase + API variables as seller.
Customer — apps/customer
Same Firebase + API variables, plus the Gemini API key (server-side only):
| Variable | Notes |
|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | Public Firebase key |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | |
NEXT_PUBLIC_FIREBASE_APP_ID | |
NEXT_PUBLIC_API_URL | |
GEMINI_API_KEY | Server-side only — NO NEXT_PUBLIC_ prefix |
Deployment
Vercel — frontend apps
Each app is deployed as a separate Vercel project pointing to the monorepo root:
- Import
github.com/abdulrazakmustafa/duka-webin Vercel - Set Root Directory to
apps/admin(orcustomer,seller,delivery,docs) - Framework preset: Next.js
- Add all environment variables for that app
- Assign custom domain
Custom domains (Cloudflare DNS)
| Subdomain | Vercel project | DNS type |
|---|---|---|
admin.duka.supplies | duka-admin | CNAME → cname.vercel-dns.com |
seller.duka.supplies | duka-seller | CNAME → cname.vercel-dns.com |
delivery.duka.supplies | duka-delivery | CNAME → cname.vercel-dns.com |
app.duka.supplies | duka-customer | CNAME → cname.vercel-dns.com |
docs.duka.supplies | duka-docs | CNAME → cname.vercel-dns.com |
api.duka.supplies | Hostinger (Laravel) | A record → server IP |
Cloudflare DNS must be set to DNS-only(grey cloud, not orange) for all Vercel CNAMEs. Orange proxy breaks Vercel's TLS certificate provisioning.
Laravel API — Hostinger
- Upload
duka-api/to Hostinger via File Manager or Git - Run
composer install --no-devin the API directory - Set
.envwith database credentials,APP_KEY, Firebase service account path - Run
php artisan migrate - Point
api.duka.suppliesA record to the Hostinger server IP - Configure document root to
public/
Firebase Setup
- Go to console.firebase.google.com
- Create project duka-app (or use existing)
- Enable Authentication → Email/Password sign-in method
- Create a Web App and copy the config object:
<span class="kw">const</span> firebaseConfig = { <span class="key">apiKey</span>: <span class="str">"AIzaSy..."</span>, <span class="key">authDomain</span>: <span class="str">"duka-app.firebaseapp.com"</span>, <span class="key">projectId</span>: <span class="str">"duka-app"</span>, <span class="key">storageBucket</span>: <span class="str">"duka-app.appspot.com"</span>, <span class="key">messagingSenderId</span>: <span class="str">"1234567890"</span>, <span class="key">appId</span>: <span class="str">"1:123:web:abc123"</span>, } - Create a Service Account (Project Settings → Service Accounts → Generate new private key)
- Upload the JSON key to Hostinger and set its path in Laravel's
.env:FIREBASE_CREDENTIALS=/path/to/firebase-credentials.json
All four web apps use the same Firebase project — one project handles auth for all portals. The backend validates the ID token server-side to determine which Laravel user it belongs to.
Email & SMTP
| Purpose | Address |
|---|---|
| Super Admin | abdulrazak.jmus@gmail.com |
| Platform Admin | admin@duka.supplies |
| Support | support@duka.supplies |
SMTP configuration (Hostinger)
<span class="comment"># Laravel .env</span> <span class="key">MAIL_MAILER</span>=<span class="val">smtp</span> <span class="key">MAIL_HOST</span>=<span class="val">smtp.hostinger.com</span> <span class="key">MAIL_PORT</span>=<span class="val">465</span> <span class="key">MAIL_ENCRYPTION</span>=<span class="val">ssl</span> <span class="key">MAIL_USERNAME</span>=<span class="val">admin@duka.supplies</span> <span class="key">MAIL_PASSWORD</span>=<span class="val">your_email_password</span> <span class="key">MAIL_FROM_ADDRESS</span>=<span class="val">admin@duka.supplies</span> <span class="key">MAIL_FROM_NAME</span>=<span class="val">"Duka"</span>
AI Chatbot
The customer app includes a Gemini 2.0 Flash powered chatbot. It answers questions about orders, delivery fees, how to sell on Duka, and general platform info.
How it works
- Chat widget (
components/ChatWidget.tsx) sends messages toPOST /api/chat - Next.js API route (
app/api/chat/route.ts) calls Gemini server-side - The API key (
GEMINI_API_KEY) is never exposed to the browser
Limits
- Free tier: 1,500 requests/day, 15 requests/minute
- Context window: last 6 messages sent with each request
- Model:
gemini-2.0-flash
<span class="comment"># Customer app .env.local</span> <span class="key">GEMINI_API_KEY</span>=<span class="val">AIzaSy...your_key_here</span> <span class="comment"># Also add GEMINI_API_KEY in Vercel dashboard</span> <span class="comment"># for the duka-customer project (no NEXT_PUBLIC_ prefix)</span>
Roles & Flows
User roles
| Role | How obtained | Access |
|---|---|---|
customer | Default on registration | Customer app, browse, order |
seller | Admin approves KYC application | Seller portal |
delivery | Admin approves agent application | Delivery app |
admin | Set manually in database | Admin panel, all management |
Seller KYC flow
- Applicant registers a Firebase account & customer account via
/auth/register - Submits shop info to
/seller/register(requiresrole:customertoken) - Seller record created with
status: pending - Admin reviews in Admin Panel → approves → user role updated to
seller - Applicant can now sign in to seller.duka.supplies
Delivery agent flow
- Applicant registers a Firebase account & customer account
- Submits vehicle info to
/delivery/register - DeliveryBoy record created with
status: pending - Admin approves → user role updated to
delivery - Agent can now sign in to delivery.duka.supplies
Order lifecycle
| Status | Who sets it |
|---|---|
pending | Created by customer checkout |
confirmed | Seller confirms order |
processing | Seller prepares item |
ready_for_pickup | Seller marks ready |
out_for_delivery | Delivery agent picks up |
delivered | Delivery agent confirms delivery |
cancelled | Seller or admin |
Delivery fee: Flat TZS 2,000 per order, added at checkout. Fee is paid to the delivery agent via platform wallet system.