A full-stack food ordering and delivery management system for a real Indian restaurant chain in Japan. Customers browse the menu, customise their order with spice levels and add-ons, and pay securely online. Restaurant staff see incoming orders in real time — no page refresh needed — and manage the entire operation from a dedicated admin panel.
Live at akbar.co.jp.
日本のインドレストランチェーン向けのフルスタック注文・配達管理システムです。お客様はメニューを閲覧し、辛さやトッピングを選んで安全にオンライン決済できます。スタッフはページを更新せずにリアルタイムで注文を確認・管理できます。
公開中のサイト:akbar.co.jp
Browser → Nginx (reverse proxy, HTTPS, rate limiting, static caching) → Gunicorn (2–6 worker processes on port 8000) → Flask (Blueprints: main, auth, admin, homepage) → PostgreSQL via SQLAlchemy | AWS S3 for media | External APIs (Stripe, Twilio, Zoho, Nager.Date).
Nginx has a special configuration for the SSE endpoint (/order_stream): buffering is disabled and HTTP/1.1 is enforced so the stream reaches the admin browser without delay.
The admin browser holds a persistent HTTP connection to /order_stream. Flask streams today's orders as JSON every few seconds using SSE — no WebSocket, no Socket.IO. A background threading.Thread runs every 60 seconds and auto-marks delivery orders as delivered after 30 minutes in ready state, preventing orders from getting stuck.
Flask creates a Stripe PaymentIntent. Stripe.js collects card details in the browser — the card number never touches the Flask server. A unique reference UUID per order prevents duplicate charges on network retries. Stripe commission (3.6%) is calculated and tracked separately in the revenue dashboard. Admin cancellations trigger automatic stripe.Refund.create() calls.
Customers register with a Japanese phone number (formatted to E.164) and password. Flask calls the Twilio Verify API to send a 6-digit SMS OTP. After the customer confirms the code, is_phone_verified is set to True and they are logged in. Sessions are stored in PostgreSQL and last 31 days. Returning customers log in with phone + password only — no SMS needed.
Four distinct user types, each with separate login flows and route-level decorators: Customers (phone + password + Twilio OTP on first registration), Admins (email + password), Delivery boys (PIN — simplified login for delivery staff), and a Super admin (4-digit passcode giving full system control including menu management, site blocking, and user management).
Menu item images and PDF menus are stored in an AWS S3 bucket in the Tokyo region (ap-northeast-1). Uploads are handled server-side via boto3 through utils/s3_utils.py. The Super Admin can drag-and-drop reorder categories and items — position is updated via AJAX. Admins can toggle item availability between available, sold out today, and sold out.
The lunch category is hidden on weekends by default. The app queries the Nager.Date API to detect Japanese national holidays and adjusts lunch visibility accordingly. Admins can override this toggle manually. All categories, items, and add-ons are loaded in a single optimised database query using eager loading for speed.
The public contact form detects whether a message contains Japanese characters via regex. If the message is Japanese, it is sent via Zoho Mail SMTP (smtp.zoho.in). If English-only, a success response is shown but no email is sent — a deliberate spam filter to block automated English-language bots while staying functional for real Japanese customers.
/store_order_details, Flask saves order and clears cart/admin/orders; browser connects to SSE stream/control-center with 4-digit passcodeLive order streaming using Server-Sent Events. Nginx configured with no buffering on /order_stream. A background thread auto-delivers orders after 30 minutes in ready state.
SMS-verified registration via Twilio Verify API. Phones formatted to E.164 before the Twilio call. No SMS required for subsequent logins.
Stripe PaymentIntent for credit cards and PayPay. Card details handled by Stripe.js — never reach the server. Unique UUID prevents duplicate charges on retries.
Each cart item keyed by item_id + spicy_level + addon_hash. Same dish with different options stored separately. Cart stored server-side in Flask sessions.
Super admin panel with drag-and-drop menu reordering, availability toggling, S3 image uploads, site blocking, and CSV export via pandas.
AWS EC2 with Nginx + Gunicorn (2–6 workers). HTTPS via Certbot. Media on S3 (Tokyo region). Live at akbar.co.jp serving real customers.
| Table | What it stores |
|---|---|
| CustomerLogin | Phone number (unique), name, email, hashed password, address fields, verification status, order count |
| Admin | Restaurant admin accounts — email, PBKDF2-SHA256 hashed password (310,000 iterations), passcode |
| SuperAdmin | Top-level admin — email and 4-digit passcode. One row expected |
| DeliveryBoy | Delivery staff — name and PIN for simplified login |
| Category | Food categories with position ordering, optional time restriction, and spicy level flag |
| MenuItem | Individual items — name, description, price, S3 image URL, availability, spice options, position, order count |
| AddonCategory | Add-on groups (e.g. "Spice Level", "Drink Choice") scoped per category or per item |
| AddonItem | Individual add-on choices (e.g. "Mild", "Medium Hot") with price. Belongs to an AddonCategory |
| Order | Placed order — links to customer, status, total, payment method, Stripe payment_intent_id, delivery method, scheduled time |
| OrderItem | Items within an order — quantity, price at time of order, spicy level, special instructions, add-ons stored as JSON |
| Settings | Restaurant settings — name, address, phone, opening hours, admin email. One row |
| TodayCurry / MonthlyCurry | CMS data: today's curry name and the monthly special (name, S3 image URL, price) |