JST --:--:--
arrow_back My Work
01 Food Ordering Platform

Akbar

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

Visit Live Site arrow_outward
Akbar tandoor mixed grill
4 User roles
5 External API integrations
SSE Real-time order streaming
Live Production at akbar.co.jp

Stack

Backend
Python 3 Flask SQLAlchemy Flask-Migrate Flask-Login Flask-WTF Flask-Mail
Frontend
Jinja2 Vanilla JS Vanilla CSS HTML5 Stripe.js
Real-time
Server-Sent Events (SSE) Background Thread
Auth & Payments
Twilio Verify Stripe PBKDF2-SHA256 CSRF (Flask-WTF)
Infrastructure
AWS EC2 Nginx Gunicorn PostgreSQL AWS S3 Certbot / SSL
Integrations
Zoho Mail (SMTP) Nager.Date API boto3 Pillow pandas pytz
バックエンド
Python Flask
フロントエンド
HTML5 CSS3 JavaScript
認証・決済
Stripe (オンライン決済) Twilio (SMS認証)
インフラ・保存先
AWS (クラウドサーバー) PostgreSQL (データベース)

Architecture

account_tree

Request path

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.

stream

Real-time via Server-Sent Events

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.

payments

Stripe payment flow

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.

phone_iphone

Phone-based authentication

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.

manage_accounts

Four-tier user system

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).

cloud_upload

Media and storage

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.

filter_alt

Smart menu filtering

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.

mail

Spam-filtered contact form

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.

Key flows

person_add

Customer registration

  1. 1Customer enters name, Japanese phone number, password
  2. 2Server formats phone to E.164 (+81…), checks for duplicates
  3. 3Account created; Twilio Verify API sends 6-digit SMS
  4. 4Customer enters the OTP on the verify screen
  5. 5Flask validates code via Twilio Checks API
  6. 6Phone verified → customer logged in, redirected to menu
shopping_cart_checkout

Checkout & payment

  1. 1Customer selects delivery or pickup, time slot, payment method
  2. 2For card/PayPay: Flask creates a Stripe PaymentIntent
  3. 3Stripe.js handles card input in the browser — details never reach Flask
  4. 4On success: browser calls /store_order_details, Flask saves order and clears cart
  5. 5For cash on delivery: order saved immediately, no Stripe call
  6. 6Customer redirected to payment success page
receipt_long

Admin order management

  1. 1Admin opens /admin/orders; browser connects to SSE stream
  2. 2New orders appear live without page refresh
  3. 3Admin clicks status buttons → AJAX updates database
  4. 4Status flow: pending → preparing → ready → delivered / completed / cancelled
  5. 5Background thread auto-delivers after 30 min in ready state
  6. 6Cancellation triggers automatic Stripe refund
restaurant_menu

Menu management

  1. 1Super admin logs in at /control-center with 4-digit passcode
  2. 2Adds, edits, or deletes categories and menu items
  3. 3Images uploaded to AWS S3 via boto3
  4. 4Add-on groups (spice levels, drinks) configured per category or item
  5. 5Drag-and-drop reordering updates position field via AJAX
  6. 6Item availability toggled: available / sold out today / sold out

Highlights

stream

Real-time via SSE

Live 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.

phone_iphone

Phone OTP signup

SMS-verified registration via Twilio Verify API. Phones formatted to E.164 before the Twilio call. No SMS required for subsequent logins.

credit_card

Stripe + PayPay

Stripe PaymentIntent for credit cards and PayPay. Card details handled by Stripe.js — never reach the server. Unique UUID prevents duplicate charges on retries.

tune

Cart with full customisation

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.

admin_panel_settings

Full admin control centre

Super admin panel with drag-and-drop menu reordering, availability toggling, S3 image uploads, site blocking, and CSV export via pandas.

cloud

Production cloud deployment

AWS EC2 with Nginx + Gunicorn (2–6 workers). HTTPS via Certbot. Media on S3 (Tokyo region). Live at akbar.co.jp serving real customers.

Data model

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)

Deployment & security

Production stack

  • AWS EC2 instance (inferred from Nginx config path)
  • Nginx — reverse proxy, rate limiting, HTTPS
  • Gunicorn — 2–6 workers, internal port 8000
  • PostgreSQL — all data including Flask sessions table
  • Certbot — SSL certificate with auto-renewal script
  • AWS S3 — images and PDFs, Tokyo region
  • Domain: akbar.co.jp (confirmed in nginx.conf)

Authentication

  • Customers: phone + password (PBKDF2-SHA256, 310,000 iterations)
  • Admins: email + password, same hash algorithm
  • Super admin: 4-digit passcode
  • Delivery boys: PIN number
  • Sessions stored in PostgreSQL, last 31 days
  • CSRF protection via Flask-WTF on all forms

Payment security

  • Card numbers never reach the Flask server
  • Stripe PaymentIntent API (industry standard approach)
  • Unique reference UUID per order prevents duplicate charges
  • Stripe commission (3.6%) tracked separately in dashboard
  • Auto-refund on cancellation via Stripe Refunds API

Environment variables

  • DB_USER, DB_PASSWORD, DB_HOST, DB_NAME, DB_PORT
  • SECRET_KEY (Flask session signing)
  • STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY
  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET
  • TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_VERIFY_SERVICE_ID
  • MAIL_USERNAME, MAIL_PASSWORD (Zoho SMTP)