A tablet-based food calling and kitchen management system designed to streamline operations at Panda Express restaurants. This system automates the food calling process and helps coordinate between table sections and kitchen staff.
- Create AdonisJS backend - Basic project structure
β COMPLETED
- Setup Postgres database - Configuration and migrations
β COMPLETED
- Create models - MenuItem and Order with Lucid ORM
β COMPLETED
- Create API endpoints - All REST API for the system
β COMPLETED
- Create seeders - 4 Panda Express dishes with data
β COMPLETED
- β Backend: AdonisJS 6.19.0 project created and working
- β Database: Postgres configured, migrations executed successfully (Postgres only)
- β Models: MenuItem and Order models created with relationships
- β API Endpoints: All REST API endpoints created and working
- β Seeders: 5 Panda Express dishes populated in database (including 10-second test item)
- β CSRF: Disabled for API routes, POST requests working
- β Frontend: Next.js 15.5.2 with TypeScript and Tailwind CSS
- β Table Interfaces: 3 table section pages with smart batch selection
- β Kitchen Interface: Kitchen tablet with timer management
- β BOH Interfaces: BOH pages (including settings)
- β WebSocket: Real-time communication between kitchen and table sections
- β Timer System: Complete timer workflow with countdown displays
- β Order Management: Full order lifecycle from creation to completion
- β Smart UI: Time-based batch recommendations and visual feedback
- β Deployment: Live on DigitalOcean
- β Environment Config: Frontend configured to connect to server backend via env
backend/app/models/menu_item.ts
- MenuItem model with cooking timesbackend/app/models/order.ts
- Order model with timer fieldsbackend/database/migrations/1756870400000_create_menu_items_table.ts
backend/database/migrations/1756870400001_create_orders_table.ts
backend/database/seeders/menu_item_seeder.ts
- 5 Panda Express dishes (including 10-second test item)backend/app/controllers/menu_item_controller.ts
- Menu API endpointsbackend/app/controllers/order_controller.ts
- Order API endpoints with WebSocket eventsbackend/app/controllers/kitchen_controller.ts
- Kitchen API endpointsbackend/app/controllers/status_controller.ts
- Status API endpointsbackend/app/services/websocket_service.ts
- Real-time WebSocket communicationbackend/start/routes.ts
- All API routes configuredbackend/start/ws.ts
- WebSocket server configurationbackend/config/shield.ts
- CSRF disabled for APIfrontend/src/app/page.tsx
- Main navigation pagefrontend/src/app/table/[id]/page.tsx
- Smart table section interfaces with timersfrontend/src/app/boh/page.tsx
- BOH interfacefrontend/src/app/boh/settings/page.tsx
- BOH settings pagefrontend/src/contexts/WebSocketContext.tsx
- WebSocket connection managementfrontend/src/hooks/useWebSocketEvents.ts
- WebSocket event handlersfrontend/package.json
- Next.js 15.5.2 with TypeScript and Socket.IOfrontend/tailwind.config.ts
- Tailwind CSS configurationfrontend/.env.local
- Environment configuration for server deployment
- One step at a time - Complete each step before moving to next
- Test after each step - Verify everything works before proceeding
- Never edit working code - Don't break existing functionality
- Always work in /backend - For all backend terminal commands
- No terminal file operations - Use direct file tools only
- β
Install and configure Chakra UI(Switched to Tailwind CSS) - β
Convert existing interfaces to Chakra UI components(Using Tailwind CSS) - β
Implement timer logic for kitchen orders(Complete timer system implemented) - β
Add real-time timer countdown displays(WebSocket-based real-time updates) - β
Integration testing- All components tested and working together - β
Deployment setup- DigitalOcean with Ubuntu (137.184.15.223) - β³ Production optimization - Performance tuning and monitoring
- β³ User testing - Real-world tablet testing in restaurant environment
- β³ Feature enhancements - Additional menu items, reporting, analytics
- β³ Mobile optimization - Responsive design improvements for various tablet sizes
- AdonisJS Version: 6.19.0
- Next.js Version: 15.5.2
- Database: Postgres with Lucid ORM (Postgres only)
- WebSocket: Socket.IO for real-time communication
- Backend Port: 3333
- Frontend Port: 3000
- Server: DigitalOcean Droplet
- Database: Local Postgres (Homebrew/Docker) during development
- Migration Status: All 4 migrations completed successfully
- API Status: All endpoints working with WebSocket events
- Frontend Status: Running with Tailwind CSS and WebSocket integration
- Deployment Status: Live and accessible via configured domain/IP
- Environment Config: Frontend configured with server API URLs from env
- Test Data: 5 Panda Express dishes (including 10-second test item)
- β GET /api/menu-items: Returns 5 Panda Express dishes (including 10-second test item)
- β GET /api/orders: Returns orders with real-time updates
- β POST /api/orders: Creates new orders with WebSocket events
- β DELETE /api/orders/:id: Deletes orders with WebSocket events
- β POST /api/kitchen/orders/:id/start-timer: Starts timers with WebSocket events
- β POST /api/kitchen/orders/:id/complete: Completes orders with WebSocket events
- β WebSocket Events: Real-time communication between kitchen and table sections
- β Database: All data persisted correctly with relationships
- β
Menu Items: GET, PUT
/api/menu-items
- β
Orders: GET, POST, PUT, DELETE
/api/orders
- β
Table Sections: GET
/api/table-sections
- β
Kitchen: GET, POST
/api/kitchen/orders
- β
System Status: GET
/api/status
backend/
βββ app/models/ # Models (Lucid) β
βββ app/controllers/ # API controllers β
βββ database/migrations/ # schema (Postgres) β
βββ database/seeders/ # seeders (incl. IDP/Performance) β
βββ start/routes.ts # API routes β
βββ config/shield.ts # CSRF configuration β
βββ (no SQLite) # Postgres only β
frontend/
βββ src/app/ # Next.js App Router β
β βββ page.tsx # Main navigation β
β βββ table/[id]/ # Table section pages β
β βββ kitchen/ # Kitchen page β
βββ package.json # Next.js 15.5.2 + TypeScript β
βββ tailwind.config.ts # Tailwind CSS config β
βββ postcss.config.mjs # PostCSS config β
.cursor/
βββ mcp.json # MCP server configuration β
ΠΡΠ°ΠΏ 1: Create AdonisJS backend
- Create AdonisJS project:
npm create adonisjs@latest backend
- Setup folder structure: controllers, models, services, middleware, validators
- Configure package.json: dependencies and scripts
- Create .env file: database configuration
ΠΡΠ°ΠΏ 2: Setup Postgres database
- Configure Postgres in
backend/config/database.ts
- Setup
.env
withPG_*
variables - Test connection: run migrations
ΠΡΠ°ΠΏ 3: Create models β COMPLETED
- β Create MenuItem model: fields for dishes and cooking times
- β Create Order model: fields for orders and timers
- β Setup relationships: Order belongsTo MenuItem
- β Create migrations: menu_items and orders tables
- β Test database: migrations executed successfully
ΠΡΠ°ΠΏ 4: Create API endpoints β COMPLETED
- β Create controllers: MenuItemController, OrderController, KitchenController, StatusController
- β Setup routes: all API endpoints from README
- β Fix CSRF: disabled for API routes
- β Test endpoints: GET/POST requests working
ΠΡΠ°ΠΏ 5: Create seeders β COMPLETED
- β Create MenuItem seeder: 4 Panda Express dishes
- β Add test data: correct cooking times (2-4 minutes)
- β Setup seeder execution: database populated successfully
- β Test data: 4 dishes + 1 test order created
ΠΡΠ°ΠΏ 6: Create Next.js frontend β COMPLETED
- β Create Next.js 15.5.2 project with TypeScript
- β Setup Tailwind CSS configuration
- β Fix PostCSS configuration for Tailwind
- β Create main navigation page with tablet links
ΠΡΠ°ΠΏ 7: Create table section interfaces β COMPLETED
- β
Create dynamic route
/table/[id]
for 3 table sections - β Implement menu items display with batch size buttons
- β Add order creation functionality
- β Implement 5-second polling for real-time updates
ΠΡΠ°ΠΏ 8: Create kitchen interface β COMPLETED
- β
Create kitchen page
/kitchen
- β Display all orders with status indicators
- β Add timer start/complete functionality
- β Implement order status management
ΠΡΠ°ΠΏ 9: UI with Tailwind CSS + shadcn/ui β COMPLETED
- β Tailwind CSS configured and applied across the app
- β
Components built using shadcn/ui (
https://ui.shadcn.com/docs
) - β Responsive, tablet-optimized design
- Create Next.js frontend - Basic structure with TypeScript and native HTML
β COMPLETED
- Create table section interfaces - 3 manager tablets with native HTML
β COMPLETED
- Create kitchen interface - Cook tablet with native HTML
β COMPLETED
- UI with Tailwind + shadcn/ui - Redesigned interfaces with shadcn/ui
β COMPLETED
- Implement polling - 5-second synchronization
β COMPLETED
- Implement timer logic - Cooking statuses and timers
β COMPLETED
- Integration testing - All components testing
β COMPLETED
- Deployment setup - DigitalOcean with Ubuntu
β COMPLETED
β COMPLETED
- Task finishedπ IN PROGRESS
- Currently working onβ³ PENDING
- Not started yetβ BLOCKED
- Waiting for dependencies
- 3 Tablet Interfaces: Dedicated interfaces for each table section
- Smart Batch Selection: Time-based recommendations (Breakfast 5-10am, Lunch 11am-1pm, Dinner 5-7pm)
- Visual Feedback: Color-coded buttons with recommended batch highlighting
- Real-time Timers: Live countdown displays showing cooking progress
- Dedicated Kitchen Tablet: Complete order management interface
- Timer Management: Automatic timer setup based on menu item cooking times
- Order Lifecycle: Full workflow from pending β cooking β timer_expired β ready
- Complete & Delete: Option to complete orders or delete them entirely
- WebSocket Integration: Instant updates between kitchen and table sections
- Event-driven Updates: Order creation, timer start/stop, completion events
- Synchronized Timers: Kitchen and table sections show identical countdowns
- Live Status Updates: Real-time order status changes across all devices
- Time-based Recommendations: System suggests optimal batch based on current time
- Visual States: Different button colors for available, processing, and waiting states
- Touch-optimized: Large buttons designed for tablet use
- Status Indicators: Clear visual feedback for order progress
- Frontend: Next.js 15.5.2 with TypeScript and Tailwind CSS
- Backend: AdonisJS 6.19.0 (Node.js framework)
- Database: PostgreSQL with Lucid ORM (built-in AdonisJS ORM)
- Real-time Communication: Socket.IO WebSocket integration
- Architecture: API-Driven Development with WebSocket events
- Data Synchronization: Real-time WebSocket events + 5-second polling fallback
- Device Support: Touch screen optimized for tablets
- Deployment: Automatic deployment from Git repository
The system consists of 4 tablets total:
- One tablet per table section
- Touch screen interface for food calling
- Manager can control all sections from any tablet
- Updates every 5 seconds when food is called
- Dedicated interface for kitchen staff
- Displays incoming food orders
- Automatic timer setup for food preparation
- Updates every 5 seconds when table sections call food
Before you begin, ensure you have the following installed:
- Node.js (v18 or higher) - required for Next.js 15.5.2
- npm
Note: Use npm exclusively for all commands (Yarn is not used).
This project uses Postgres with Lucid ORM for data storage (Postgres only).
Run Postgres locally (Homebrew or Docker). The app connects using PG_* variables.
This project uses strict runtime validation for backend environment variables and env-driven rewrites in the frontend. All network addresses are configured via env (no hard-coded URLs).
Copy backend/.env.example
to backend/.env
and adjust as needed.
Required variables:
# Application
NODE_ENV=development
HOST=0.0.0.0
PORT=3333
APP_NAME=blimp-backend
APP_URL=http://localhost:3333
APP_KEY=change-me-in-production
LOG_LEVEL=debug
# Sessions
SESSION_DRIVER=cookie
# CORS
CORS_ORIGIN=*
# Database (Postgres)
DB_CLIENT=pg
PG_HOST=127.0.0.1
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=postgres
PG_DB_NAME=blimp
PG_SSL=false
# Websocket / Realtime
WS_CORS_ORIGIN=*
Notes:
- Use a strong, random
APP_KEY
in production. - Postgres is the only supported database in this project.
Create frontend/.env.local
with the following variables:
# API URL for backend requests
NEXT_PUBLIC_API_URL=http://localhost:3333
# WebSocket URL for real-time connections
NEXT_PUBLIC_WS_URL=http://localhost:3333
# Backend URL for Next.js rewrites
NEXT_PUBLIC_BACKEND_URL=http://localhost:3333
Important: All URLs must be configured via environment variables. No hardcoded localhost URLs are used in the code.
-
Clone the repository
git clone <repository-url> cd blimp1
-
Install frontend dependencies
cd frontend npm install
-
Install backend dependencies
cd ../backend npm install
-
Set up environment variables
cp .env.example .env # Edit .env file with your configuration
-
Run database migrations
cd backend node ace migration:run
-
Start the backend server
cd backend npm run dev
Note: On first run, the backend will automatically apply pending migrations and seed initial data (via
predev
). For production runs,npm start
will auto-apply migrations (viaprestart
) but will not seed. -
Start the frontend development server
cd frontend npm run dev
-
Open your browser
- Local Development: Navigate to
http://localhost:3000
- Live Server: Use your configured domain/IP
- Local Development: Navigate to
From the repository root:
chmod +x scripts/bootstrap.sh
./scripts/bootstrap.sh
This will:
- Install dependencies for backend and frontend
- Create
.env
files if missing (using examples or minimal defaults) - Run database migrations and seed data
- Start backend (port 3333) and frontend (port 3000)
# 1. Configure server details
nano deploy.config.sh # Set DEPLOY_SERVER_IP and credentials
# 2. Validate everything is ready
./scripts/validate-deployment.sh
# 3. Deploy!
./scripts/deploy-easy.sh
That's it! β
- Quick Start Guide - Deploy in 5 minutes
- Complete Deployment Guide - Full documentation with troubleshooting
- Old Manual Process - Previous deployment steps (legacy)
# Check server status
./scripts/server-monitor.sh status
# View application logs
./scripts/server-monitor.sh logs
# Update application
./scripts/server-monitor.sh update
# Restart services
./scripts/server-monitor.sh restart
# Database operations
./scripts/server-monitor.sh db
# Health check
./scripts/health-check.sh
- Backend: AdonisJS API on port 3333
- Frontend: Next.js app on port 3000
- Database: PostgreSQL on port 5432
- Process Manager: PM2 for service management
- Reverse Proxy: Nginx for routing
- WebSocket: Real-time communication support
project/
βββ frontend/ # Next.js 15 Frontend
β βββ src/app/ # Next.js App Router
β β βββ table/ # Table section pages
β β βββ boh/ # BOH pages (incl. settings)
β β βββ ...
β βββ src/components/
β βββ src/contexts/
β βββ src/hooks/
β βββ src/lib/
β βββ public/ # Static assets
β βββ package.json # Frontend dependencies
βββ backend/ # AdonisJS 6.19.0 Backend
β βββ app/ # AdonisJS application code
β β βββ controllers/ # API controllers
β β βββ models/ # Database models
β β βββ services/ # Business logic
β β βββ middleware/ # API middleware
β β βββ validators/ # Request validation
β βββ config/ # Configuration files
β βββ database/ # Database migrations and seeders
β βββ tmp/ # Temporary files directory
β βββ package.json # Backend dependencies
βββ README.md # This file
- Uses Tailwind CSS for consistent design and styling
- Components from shadcn/ui only (
https://ui.shadcn.com/docs
) - No other UI kits are used
- Touch-optimized interface for tablet use
- Large, easy-to-tap buttons for kitchen environment
- High contrast design for visibility in restaurant lighting
- Responsive design optimized for tablet screens
This project follows API-Driven Development approach:
- Base URL:
/api
in development (Next.js rewrite) |${NEXT_PUBLIC_API_URL}/api
in production - Authentication: Cookie-based session (httpOnly) after sign-in
- Data Format: JSON
- Polling: Frontend polls API every 5 seconds (plus WebSocket events)
- Environment: All URLs come from env; no hardcoded addresses
POST /api/auth/sign-up-invite # Register using invite code
POST /api/auth/sign-in # Sign in (sets cookie)
POST /api/auth/logout # Logout (clears cookie)
GET /api/auth/me # Current user and memberships
POST /api/invites # Create invite (role-based rules)
GET /api/menu-items # Get all menu items
GET /api/menu-items/:id # Get specific menu item
PUT /api/menu-items/:id # Update menu item status
GET /api/orders # Get all orders
POST /api/orders # Create new order
GET /api/orders/:id # Get specific order
PUT /api/orders/:id # Update order status
DELETE /api/orders/:id # Delete order
GET /api/table-sections # Get all table sections data
GET /api/table-sections/:id # Get specific table section
GET /api/table-sections/:id/orders # Get orders for specific table section
GET /api/kitchen/orders # Get all kitchen orders
GET /api/kitchen/orders/pending # Get pending orders
GET /api/kitchen/orders/cooking # Get cooking orders
POST /api/kitchen/orders/:id/start-timer # Start cooking timer
POST /api/kitchen/orders/:id/cancel-timer # Cancel cooking timer
POST /api/kitchen/orders/:id/complete # Mark order as done
GET /api/status # Get system status
GET /api/status/table-sections # Get all table sections status
GET /api/status/kitchen # Get kitchen status
GET /api/menu-items
Response:
{
"data": [
{
"id": 1,
"item_title": "Fried Rice",
"batch_breakfast": 1,
"batch_lunch": 2,
"batch_downtime": 1,
"batch_dinner": 3,
"cooking_time_batch1": 3,
"cooking_time_batch2": 4,
"cooking_time_batch3": 2,
"status": "available",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z"
}
]
}
POST /api/orders
Content-Type: application/json
{
"table_section": 1,
"menu_item_id": 1,
"batch_size": 2
}
Response:
{
"data": {
"id": 1,
"table_section": 1,
"menu_item_id": 1,
"batch_size": 2,
"status": "pending",
"timer_start": null,
"timer_end": null,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}
GET /api/kitchen/orders
Response:
{
"data": [
{
"id": 1,
"table_section": 1,
"menu_item": {
"id": 1,
"item_title": "Fried Rice",
"cooking_time_batch1": 3,
"cooking_time_batch2": 4,
"cooking_time_batch3": 2
},
"batch_size": 2,
"status": "pending",
"timer_start": null,
"timer_end": null,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
]
}
POST /api/kitchen/orders/1/start-timer
Content-Type: application/json
{
"cooking_time": 3
}
Response:
{
"data": {
"id": 1,
"status": "cooking",
"timer_start": "2024-01-01T12:00:00Z",
"timer_end": "2024-01-01T12:03:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}
POST /api/kitchen/orders/1/complete
Content-Type: application/json
{
"completed_at": "2024-01-01T12:05:00Z"
}
Response:
{
"data": {
"id": 1,
"status": "ready",
"completed_at": "2024-01-01T12:05:00Z",
"updated_at": "2024-01-01T12:05:00Z"
}
}
GET /api/status/table-sections
Response:
{
"data": {
"table_sections": [
{
"id": 1,
"orders": [
{
"id": 1,
"menu_item": {
"id": 1,
"item_title": "Fried Rice"
},
"status": "cooking",
"timer_end": "2024-01-01T12:03:00Z",
"remaining_time": 120
}
]
}
]
}
}
- Frontend: Next.js makes HTTP requests to AdonisJS API
- Backend: AdonisJS serves RESTful API endpoints
- Database: Lucid ORM handles data persistence
- Synchronization: All 4 tablets poll the same API endpoints
No testing framework is used in this project. The development approach focuses on:
- Manual testing during development
- Real-world testing with actual tablet devices
- Direct feedback from restaurant staff during implementation
This project is deployed on a DigitalOcean Droplet running Ubuntu.
- Platform: DigitalOcean Droplet
- OS: Ubuntu
- Server IP: 137.184.15.223
- Deployment: Manual deployment with environment configuration
- Process:
- Code changes pushed to repository
- Files copied to server via SCP
- Environment variables configured
- Services restarted
- Ubuntu 20.04 LTS or higher
- Node.js v18 or higher
- PM2 for process management (recommended)
- Nginx for reverse proxy (recommended)
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
If you have any questions or need help, please open an issue in the repository.
- Database Seeders Documentation - Detailed guide for seeder execution order and dependencies
-
Table Section Tablets (3 tablets)
- Display 4 menu items that should always be available on the table
- Manager sees current status of each dish
- Manager clicks on dish + selects batch size (1, 2, or 3)
- System creates order with status
pending
-
Kitchen Tablet
- Cook sees new order with status
pending
- Cook Actions:
"Start Timer"
- start cooking timer"Cancel Timer"
- cancel timer (if not expired yet)
- Cook sees new order with status
-
Timer Active
- Order status:
cooking
- Kitchen screen: shows countdown timer
- Table section: shows "Cooking" + remaining time
- Order status:
-
Timer Expired
- Order status:
timer_expired
- Kitchen screen: shows "Timer Expired!"
- Cook Action:
"Done"
- confirm completion
- Order status:
-
Completion
- Cook clicks
"Done"
- Order status:
ready
- Table section: shows "Ready"
- Cook clicks
pending
- Order created, waiting for cookcooking
- Timer is runningtimer_expired
- Timer expired, waiting for cook confirmationready
- Dish is readycompleted
- Dish taken from table
- WebSocket Events: Real-time communication between kitchen and table sections
- Polling Fallback: 5-second API polling as backup for updates
- Event Types: order:created, order:updated, timer:started, timer:expired, order:completed, orders:all_deleted
- Room-based Updates: Kitchen and specific table sections receive targeted events
The system manages 5 core Panda Express dishes:
- Batch sizes: Breakfast(1), Lunch(2), Downtime(1), Dinner(3)
- Cooking times: Random 2-4 minutes per batch
- Batch sizes: Breakfast(1), Lunch(2), Downtime(1), Dinner(3)
- Cooking times: Random 2-4 minutes per batch
- Batch sizes: Breakfast(1), Lunch(2), Downtime(1), Dinner(3)
- Cooking times: Random 2-4 minutes per batch
- Batch sizes: Breakfast(1), Lunch(2), Downtime(1), Dinner(3)
- Cooking times: Random 2-4 minutes per batch
- Batch sizes: Breakfast(2), Lunch(4), Downtime(1), Dinner(5)
- Cooking times: Batch 1: 10 seconds (for testing), Batch 2: 3-6 minutes, Batch 3: 4-8 minutes
id
,item_title
,batch_breakfast
,batch_lunch
,batch_downtime
,batch_dinner
cooking_time_batch1
(INTEGER),cooking_time_batch2
(INTEGER),cooking_time_batch3
(INTEGER)status
,created_at
,updated_at
- Data: 5 Panda Express dishes with random cooking times (2-4 minutes) + 10-second test item
id
,table_section
(1,2,3),menu_item_id
,batch_size
,status
timer_start
,timer_end
,completed_at
,created_at
,updated_at
- Test Data: 1 test order (Fried Rice, Table Section 1, Batch Size 2, Status: pending)
- MenuItem Model - with all dish fields and cooking times
- Order Model - with order fields, timers, and relationship to MenuItem
- Database Migrations - executed successfully, tables created
- Seeders - populated with 4 Panda Express dishes
- DB Engine: Postgres (only)
- Driver:
pg
- ORM: Lucid (
@adonisjs/lucid
)
- Config file:
backend/config/database.ts
- Default connection:
pg
// backend/config/database.ts (excerpt)
connection: 'pg',
connections: {
pg: {
client: 'pg',
connection: {
host: env.get('PG_HOST'),
port: env.get('PG_PORT'),
user: env.get('PG_USER'),
password: env.get('PG_PASSWORD'),
database: env.get('PG_DB_NAME'),
ssl: env.get('PG_SSL') ? { rejectUnauthorized: false } : false,
},
migrations: { naturalSort: true, paths: ['database/migrations'] },
},
}
- Declared and validated in
backend/start/env.ts
. - Required/optional keys impacting DB:
PG_HOST
,PG_PORT
,PG_USER
,PG_PASSWORD
,PG_DB_NAME
,PG_SSL
Location: backend/database/migrations
Core app tables:
- Users & auth:
create_users_table
,create_access_tokens_table
,add_role_and_deleted_to_users_table
,add_job_title_to_users_table
- Restaurants & relations:
create_restaurants_table
,add_owner_and_deleted_to_restaurants_table
,create_user_restaurants_table
,create_lead_relations_table
- Invites & audit:
create_invitations_table
,create_audit_log_table
,add_job_title_to_invitations_table
- Menu & orders:
create_menu_items_table
,create_orders_table
, and subsequent enhancements (batch number, soft delete, constraints, foreign keys) - IDP/Performance: roles, competencies, questions, actions, assessments, assessment_answers, descriptions, role_performances, performance_sections, performance_items, user_performance_answers
Notes:
- Natural sort for migrations is enabled.
- Soft delete columns exist for entities like users and restaurants (and orders
deleted_at
). - Global uniqueness on restaurant names is enforced.
Location: backend/database/seeders
# Method 1: Use the ordered script (recommended)
npm run db:seed:ordered
# Method 2: Use ace with specific files
node ace db:seed --files database/seeders/admin_user_seeder,database/seeders/restaurant_seeder,database/seeders/idp_role_seeder,database/seeders/idp_competency_seeder,database/seeders/idp_description_seeder,database/seeders/idp_assessment_seeder,database/seeders/additional_data_seeder
# Method 3: Run individual seeders in order
node ace db:seed --files database/seeders/admin_user_seeder
node ace db:seed --files database/seeders/restaurant_seeder
node ace db:seed --files database/seeders/idp_role_seeder
# ... continue in order
- admin_user_seeder.ts - Creates admin user and tablet user (no dependencies)
- restaurant_seeder.ts - Creates sample restaurants (no dependencies)
- idp_role_seeder.ts - Creates IDP roles (no dependencies)
- idp_competency_seeder.ts - Creates competencies, questions, actions (depends on idp_role_seeder)
- idp_description_seeder.ts - Creates competency descriptions (depends on idp_competency_seeder)
- idp_assessment_seeder.ts - Creates assessments for users (depends on admin_user_seeder + idp_role_seeder)
- additional_data_seeder.ts - Creates additional users and assessments (depends on all previous)
- roles_performance_seeder.ts - Creates performance data (independent)
- pl_question_seeder.ts - Creates P&L questions (independent)
- menu_item_seeder.ts - Creates menu items (independent)
admin_user_seeder.ts
: Creates an admin or baseline userrestaurant_seeder.ts
: Sample restaurant(s)menu_item_seeder.ts
: Baseline menu items for demo/testing- IDP/Performance seeders:
idp_role_seeder.ts
,idp_competency_seeder.ts
,idp_description_seeder.ts
,idp_assessment_seeder.ts
,roles_performance_seeder.ts
additional_data_seeder.ts
: Any extra data needed for local testing
npm run dev
runspredev
: applies pending migrations and seeds in correct ordernpm start
runsprestart
: applies migrations only (no seed)
Each seeder includes dependency checks to prevent errors:
idp_assessment_seeder
checks for users and IDP rolesidp_competency_seeder
checks for IDP rolesidp_description_seeder
checks for IDP competenciesadditional_data_seeder
checks for users, restaurants, and IDP roles
Location: backend/app/models
user.ts
: user fields incl.role
enum; soft delete (deleted_at
)restaurant.ts
:owner_user_id
(black_shirt owner); soft deleteuser_restaurant.ts
: many-to-many membership between users and restaurantslead_relation.ts
: maps ops_lead to black_shirt (circle ownership)invitation.ts
: single-use invite codes with optionalrestaurant_id
audit.ts
: append-only audit log entriesmenu_item.ts
: menu metadata, batch sizes, cooking times, statusorder.ts
: ties table section +menu_item_id
+ batch size + timer fields- IDP/Performance:
idp_*
and performance models (roles, competencies, assessments, answers, sections, items)
Relationship highlights:
- User β Restaurant: many-to-many via
user_restaurants
- Restaurant β User (owner):
owner_user_id
- Ops lead circle:
lead_relations
connects ops_lead β black_shirt - Order β MenuItem:
order.menu_item_id
foreign key; eager loads used in controllers
# From backend/
# Run migrations
node ace migration:run
# Rollback last batch
node ace migration:rollback
# Rerun all migrations from scratch
node ace migration:reset && node ace migration:run
# Seed database (in correct order)
npm run db:seed:ordered
# Seed database (all seeders - may fail due to dependencies)
node ace db:seed
# Seed specific seeder
node ace db:seed --files database/seeders/admin_user_seeder
# Generate a new migration
node ace make:migration add_field_to_table
# Generate a new seeder
node ace make:seeder sample_data
- Reset (dev):
dropdb blimp && createdb blimp
then migrate + seed in order - Backup:
pg_dump blimp > backup.sql
- Restore:
psql blimp < backup.sql
- Full reset with proper seeding:
dropdb blimp && createdb blimp && node ace migration:run && npm run db:seed:ordered
- Keep
APP_KEY
secret and strong - All network addresses are configured via environment variables (no hard-coded URLs)
- Config:
backend/config/database.ts
(pg only) - Env schema:
backend/start/env.ts
(pg only) - Migrations:
backend/database/migrations
- Seeders:
backend/database/seeders
- Models:
backend/app/models
The application uses PostgreSQL as the primary database. Configure your database connection in backend/.env
:
NODE_ENV=development
HOST=0.0.0.0
PORT=3333
APP_NAME=blimp-backend
APP_URL=http://localhost:3333
# Generate: openssl rand -hex 32
APP_KEY=change-me-to-a-long-random-string
LOG_LEVEL=debug
SESSION_DRIVER=cookie
# CORS
CORS_ORIGIN=*
# Database (PostgreSQL)
PG_HOST=127.0.0.1
PG_PORT=5432
PG_USER=postgres
PG_PASSWORD=postgres
PG_DB_NAME=blimp
PG_SSL=false
# Websocket / Realtime
WS_CORS_ORIGIN=*
Note: This README will be updated as the project develops and new features are added.
This project uses role-based access control with invitations. Self-registration without an invite is not allowed.
- admin: full access everywhere
- ops_lead: manages only their circle (their black_shirts and all restaurants owned by those black_shirts)
- black_shirt: manages only their own restaurants and associates
- associate: basic role (future pages)
Notes:
- Hierarchy is fixed: admin > ops_lead > black_shirt > associate
- waiter role is not used
- users: id, email, password, full_name, role (enum)
- restaurants: id, name (globally unique), owner_user_id (black_shirt owner) β soft delete enabled
- user_restaurants: id, user_id, restaurant_id β membership (many-to-many), supports multiple memberships for users
- lead_relations: id, lead_user_id (ops_lead), black_shirt_user_id β defines ops_lead circle
- invitations: id, code (uuid), role (enum), created_by_user_id, restaurant_id (nullable), used_at, expires_at (nullable). Invite is single-use and does not expire by default
- audit_log: id, actor_user_id, action, entity_type, entity_id, payload (json), created_at β records all important actions
- admin: unrestricted
- ops_lead: full control ONLY over their circle:
- circle = all black_shirts linked via lead_relations + all restaurants owned by those black_shirts + associates in those restaurants
- cannot take (reassign) black_shirt from another ops_lead (only admin can)
- black_shirt:
- control limited to restaurants they own and associates within those restaurants
- can create unlimited restaurants they own
- can create associates and attach them to their restaurants
- associate: access only to the restaurants where they have membership (future pages)
Restaurants temporarily without a black_shirt owner are considered managed by admin and the corresponding ops_lead from the original circle.
- Self-register: disabled (always via invite)
- Invite for black_shirt: created by ops_lead (role=black_shirt, restaurant_id=null). After registration, black_shirt creates their restaurants and becomes owner
- Invite for associate: created by black_shirt (role=associate, restaurant_id=required). On registration, membership is created in that restaurant. Multiple memberships are supported
- Invite properties: single-use, no expiration by default (can be extended later)
- Soft delete for users and restaurants (deleted_at)
- Deleting a black_shirt:
- We reassign their restaurants and associates per admin/ops_lead decision
- Option A: transfer restaurants to another black_shirt in the same ops_lead circle
- Option B: keep restaurants without black_shirt (temporarily managed by admin and the same ops_lead)
- Ops_lead cannot take black_shirt from another ops_lead; only admin may reassign lead_relations
- ensureRole(...roles): checks user.role
- ensureRestaurantMembership(restaurantId): ensures the user is member/owner; admin bypasses
- ensureLeadAccess(restaurantId): ensures ops_lead has access because the restaurant owner is their black_shirt
- High-level abilities:
- manageUsersInRestaurant: admin; ops_lead (in circle); black_shirt (only associates in own restaurants)
- manageRestaurants: admin; ops_lead (restaurants in circle); black_shirt (own restaurants)
- AccessControlService centralizes checks:
canManageRestaurants(user, restaurant)
canManageUsersInRestaurant(user, restaurant, targetRole)
isMemberOfRestaurant(user, restaurantId)
- Middleware:
role
: ensures user has one of allowed rolesrestaurantAccess
: ensures access by membership/ownership or ops_lead circle
- Route protection applied across
/api
groups withauth
and role checks
- POST /auth/sign-in β 200 { user, role, restaurant_ids[] } (sets/returns token)
- POST /auth/logout β 204
- GET /auth/me β 200 { user, role, restaurant_ids[], circle? } or 401
- POST /auth/sign-up-invite β 201 { user, role, restaurant_ids[] }
- POST /invites β 201 { code, role, restaurant_id? }
- POST
/api/auth/sign-up-invite
- Body:
{ code, email, password, full_name? }
- Behavior: single-use invite, no expiry by default; attaches associate to
restaurant_id
if present; if invite role isblack_shirt
and inviter is ops_lead/admin, auto-links inlead_relations
- Body:
- POST
/api/auth/sign-in
- Body:
{ email, password }
- Returns:
{ user, token }
- Body:
- POST
/api/auth/logout
- GET
/api/auth/me
- Returns:
{ user, restaurant_ids[] }
- Returns:
- POST
/api/invites
- Body:
- Invite black_shirt (ops_lead/admin):
{ role: 'black_shirt' }
- Invite associate (black_shirt/ops_lead/admin):
{ role: 'associate', restaurant_id }
- Invite black_shirt (ops_lead/admin):
- Validations:
- black_shirt invites only by ops_lead/admin
- associate invites: black_shirt β only own restaurant; ops_lead β only restaurants in his circle (or orphan)
- Body:
-
role
β ensureRole(...roles) -
restaurantAccess
β ensure access via membership/ownership or ops_lead circle
All network addresses are configured via environment variables, not hard-coded.
- Auth guarded routes and UI:
- BOH and management pages require auth; redirect unauthenticated to /login
- Show/hide navigation items based on role and membership
- After login, fetch /auth/me and cache role + restaurant_ids to drive UI and redirects
- All requests use credentials (httpOnly cookie) and backend URL from env
-
Backend: Database & Models
- users: add
role
enum (admin | ops_lead | black_shirt | associate) - restaurants: add
owner_user_id
(black_shirt); enable soft delete - user_restaurants: (user_id, restaurant_id) many-to-many membership (unique pair)
- lead_relations: (lead_user_id[ops_lead], black_shirt_user_id)
- invitations: (code, role, created_by_user_id, restaurant_id?, used_at, expires_at nullable)
- audit_log: (actor_user_id, action, entity_type, entity_id, payload JSON)
- Enable soft delete for users as well
- users: add
-
Backend: Auth & Invites
- POST /auth/register-by-invite (single-use code, no expiry by default)
- POST /auth/login (httpOnly cookie token)
- POST /auth/logout (clear token)
- GET /auth/me β { user, role, restaurant_ids[], circle: { black_shirt_ids[], restaurant_ids[] } }
- POST /invites (create invite):
- ops_lead β black_shirt (restaurant_id null)
- black_shirt β associate (restaurant_id required)
-
Backend: Access Control
- Middleware: ensureRole(...roles)
- Middleware: ensureRestaurantAccess(restaurantId)
- Policies: manageUsersInRestaurant, manageRestaurants per agreed rules
-
Backend: Management APIs
- ops_lead: manage black_shirts and associates within circle
- black_shirt: manage associates in own restaurants; create restaurants
- Admin-only: reassign black_shirt between ops_leads
- Transfer restaurants on black_shirt removal (orphan allowed; managed by admin + respective ops_lead)
-
Backend: Audit Logging
- Log
invite_create
,register_by_invite
,login
,logout
- Log role changes, membership add/remove, restaurant create/update/delete, transfers
- Log
-
Frontend: Auth & Guards
- API client with credentials (cookie) and env URLs
- Pages: /sign-in, /sign-up-invite
- Fetch /auth/me in layout; store role + restaurant_ids
- Route guards and role-based navigation visibility
-
Frontend: Dashboards (MVP)
- ops_lead: list black_shirts and their restaurants; basic management actions
- black_shirt: manage own restaurants and associates
-
Testing & Seeds
- Seeds for sample roles and relations
- E2E flow: invite β register β login β access protected routes
-
Config
- CORS with credentials, cookie flags (Secure/SameSite in prod)
- All URLs from env (no hardcoded addresses)
- Migrations
backend/database/migrations/1758800000000_add_role_and_deleted_to_users_table.ts
backend/database/migrations/1758800000001_add_owner_and_deleted_to_restaurants_table.ts
backend/database/migrations/1758800000002_create_user_restaurants_table.ts
backend/database/migrations/1758800000003_create_lead_relations_table.ts
backend/database/migrations/1758800000004_create_invitations_table.ts
backend/database/migrations/1758800000005_create_audit_log_table.ts
- Models
backend/app/models/user.ts
(role, soft delete)backend/app/models/restaurant.ts
(ownerUserId, soft delete)backend/app/models/user_restaurant.ts
backend/app/models/lead_relation.ts
backend/app/models/invitation.ts
- Middleware
backend/app/middleware/ensure_role_middleware.ts
backend/app/middleware/ensure_restaurant_access_middleware.ts
# Base URL
BASE=http://localhost:3333 # dev backend
# 1) Sign up via invite (pre-created code)
INVITE_CODE=REPLACE_WITH_CODE
curl -i -X POST "$BASE/api/auth/sign-up-invite" \
-H 'Content-Type: application/json' \
-d '{"code":"'$INVITE_CODE'","email":"user@example.com","password":"pass123"}'
# 2) Sign in
curl -s "$BASE/api/auth/sign-in" \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","password":"pass123"}' | tee /tmp/login.json
TOKEN=$(node -e "let d=require('fs').readFileSync('/tmp/login.json','utf8');try{let j=JSON.parse(d);console.log(j.token?.token||j.token)}catch(e){console.log('')} ")
# 3) Me (Bearer token)
curl -i "$BASE/api/auth/me" -H "Authorization: Bearer $TOKEN"
# 4) Create invite (requires auth)
# 4a) Invite black_shirt (ops_lead/admin)
curl -i -X POST "$BASE/api/invites" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"role":"black_shirt"}'
# 4b) Invite associate (black_shirt/ops_lead/admin), restaurant scoped
curl -i -X POST "$BASE/api/invites" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"role":"associate","restaurant_id":123}'
Notes:
- Replace
INVITE_CODE
with an actual code (see Invites API or DB). - The sign-in response contains
{ token: { token: "..." } }
. Use that string as Bearer. - All protected routes require
Authorization: Bearer $TOKEN
.
- Integrated
@adonisjs/bouncer
with initialize middleware (available asctx.bouncer
). - Abilities defined in
backend/app/abilities/main.ts
:manageRestaurants(user, restaurant)
manageUsersInRestaurant(user, restaurant, targetRole)
- Use inside controllers for fine-grained checks:
import { manageRestaurants, manageUsersInRestaurant } from '#abilities/main'
// await bouncer.authorize(manageRestaurants, restaurant)
// await bouncer.authorize(manageUsersInRestaurant, restaurant, 'associate')
- Official guide: AdonisJS Authorization