D
Duka Platform — Developer Documentation

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

  1. User authenticates with Firebase (email/password)
  2. Client exchanges Firebase ID token for a Laravel Sanctum token via POST /api/auth/login
  3. Subsequent requests use Authorization: Bearer <sanctum_token>
  4. 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

EndpointMethodAuthDescription
/auth/loginPOSTExchange Firebase ID token for Sanctum token
/auth/registerPOSTCreate new user account (role: customer)
/auth/meGETBearerGet authenticated user profile

Products

EndpointMethodAuthDescription
/productsGETList products (filterable by category, seller, search)
/products/:idGETGet product detail
/seller/productsGET/POSTBearer (seller)Seller's product management
/seller/products/:idPUT/DELETEBearer (seller)Update or delete own product
/admin/productsGETBearer (admin)All products with moderation tools

Orders

EndpointMethodAuthDescription
/ordersPOSTBearer (customer)Place an order
/ordersGETBearer (customer)Customer's order history
/seller/ordersGETBearer (seller)Orders for seller's products
/delivery/ordersGETBearer (delivery)Available & assigned deliveries
/admin/ordersGETBearer (admin)All orders platform-wide

Registration (KYC)

EndpointMethodAuthDescription
/seller/registerPOSTBearer (customer)Submit seller application
/delivery/registerPOSTBearer (customer)Submit delivery agent application
/admin/sellers/:id/approvePOSTBearer (admin)Approve seller (role → seller)
/admin/delivery/:id/approvePOSTBearer (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

ApplocalStorage key
Customerduka_customer_token
Sellerduka_seller_token
Deliveryduka_delivery_token
Adminduka_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

VariableExample value
NEXT_PUBLIC_FIREBASE_API_KEYAIzaSy...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINduka-app.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_IDduka-app
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKETduka-app.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID1234567890
NEXT_PUBLIC_FIREBASE_APP_ID1:123:web:abc
NEXT_PUBLIC_API_URLhttps://api.duka.supplies/api

Seller — apps/seller

Same Firebase variables as admin, plus:

VariableExample value
NEXT_PUBLIC_FIREBASE_API_KEYAIzaSy...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINduka-app.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_IDduka-app
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKETduka-app.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID1234567890
NEXT_PUBLIC_FIREBASE_APP_ID1:123:web:abc
NEXT_PUBLIC_API_URLhttps://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):

VariableNotes
NEXT_PUBLIC_FIREBASE_API_KEYPublic 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_KEYServer-side only — NO NEXT_PUBLIC_ prefix

Deployment

Vercel — frontend apps

Each app is deployed as a separate Vercel project pointing to the monorepo root:

  1. Import github.com/abdulrazakmustafa/duka-web in Vercel
  2. Set Root Directory to apps/admin (or customer, seller, delivery, docs)
  3. Framework preset: Next.js
  4. Add all environment variables for that app
  5. Assign custom domain

Custom domains (Cloudflare DNS)

SubdomainVercel projectDNS type
admin.duka.suppliesduka-adminCNAME → cname.vercel-dns.com
seller.duka.suppliesduka-sellerCNAME → cname.vercel-dns.com
delivery.duka.suppliesduka-deliveryCNAME → cname.vercel-dns.com
app.duka.suppliesduka-customerCNAME → cname.vercel-dns.com
docs.duka.suppliesduka-docsCNAME → cname.vercel-dns.com
api.duka.suppliesHostinger (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

  1. Upload duka-api/ to Hostinger via File Manager or Git
  2. Run composer install --no-dev in the API directory
  3. Set .env with database credentials, APP_KEY, Firebase service account path
  4. Run php artisan migrate
  5. Point api.duka.supplies A record to the Hostinger server IP
  6. Configure document root to public/

Firebase Setup

  1. Go to console.firebase.google.com
  2. Create project duka-app (or use existing)
  3. Enable Authentication → Email/Password sign-in method
  4. 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>,
    }
  5. Create a Service Account (Project Settings → Service Accounts → Generate new private key)
  6. 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

PurposeAddress
Super Adminabdulrazak.jmus@gmail.com
Platform Adminadmin@duka.supplies
Supportsupport@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

  1. Chat widget (components/ChatWidget.tsx) sends messages to POST /api/chat
  2. Next.js API route (app/api/chat/route.ts) calls Gemini server-side
  3. 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

RoleHow obtainedAccess
customerDefault on registrationCustomer app, browse, order
sellerAdmin approves KYC applicationSeller portal
deliveryAdmin approves agent applicationDelivery app
adminSet manually in databaseAdmin panel, all management

Seller KYC flow

  1. Applicant registers a Firebase account & customer account via /auth/register
  2. Submits shop info to /seller/register (requires role:customer token)
  3. Seller record created with status: pending
  4. Admin reviews in Admin Panel → approves → user role updated to seller
  5. Applicant can now sign in to seller.duka.supplies

Delivery agent flow

  1. Applicant registers a Firebase account & customer account
  2. Submits vehicle info to /delivery/register
  3. DeliveryBoy record created with status: pending
  4. Admin approves → user role updated to delivery
  5. Agent can now sign in to delivery.duka.supplies

Order lifecycle

StatusWho sets it
pendingCreated by customer checkout
confirmedSeller confirms order
processingSeller prepares item
ready_for_pickupSeller marks ready
out_for_deliveryDelivery agent picks up
deliveredDelivery agent confirms delivery
cancelledSeller or admin

Delivery fee: Flat TZS 2,000 per order, added at checkout. Fee is paid to the delivery agent via platform wallet system.

D
Duka Platform· Tanzania & Zanzibar