A full-stack apartment grocery platform built with Django and Next.js. A single vendor serves residents of an apartment complex — customers browse products, place orders, and pay via UPI. The vendor manages inventory, finalizes bills, and coordinates delivery. A delivery user handles fulfillment. An admin dashboard provides analytics and oversight.
| Layer | Technology |
|---|---|
| Backend | Django 5.1, Django REST Framework, PostgreSQL 15 |
| Frontend | Next.js 14 (App Router), React 18, TypeScript, Tailwind CSS |
| Auth | JWT via djangorestframework-simplejwt (access: 30 min, refresh: 7 days) |
| Data Fetching | SWR (stale-while-revalidate) on the frontend |
| Containerization | Docker Compose (dev), Docker + Nginx + Gunicorn (prod) |
| Deployment | Frontend on Vercel, Backend on DigitalOcean, DNS/SSL via Cloudflare |
Browser (port 3000)
|
| /api/* and /media/* requests
v
Next.js ──rewrites──> Django (port 8000) ──> PostgreSQL
- Next.js acts as a proxy. The
next.config.jsrewrites/api/:path*tohttp://backend:8000/api/:path/and/media/:path*tohttp://backend:8000/media/:path*. The browser never talks to Django directly. - Why this matters: Django's
build_absolute_uri()returnshttp://backend:8000/...(the Docker internal hostname), which browsers can't reach. So serializers always return relative URLs like/media/products/image.jpg. - JWT tokens are stored in
localStorage(lf_access,lf_refresh). The frontend auto-refreshes on 401 and redirects to/loginif the refresh fails.
Custom user model with four roles. Extra fields: user_type, phone, flat_number.
user_type: customer | vendor | admin | delivery
Belongs to a vendor (FK to User). Fields: name, category (vegetables/fruits/dairy/staples/other), price (nullable — vendor can list without price), unit (kg/gm/piece/pack/etc.), is_available, image, description.
No stock tracking — availability is a simple boolean toggle.
OneToOne with User. Fields: store_name, category, apartment_code, is_active.
One cart per customer (OneToOne). CartItems link to products with a quantity. unique_together = ['cart', 'product'] prevents duplicates.
Orders have a human-readable order_number (format: ORD-YYYYMMDD-NNN). Each order belongs to a customer and a vendor. OrderItems snapshot the product, quantity, and price at the time of ordering.
After the customer places an order, the vendor can edit the bill — adjusting quantities, prices, or adding custom items not in the original order. These are stored as FinalBillItems. The order's total is recalculated from these when the bill is sent.
Image posts from the vendor. Auto-expire after 24 hours. Max 3 active at a time.
User ──1:1──> Vendor
User ──1:1──> Cart ──1:N──> CartItem ──N:1──> Product
User ──1:N──> Order ──1:N──> OrderItem ──N:1──> Product
──1:N──> FinalBillItem
Vendor ──1:N──> VendorUpdate
Product.vendor ──FK──> User (not Vendor)
Note: Product.vendor is a FK to User, not to Vendor. To get the store name: product.vendor.vendor.store_name (User -> Vendor -> store_name).
Four custom permission classes in accounts/permissions.py: IsVendor, IsAdmin, IsCustomer, IsDelivery.
| Role | Can Do |
|---|---|
| Customer | Browse products, manage cart, place/cancel/reorder orders, view order status, pay via UPI |
| Vendor | Manage inventory (CRUD products, toggle availability), view incoming orders, edit & send bills, update order status, post stories |
| Delivery | View orders ready for pickup, mark orders as picked up and delivered |
| Admin | View analytics (revenue, order counts, top products), manage users, view all orders |
placed ──> bill_sent ──> out_for_delivery ──> delivered ──> paid
|
└──> cancelled
| Transition | Who | What Happens |
|---|---|---|
placed -> bill_sent |
Vendor | Vendor finalizes the bill (FinalBillItems created, total recalculated) |
bill_sent -> out_for_delivery |
Delivery | Delivery user picks up the order |
out_for_delivery -> delivered |
Delivery | Delivery user confirms delivery, delivered_at timestamp set |
delivered -> paid |
Vendor | Vendor marks payment as received |
placed -> cancelled |
Customer | Only possible while still in placed status |
Constraints:
- Max 2 active orders per customer (enforced on creation)
- UPI payment link is generated for statuses
bill_sent,out_for_delivery, anddelivered - Order number generation retries up to 5 times on collision (race condition safety)
All endpoints are under /api/. Auth endpoints require JWT unless marked public.
| Method | Endpoint | Description |
|---|---|---|
| POST | register/ |
Create account (public) |
| POST | login/ |
Get JWT tokens (public) |
| POST | token/refresh/ |
Refresh access token (public) |
| GET | me/ |
Current user info |
| POST | change-password/ |
Change password |
| Method | Endpoint | Description |
|---|---|---|
| GET | products/ |
List all products (public, filterable by category, searchable) |
| GET | products/{id}/ |
Single product (public) |
| POST | products/ |
Create product (vendor) |
| PATCH | products/{id}/ |
Update product (vendor) |
| DELETE | products/{id}/ |
Delete product (vendor) |
| GET | products/categories/ |
List categories with counts (public) |
| Method | Endpoint | Description |
|---|---|---|
| GET | cart/ |
Get cart with items and totals |
| POST | cart/add/ |
Add item (upserts if exists) |
| PATCH | cart/update/{item_id}/ |
Update quantity |
| DELETE | cart/remove/{item_id}/ |
Remove item |
| DELETE | cart/clear/ |
Empty cart |
| GET | cart/validate/ |
Check item availability |
All mutating cart endpoints return the full updated Cart object.
| Method | Endpoint | Description |
|---|---|---|
| POST | orders/create/ |
Create order from cart |
| GET | orders/ |
List orders (paginated) |
| GET | orders/{order_number}/ |
Order detail |
| POST | orders/{order_number}/cancel/ |
Cancel order |
| POST | orders/{order_number}/reorder/ |
Re-add items to cart |
| Method | Endpoint | Description |
|---|---|---|
| GET | vendor/orders/ |
List vendor's orders (filterable by status, date, sortable by block) |
| GET | vendor/orders/{order_number}/ |
Order detail for packing |
| PATCH | vendor/orders/{order_number}/status/ |
Update order status |
| PATCH | vendor/orders/{order_number}/send-bill/ |
Finalize and send bill |
| PATCH | vendor/orders/{order_number}/mark-paid/ |
Mark as paid |
| GET | vendor/orders/new-count/ |
Count of unseen orders |
| POST | vendor/orders/mark-seen/ |
Mark all orders as seen |
| GET/POST | vendor/updates/ |
List/create vendor stories |
| DELETE | vendor/updates/{id}/ |
Delete a story |
| Method | Endpoint | Description |
|---|---|---|
| GET | delivery/orders/ |
Orders in bill_sent + out_for_delivery |
| PATCH | delivery/orders/{order_number}/pickup/ |
Mark picked up |
| PATCH | delivery/orders/{order_number}/deliver/ |
Mark delivered |
| Method | Endpoint | Description |
|---|---|---|
| GET | admin/analytics/ |
Dashboard stats, daily breakdown, top products |
| GET | admin/users/ |
All users (searchable) |
| GET | admin/orders/ |
All orders (filterable by status) |
| Method | Endpoint | Description |
|---|---|---|
| GET | updates/ |
Active vendor stories (no auth required) |
src/app/
├── (customer)/ # Customer pages — maps to /, /cart, /orders, /profile, /products/[id]
├── vendor/ # Vendor pages — /vendor/orders, /vendor/inventory
├── admin/ # Admin pages — /admin/dashboard, /admin/users, /admin/orders
├── delivery/ # Delivery page — /delivery
├── login/ # /login
└── register/ # /register
Each route group has its own layout.tsx with:
- Auth guard — redirects to
/loginif unauthenticated, or to the correct dashboard if wrong role - Navigation — mobile bottom nav + desktop top/side nav
AuthContext — JWT user state. Provides login(), logout(), refreshUser(), user, isAuthenticated, isLoading.
CartContext — SWR-powered cart state. Only fetches for authenticated customers. Provides addItem(), updateItem() (optimistic), removeItem() (optimistic), clearCart(), mutateCart().
ToastContext — Notification system. toast(message, type) shows a 3.5-second auto-dismissing notification.
Exports: get, post, patch, del, postForm, patchForm, fetcher.
- All JSON methods go through a shared
request()function that auto-attaches the JWT and handles 401 refresh. postForm/patchFormare separate implementations forFormData(file uploads) — they don't setContent-Typeso the browser can set the multipart boundary.fetcheris a SWR-compatible wrapper aroundget.
- SWR everywhere — all data fetching uses
useSWRwith thefetcherfromapi.ts - Optimistic updates — cart updates and removals update local state before the API call, reverting on error
- Mobile-first — single
mdbreakpoint (768px), bottom nav on mobile, top/side nav on desktop - Touch-friendly — all interactive elements have
min-h-[44px] - Design system — emerald as the primary color, reusable UI components (Button, Card, Modal, Input, Badge, Spinner)
cd localforce-backend
docker compose up # starts db, backend (port 8000), frontend (port 3000)Required env vars in localforce-backend/.env:
LOCALFORCE_DB_PASSWORD=<your_password>
VENDOR_UPI_ID=<upi_id>
VENDOR_NAME=<vendor_display_name>
docker compose exec backend python manage.py migrate
docker compose exec backend python manage.py createsuperuser
docker compose exec backend python manage.py cleanup_expired_updates# Backend
cd localforce-backend
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver # port 8000
# Frontend (separate terminal)
cd frontend
npm install
npm run dev # port 3000-
No stock tracking — Products have a simple
is_availabletoggle. Stock quantity was removed to keep things simple for the vendor. -
Billing flow — The vendor doesn't just confirm orders. They can edit quantities, adjust prices, and add custom items before sending the bill. This is the
FinalBillItemmodel. The originalOrderItemrecords are preserved for reference. -
UPI payment — Payment happens outside the app. The backend generates a
upi://pay?...intent URI that opens the customer's UPI app. No payment verification or webhook — the vendor manually marks orders as paid. -
Relative image URLs — Because Next.js proxies to Django, and Django runs behind Docker as
backend:8000, serializers return/media/products/image.jpginstead of absolute URLs. -
Product.vendor FK points to User, not Vendor — This means getting the store name requires
product.vendor.vendor.store_name. TheVendormodel is accessed via the OneToOne reverse relation. -
Order numbers — Human-readable format
ORD-YYYYMMDD-NNNwith retry logic for race conditions instead of UUIDs. -
No tests — Changes are verified by running the app manually. The
building_documents/folder contains manual test plans. -
URL ordering matters — In
store/urls.py, static vendor paths (/vendor/orders/new-count/) must appear before the parametric<order_number>path to avoid routing conflicts.
docs/frontend-reference.md— full breakdown of the current frontend: pages, components, state, styling conventionsdocs/frontend-redesign-spec.md— design spec for an in-progress visual redesign