When I set out to build KupwaraCart, I knew a single app wouldn't cut it. A hyperlocal delivery platform isn't just a shopping app — it's an entire ecosystem where customers, sellers, and delivery riders all need to work together in real-time. This article is a technical deep dive into how I architected and built three interconnected mobile applications with a shared backend, real-time communication, and offline-first capabilities.

System Architecture Overview

KupwaraCart is composed of four major components:

The key architectural decision was to build all three apps from a monorepo with maximum code sharing. Let me walk through each layer.

The Monorepo Strategy

Instead of three separate repositories, I used a monorepo structure that allows the three apps to share common code while maintaining their own app-specific logic:

kupwaracart/
├── packages/
│   ├── shared/              # Shared across all apps
│   │   ├── api/             # API client, interceptors
│   │   ├── components/      # Reusable UI components
│   │   ├── hooks/           # Custom React hooks
│   │   ├── store/           # Shared state management
│   │   ├── types/           # TypeScript type definitions
│   │   └── utils/           # Helper functions
│   │
│   ├── customer-app/        # Customer-facing app
│   │   ├── screens/
│   │   ├── navigation/
│   │   └── app.json
│   │
│   ├── seller-app/          # Seller dashboard app
│   │   ├── screens/
│   │   ├── navigation/
│   │   └── app.json
│   │
│   └── rider-app/           # Delivery rider app
│       ├── screens/
│       ├── navigation/
│       └── app.json
│
└── backend/                 # Django backend
    ├── accounts/
    ├── catalog/
    ├── orders/
    ├── delivery/
    └── payments/

This approach gave me roughly 80% code reuse across the three apps. The shared package contains the API client, authentication logic, WebSocket manager, common UI components (buttons, cards, modals, inputs), and TypeScript type definitions that mirror the backend's serializer schemas.

Backend Architecture: Django REST Framework

The backend is a Django application with Django REST Framework (DRF) handling the API layer. Here's how the major Django apps are structured:

Authentication: JWT with Refresh Tokens

I use djangorestframework-simplejwt for token-based authentication. Each user type (customer, seller, rider) is a single User model with a role field. Permissions are enforced through custom DRF permission classes:

class IsSeller(BasePermission):
    def has_permission(self, request, view):
        return (request.user.is_authenticated 
                and request.user.role == 'seller')

class IsRider(BasePermission):
    def has_permission(self, request, view):
        return (request.user.is_authenticated 
                and request.user.role == 'rider')

Order Lifecycle: State Machine Pattern

The order model uses a state machine pattern with well-defined transitions. An order moves through these states:

PENDING → ACCEPTED → PREPARING → READY_FOR_PICKUP 
→ RIDER_ASSIGNED → PICKED_UP → IN_TRANSIT → DELIVERED

# Alternative flows:
PENDING → REJECTED (by seller)
PENDING → CANCELLED (by customer)
ANY_STATE → FAILED (system error)

Each state transition triggers side effects — push notifications to the relevant user, inventory updates, analytics events, and real-time WebSocket broadcasts.

Geospatial Features: PostGIS

PostgreSQL with the PostGIS extension powers all location-based features. Store locations, delivery addresses, and rider positions are stored as PointField types, enabling queries like "find all stores within 5km of the customer" with:

from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point

user_location = Point(74.2543, 34.5297, srid=4326)

nearby_stores = Store.objects.annotate(
    distance=Distance('location', user_location)
).filter(
    distance__lte=D(km=5)
).order_by('distance')

Real-Time Communication

Real-time updates are the heartbeat of a delivery platform. When a customer places an order, the seller needs to know immediately. When the rider moves, the customer needs to see live tracking.

I implemented real-time communication using Django Channels with WebSocket connections. Each app maintains a persistent WebSocket connection to the backend, subscribing to relevant channels:

On the React Native side, I created a custom useWebSocket hook that handles connection management, automatic reconnection with exponential backoff, and message parsing:

const useWebSocket = (channel: string) => {
  const [messages, setMessages] = useState([]);
  const wsRef = useRef<WebSocket | null>(null);
  const retryCount = useRef(0);

  const connect = useCallback(() => {
    const ws = new WebSocket(
      `wss://api.kupwaracart.in/ws/${channel}/`
    );
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setMessages(prev => [...prev, data]);
    };
    
    ws.onclose = () => {
      // Exponential backoff reconnection
      const delay = Math.min(
        1000 * Math.pow(2, retryCount.current), 
        30000
      );
      setTimeout(connect, delay);
      retryCount.current++;
    };
    
    wsRef.current = ws;
  }, [channel]);

  useEffect(() => { connect(); }, [connect]);
  
  return messages;
};

Offline-First Architecture

Given Kashmir's unreliable internet connectivity, offline support wasn't a nice-to-have — it was essential. I implemented an offline-first strategy using:

The sync mechanism uses a conflict resolution strategy based on timestamps. When a queued action conflicts with a server-side change (e.g., a product's price changed while the user was offline), the server's version wins, and the user is notified.

Push Notifications Architecture

Push notifications are critical for a delivery app. I use Expo Notifications combined with Firebase Cloud Messaging (FCM) for Android and APNs for iOS. The backend sends notifications through a unified notification service:

# notifications/service.py
class NotificationService:
    @staticmethod
    def send_order_update(order, status):
        templates = {
            'accepted': {
                'title': 'Order Accepted! 🎉',
                'body': f'{order.store.name} is preparing your order'
            },
            'picked_up': {
                'title': 'Order Picked Up 🚴',
                'body': f'Your rider is on the way!'
            },
            'delivered': {
                'title': 'Delivered! ✅',
                'body': f'Enjoy your order from {order.store.name}'
            }
        }
        
        template = templates.get(status)
        if template:
            push_notification(
                token=order.customer.push_token,
                **template,
                data={'order_id': str(order.id)}
            )

Performance Optimizations

To keep the apps fast and responsive, even on lower-end devices common in our market:

Deployment & DevOps

The deployment pipeline is designed for rapid iteration:

Key Technical Takeaways

If you're building a similar multi-app ecosystem, here are my key recommendations:

  1. Invest heavily in shared code infrastructure. The time spent setting up the monorepo paid for itself 10x over.
  2. Design your state machine first. The order lifecycle state machine is the core of the business logic. Get it right before writing any code.
  3. Build offline-first from day one. Retrofitting offline support is exponentially harder than building it in from the start.
  4. Type everything. TypeScript on the frontend, Pydantic/DRF serializers on the backend. Shared type definitions prevent an entire class of bugs.
  5. Monitor everything. You can't fix what you can't see. Logging, error tracking, and performance monitoring are not optional.

Building KupwaraCart has been the most challenging and rewarding technical project of my career. If you have questions about the architecture or want to discuss similar projects, feel free to reach out on GitHub or LinkedIn.