Full Stack Migration Guide v2.1
================================================================================
DUTYBOOST - FULL STACK DEVELOPMENT WIREFRAME & MIGRATION GUIDE
For implementation outside Base44 (e.g. AWS)
Version 2.1 | Prepared for internal engineering use
================================================================================
TABLE OF CONTENTS
1. System Overview
2. Technology Stack Recommendation (AWS)
3. Database Schema (all entities)
4. Authentication Architecture
5. Backend API Design (REST)
6. Core Business Logic
7. Frontend Architecture
8. Email Service
9. File Storage
10. Role-Based Access Control (RBAC)
11. POS Webhook Integration
12. Environment Variables / Secrets
13. Deployment Architecture (AWS)
14. CI/CD Pipeline
15. Key User Flows (End-to-End)
16. Backend Functions Reference
17. Migration Checklist
================================================================================
1. SYSTEM OVERVIEW
================================================================================
DutyBoost is a B2B2C SaaS travel retail platform. It connects three parties:
- Travel Partners (agencies, hotels, car rentals, cruise lines, train operators)
- Airport / Station / Port Retailers (duty-free shops, retail stores)
- Travelers (end consumers)
Core value proposition:
Travel partners generate 'DutyCredits' when they issue bookings.
Travelers redeem DutyCredits at partner airport retailers for discounts.
DutyBoost earns a 2.5% platform fee on each redemption transaction.
Travel partners earn a commission of 20% of the DutyBoost fee on each redemption.
Business verticals (as of v2.0):
1. Flight bookings (core) - generates Voucher / DutyCredit
2. Hotel bookings - generates Voucher
3. Car rental bookings - generates Voucher
4. Cruise lines - generates CruiseCredit
5. Train / Rail operators - generates TrainCredit
DutyCredit validity: 30 days from the traveler's departure date.
================================================================================
2. TECHNOLOGY STACK RECOMMENDATION (AWS)
================================================================================
FRONTEND
Framework: React 18 + Vite
Language: JavaScript (or TypeScript)
Styling: Tailwind CSS + shadcn/ui
State: @tanstack/react-query (server state) + useState/useContext (local)
Routing: react-router-dom v6
i18n: i18next + react-i18next (6 languages: EN, NL, DE, FR, ES, IT)
Charts: Recharts
QR Scanning: jsQR (webcam-based scanning in-browser)
Maps: react-leaflet
Drag & Drop: @hello-pangea/dnd
Animations: framer-motion
Hosting: AWS CloudFront + S3 (static site)
BACKEND
Runtime: Node.js 20 (or Deno 2)
Framework: Express.js (or Hono for Deno/edge)
Language: JavaScript / TypeScript
Hosting: AWS Lambda + API Gateway (or ECS Fargate for persistent service)
Database: PostgreSQL on AWS RDS
ORM: Prisma (recommended) or Drizzle
Auth: AWS Cognito (or Auth.js / custom JWT)
Email: Postmark (already used) or AWS SES
File Storage: AWS S3
QR Code: api.qrserver.com (external, already in use) or sharp + qrcode npm
Payments: Mollie (already integrated - keep existing)
Secret Management: AWS Secrets Manager or SSM Parameter Store
Cron / Scheduled: AWS EventBridge Scheduler -> Lambda
Business Registry: KVK API (Dutch Chamber of Commerce)
================================================================================
3. DATABASE SCHEMA (PostgreSQL)
================================================================================
All tables include: id (UUID PK), created_at, updated_at, created_by (user email)
--- users ---
id UUID PK
email VARCHAR UNIQUE NOT NULL
full_name VARCHAR
role VARCHAR -- 'admin' | 'agency' | 'retailer' | 'user'
created_at TIMESTAMP
updated_at TIMESTAMP
--- signup_applications ---
id UUID PK
full_name VARCHAR NOT NULL
email VARCHAR NOT NULL
company VARCHAR NOT NULL
business_type VARCHAR -- 'Travel Partner' | 'Airport Retailer' | 'Train Partner'
website VARCHAR
kvk VARCHAR
vat VARCHAR
booking_system VARCHAR
airport VARCHAR
store_category VARCHAR
travel_partners TEXT[] -- array of partner names
status VARCHAR -- 'pending' | 'invited' | 'rejected'
mollie_customer_id VARCHAR
mollie_payment_id VARCHAR
payment_method VARCHAR
bank_account_name VARCHAR
bank_iban VARCHAR
terms_accepted BOOLEAN DEFAULT false
created_at TIMESTAMP
updated_at TIMESTAMP
--- bookings ---
id UUID PK
booking_id VARCHAR NOT NULL
total_ticket_price DECIMAL(10,2)
number_of_travelers INTEGER
travel_type VARCHAR -- 'Short-haul' | 'Long-haul'
departure_airport VARCHAR
travel_date DATE
return_date DATE
agency_email VARCHAR
traveler_email VARCHAR
flight_reminder_disabled BOOLEAN DEFAULT false
created_at TIMESTAMP
updated_at TIMESTAMP
--- vouchers ---
id UUID PK
voucher_code VARCHAR UNIQUE NOT NULL -- format: 'DB-XXXXXXXX'
booking_id VARCHAR NOT NULL
discount_percentage DECIMAL(10,2) -- stores euro credit amount, not a % despite name
airport VARCHAR
expiration_date DATE -- travel_date + 30 days
status VARCHAR -- 'active' | 'redeemed' | 'expired'
traveler_count INTEGER
ticket_price DECIMAL(10,2)
travel_type VARCHAR
agency_email VARCHAR
traveler_email VARCHAR -- NOTE: also used by hotel / car-rental generated vouchers
created_at TIMESTAMP
updated_at TIMESTAMP
--- retailers ---
id UUID PK
store_name VARCHAR NOT NULL
airport VARCHAR NOT NULL
category VARCHAR -- 'Retail'
description TEXT
accepts_vouchers BOOLEAN DEFAULT true
owner_email VARCHAR
created_at TIMESTAMP
updated_at TIMESTAMP
--- transactions ---
id UUID PK
voucher_code VARCHAR NOT NULL
retailer_id UUID REFERENCES retailers(id)
store_name VARCHAR
transaction_value DECIMAL(10,2)
num_items INTEGER
discount_applied DECIMAL(6,2) -- effective % applied
base_discount DECIMAL(10,2)
discount_amount DECIMAL(10,2) -- euro saved by traveler
platform_revenue DECIMAL(10,2) -- 2.5% of gross, capped at EUR 25.00
agency_commission DECIMAL(10,2) -- 20% of platform_revenue, capped at EUR 5.00
agency_email VARCHAR
airport VARCHAR
created_at TIMESTAMP
updated_at TIMESTAMP
--- airports ---
id UUID PK
name VARCHAR NOT NULL
code VARCHAR NOT NULL UNIQUE -- IATA code e.g. AMS
city VARCHAR
country VARCHAR
created_at TIMESTAMP
updated_at TIMESTAMP
--- monthly_invoices ---
id UUID PK
retailer_email VARCHAR NOT NULL
retailer_name VARCHAR
amount DECIMAL(10,2)
month_year VARCHAR -- 'YYYY-MM'
invoice_date DATE
due_date DATE
status VARCHAR -- 'pending' | 'overdue' | 'paid'
created_at TIMESTAMP
updated_at TIMESTAMP
--- leads ---
id UUID PK
company_name VARCHAR NOT NULL
website VARCHAR
industry VARCHAR -- 'Retail' | 'Agency'
email VARCHAR NOT NULL
phone VARCHAR
full_name VARCHAR NOT NULL
position VARCHAR
decision_level VARCHAR -- 'Executive' | 'Manager' | 'Staff'
created_at TIMESTAMP
updated_at TIMESTAMP
--- survey_responses ---
id UUID PK
company_name VARCHAR
company_website VARCHAR
full_name VARCHAR
email VARCHAR
phone VARCHAR
company_type VARCHAR
annual_bookings VARCHAR
current_loyalty VARCHAR
interest_level VARCHAR
commission_attractive VARCHAR
snippet_willingness VARCHAR
biggest_challenge TEXT
wants_demo BOOLEAN DEFAULT false
created_at TIMESTAMP
--- travel_agencies ---
id UUID PK
name VARCHAR NOT NULL
website VARCHAR
country VARCHAR DEFAULT 'Netherlands'
created_at TIMESTAMP
updated_at TIMESTAMP
--- retailer_feedback ---
id UUID PK
voucher_code VARCHAR
airport VARCHAR
store_name VARCHAR
rating INTEGER -- 1-5 stars
comment TEXT
created_at TIMESTAMP
updated_at TIMESTAMP
--- account_deletions ---
id UUID PK
email VARCHAR NOT NULL
user_type VARCHAR -- 'Airport Retailer' | 'Travel Partner'
reason VARCHAR NOT NULL
other_reason TEXT
created_at TIMESTAMP
updated_at TIMESTAMP
--- transaction_aggregates ---
id UUID PK
date DATE NOT NULL
retailer_id UUID REFERENCES retailers(id)
store_name VARCHAR
airport VARCHAR
transaction_count INTEGER
total_volume DECIMAL(10,2)
total_discount DECIMAL(10,2)
platform_revenue DECIMAL(10,2)
created_at TIMESTAMP
updated_at TIMESTAMP
--- terminal_movement_patterns ---
id UUID PK
airport VARCHAR NOT NULL
terminal VARCHAR NOT NULL
time_window VARCHAR -- e.g. '6:00-9:00 (Early Morning)'
season VARCHAR -- 'Spring' | 'Summer' | 'Fall' | 'Winter' | etc.
traffic_level INTEGER -- 1-10
estimated_travelers INTEGER
notes TEXT
submitted_by_email VARCHAR
is_agency BOOLEAN DEFAULT false
created_at TIMESTAMP
updated_at TIMESTAMP
--- CRUISE MODULE ---
--- cruise_credits ---
id UUID PK
token_id VARCHAR UNIQUE NOT NULL
traveler_email VARCHAR
traveler_name VARCHAR
cruise_line VARCHAR
home_port VARCHAR
balance DECIMAL(10,2)
remaining_balance DECIMAL(10,2)
status VARCHAR -- 'active' | 'used' | 'expired' | 'partially_used'
expiry TIMESTAMP
agency_email VARCHAR
created_at TIMESTAMP
updated_at TIMESTAMP
--- cruise_transactions ---
id UUID PK
token_id VARCHAR NOT NULL
store_name VARCHAR
retailer_id UUID REFERENCES retailers(id)
traveler_email VARCHAR
amount DECIMAL(10,2)
discount_amount DECIMAL(10,2)
airport VARCHAR
status VARCHAR -- 'approved' | 'rejected'
created_at TIMESTAMP
updated_at TIMESTAMP
--- TRAIN / RAIL MODULE ---
--- train_credits ---
id UUID PK
token_id VARCHAR UNIQUE NOT NULL
traveler_email VARCHAR
traveler_name VARCHAR
train_operator VARCHAR
departure_station VARCHAR
balance DECIMAL(10,2)
remaining_balance DECIMAL(10,2)
status VARCHAR -- 'active' | 'used' | 'expired' | 'partially_used'
expiry TIMESTAMP
agency_email VARCHAR
created_at TIMESTAMP
updated_at TIMESTAMP
--- train_transactions ---
id UUID PK
token_id VARCHAR NOT NULL
store_name VARCHAR
retailer_id UUID REFERENCES retailers(id)
traveler_email VARCHAR
amount DECIMAL(10,2)
discount_amount DECIMAL(10,2)
station VARCHAR
status VARCHAR -- 'approved' | 'rejected'
created_at TIMESTAMP
updated_at TIMESTAMP
================================================================================
4. AUTHENTICATION ARCHITECTURE
================================================================================
Recommended: AWS Cognito User Pools
- Email/password login (primary)
- Magic link login (already used via sendMagicLink function)
- JWT access tokens with short TTL (1h) + refresh tokens (30 days)
- User roles stored in Cognito custom attributes OR in your own DB users table
Token flow:
Client -> POST /auth/login -> Server validates -> Returns JWT
Client stores JWT in memory (not localStorage for security)
Client sends JWT in Authorization: Bearer <token> header on every request
Server middleware decodes + verifies JWT on every protected route
Role-based middleware pattern (Express.js):
function requireRole(...roles) {
return (req, res, next) => {
const user = req.user; // set by JWT middleware
if (!roles.includes(user.role)) return res.status(403).json({ error: 'Forbidden' });
next();
};
}
User roles:
admin - full access to everything
agency - travel partner (can generate DutyCredits, view own transactions)
retailer - airport retailer (can scan/redeem vouchers, view own stats)
user - general (traveler-facing, limited access)
NOTE: Non-admin users are served a ComingSoonPage for all routes except surveys.
================================================================================
5. BACKEND API DESIGN (REST)
================================================================================
BASE URL: https://api.dutyboost.com/v1
All endpoints return JSON. Auth endpoints marked with [public].
All others require Authorization: Bearer <token> header.
--- AUTH ---
POST /auth/login [public] Email + password login
POST /auth/magic-link [public] Send magic link email
POST /auth/magic-link/verify [public] Verify magic link token -> return JWT
POST /auth/logout Invalidate refresh token
GET /auth/me Return current user profile
PATCH /auth/me Update current user profile
--- VOUCHERS / DUTYCREDITS ---
POST /vouchers/generate [agency|admin] Generate DutyCredit from booking
Body: { booking_id, total_ticket_price, number_of_travelers, travel_type,
departure_airport, travel_date, return_date?, traveler_email, traveler_name? }
Returns: { voucher_code, credit_amount_eur, expiration_date, qr_image_url }
GET /vouchers [admin] List all vouchers (paginated)
GET /vouchers/my [agency] List vouchers created by this agency
GET /vouchers/:code [admin|agency] Get single voucher by code
POST /vouchers/:code/expire [admin] Manually expire a voucher
--- POS / REDEMPTION ---
POST /pos/redeem [public + POS secret header]
Header: X-POS-Secret: <secret>
Body: { voucher_code, transaction_value, retailer_id, store_name, airport }
Returns: { success, discount_amount, amount_due, platform_fee, agency_commission }
GET /pos/validate/:code [public + POS secret] Check voucher status without redeeming
--- RETAILERS ---
GET /retailers [admin] List all retailers
POST /retailers [admin] Create retailer
GET /retailers/:id [admin|retailer] Get retailer
PATCH /retailers/:id [admin] Update retailer
DELETE /retailers/:id [admin] Delete retailer
GET /retailers/my [retailer] Get own retailer profile
--- TRANSACTIONS ---
GET /transactions [admin] All transactions (paginated, filterable)
GET /transactions/my [agency] Transactions for this agency's vouchers
GET /transactions/retailer [retailer] Transactions at this retailer
GET /transactions/:id [admin] Single transaction
--- USERS ---
GET /users [admin] List all users
POST /users/invite [admin] Invite user by email + role
PATCH /users/:id/role [admin] Change user role
DELETE /users/:id [admin] Delete user
--- SIGNUP APPLICATIONS ---
POST /applications [public] Submit signup application
GET /applications [admin] List all applications
POST /applications/:id/approve [admin] Approve + invite user
POST /applications/:id/reject [admin] Reject application
DELETE /applications/:id [admin] Delete application
--- AIRPORTS ---
GET /airports [public] List all airports
POST /airports/import [admin] Bulk import airports
--- INVOICES ---
GET /invoices [admin] List all monthly invoices
POST /invoices/generate [admin] Generate invoices for a given month
PATCH /invoices/:id [admin] Update invoice status (paid/overdue)
--- ANALYTICS ---
GET /analytics/dashboard [admin] Platform-wide KPIs
GET /analytics/agency [agency] Agency-specific metrics
GET /analytics/retailer [retailer] Retailer-specific metrics
--- CRUISE CREDITS ---
POST /cruise-credits/generate [agency|admin] Generate cruise credit
GET /cruise-credits/my [agency] List own cruise credits
POST /cruise-credits/:id/redeem [retailer] Redeem cruise credit at port retailer
--- TRAIN CREDITS ---
POST /train-credits/generate [agency|admin] Generate train credit
GET /train-credits/my [agency] List own train credits
POST /train-credits/:id/redeem [retailer] Redeem at station retailer
--- LEADS & SURVEYS ---
POST /leads [public] Submit lead (GetInTouch form)
POST /surveys [public] Submit survey response
GET /leads [admin] List all leads
GET /surveys [admin] List all survey responses
DELETE /leads/cleanup [admin] Delete old leads (>90 days)
--- ACCOUNT DELETION ---
POST /account-deletion [public] Submit account deletion request
GET /account-deletion [admin] List deletion requests
--- FEEDBACK ---
POST /feedback/retailer [public] Submit retailer feedback (star + comment)
GET /feedback/retailer [admin] List all retailer feedback
================================================================================
6. CORE BUSINESS LOGIC
================================================================================
A. DutyCredit Calculation (flights)
------------------------------------
Config:
Short-haul base: EUR 2.50 per traveler
Long-haul base: EUR 7.00 per traveler
Floor: EUR 5.00
Cap: EUR 60.00
Multipliers (based on total ticket price):
< EUR 200: 1.0x
EUR 200-499: 1.5x
EUR 500-999: 2.0x
>= EUR 1000: 2.5x
Formula:
raw = basePerTraveler * numTravelers * multiplier
creditAmount = clamp(raw, floor=5, cap=60)
Expiration:
expirationDate = travelDate + 30 days
Voucher code format: 'DB-' + 8 random alphanumeric chars (e.g. DB-A3X7KQ2P)
B. Redemption Tiers (applied at POS for standard vouchers)
----------------------------------------------------------
EUR 70 - 99 -> Boarding DutyCredit: EUR 10 discount
EUR 100 - 199 -> Priority DutyCredit: EUR 30 discount
EUR 200 - 299 -> Lounge DutyCredit: EUR 45 discount
>= EUR 300 -> VIP DutyCredit: EUR 60 discount
< EUR 70 -> Not eligible (minimum spend)
Financial split per transaction:
Platform revenue (DutyBoost fee): 2.5% of gross transaction value, capped at EUR 25.00
Agency commission: 20% of DutyBoost fee (capped at EUR 5.00 when fee is capped)
Retailer pays: discountAmount to traveler
C. Voucher Lifecycle
--------------------
active -> redeemed (after successful POS scan)
active -> expired (if expiration_date < today when scanned)
Manual: admin can force-expire a voucher
D. Invoice Generation (monthly)
-------------------------------
Run on the 1st of each month via cron.
For each retailer: sum all transactions from prior month.
Create MonthlyInvoice record with amount = total platform_revenue.
Note: platform_revenue = 2.5% of transaction_value, capped at EUR 25.00 per transaction.
due_date = invoice_date + 14 days. Send invoice email to retailer.
E. Agency Payouts (monthly)
---------------------------
Run on the 1st of each month via cron.
For each agency: sum all agency_commission from prior month's transactions.
Note: agency_commission = 20% of platform_revenue per transaction (capped at EUR 5.00/tx).
Process payout via Mollie API (bank transfer to agency IBAN).
F. Flight Reminders
-------------------
Run daily via cron.
Find bookings where travel_date = tomorrow AND flight_reminder_disabled = false.
Send reminder email to traveler_email with their voucher code + QR.
H. Cruise Credit Logic
----------------------
CruiseCredit generated at booking time by cruise line partner.
token_id = unique credit identifier sent to traveler.
Redeemable at participating port/duty-free retailers.
Balance is partial-use (remaining_balance decreases per transaction).
I. Train Credit Logic
---------------------
TrainCredit generated at booking time by train/rail operator.
Redeemable at participating station retailers before boarding.
Same partial-balance model as CruiseCredit.
================================================================================
7. FRONTEND ARCHITECTURE
================================================================================
Directory Structure:
/src
|-- api/
| +-- base44Client.js # SDK client (replace with your own Axios client)
|-- pages/
| |-- LandingPage.jsx # Public landing page
| |-- ComingSoonPage.jsx # Default for non-admin users
| |-- SelectUserType.jsx # Partner type selection (5 verticals)
| |-- GetStarted.jsx # Legacy signup application form
| |-- AgencyPortal.jsx # Travel partner dashboard
| |-- AgencySignup.jsx # Travel partner signup wizard
| |-- AgencyShowcase.jsx # Travel partner showcase/marketing
| |-- RetailerPortal.jsx # Retailer dashboard
| |-- RetailerSignup.jsx # Retailer signup wizard
| |-- RetailerShowcase.jsx # Retailer showcase/marketing
| |-- RetailerScanQR.jsx # QR scanner for redemptions
| |-- RetailerScanOnly.jsx # Simplified scan-only mode
| |-- HotelPortal.jsx # Hotel partner dashboard
| |-- HotelSignup.jsx # Hotel signup wizard
| |-- HotelShowcase.jsx # Hotel showcase/marketing
| |-- CarRentalPortal.jsx # Car rental dashboard
| |-- CarRentalSignup.jsx # Car rental signup wizard
| |-- CarRentalShowcase.jsx # Car rental showcase/marketing
| |-- CruisePortal.jsx # Cruise line dashboard
| |-- CruiseSignup.jsx # Cruise signup wizard
| |-- CruiseShowcase.jsx # Cruise showcase/marketing
| |-- TrainPortal.jsx # Train operator dashboard
| |-- TrainSignup.jsx # Train signup wizard
| |-- TrainShowcase.jsx # Train showcase/marketing
| |-- AdminDashboard.jsx # Admin analytics
| |-- AdminPanel.jsx # User/app management
| |-- ScanVoucher.jsx # Voucher scan/lookup
| |-- AirportMap.jsx # Interactive airport map (react-leaflet)
| |-- DemoLanding.jsx # Demo mode landing
| |-- DemoAgencyDashboard.jsx
| |-- DemoRetailerDashboard.jsx
| |-- DemoHotelDashboard.jsx
| |-- DemoCarRentalDashboard.jsx
| |-- DemoCruiseDashboard.jsx
| |-- DemoTrainDashboard.jsx
| |-- DemoAdminDashboard.jsx
| |-- SurveyDashboard.jsx # Admin view of survey responses
| |-- LeadsAdmin.jsx # Admin view of leads
| |-- FAQ.jsx
| |-- TermsOfService.jsx
| |-- PrivacyPolicy.jsx
| |-- RefundPolicy.jsx
| |-- CookiePolicy.jsx
| |-- DataProcessingAgreement.jsx
| |-- CodeOfConduct.jsx
| |-- UserGuide.jsx
| |-- AgencyIntegrationGuide.jsx
| |-- AgencyIntegrationPage.jsx
| |-- RetailerIntegrationGuide.jsx
| |-- AgencyOnePager.jsx
| |-- RetailerOnePager.jsx
| |-- RetailerPresentationFuture.jsx
| |-- TravelPartnerPresentationFuture.jsx
| |-- RetailerCalculator.jsx
| |-- RegistrationFlow.jsx
| |-- EmailPreview.jsx
| |-- GTMTestingChecklist.jsx
| |-- StagingValidationGuide.jsx
| |-- DevWireframe.jsx # This document viewer
| |-- TravelPartnerSurvey.jsx (+ NL, DE, FR, ES, IT variants)
| +-- AirportRetailerSurvey.jsx (+ NL, DE, FR, ES, IT variants)
|-- components/
| |-- Layout.jsx # Sidebar + nav shell
| |-- DutyBoostLogo.jsx
| |-- BookingForm.jsx # Generate DutyCredit form
| |-- VoucherCard.jsx
| |-- QRScanner.jsx # jsQR-based scanner
| |-- QRCodeDisplay.jsx
| |-- PageFooter.jsx
| |-- FooterLinks.jsx
| |-- LanguageSwitcher.jsx
| |-- CookieConsentBar.jsx
| |-- WeCallYouModal.jsx # 'We call you' lead capture modal
| |-- WeCallYouForm.jsx
| |-- GetInTouchModal.jsx
| |-- GetInTouchForm.jsx
| |-- KvkValidator.jsx # Dutch Chamber of Commerce validator
| |-- ScrollRestoration.jsx
| |-- TravelerIcon.jsx # Icon components for each vertical
| |-- TravelAgencyIcon.jsx
| |-- AirportRetailerIcon.jsx
| |-- HotelIcon.jsx
| |-- CarRentalIcon.jsx
| |-- CruiseShipIcon.jsx
| |-- TrainIcon.jsx
| +-- ui/ # shadcn/ui components (keep as-is)
|-- lib/
| |-- AuthContext.jsx # Replace Base44 auth with your own JWT auth
| |-- i18n.js # i18next config
| |-- i18nTraveler.js # Traveler-facing i18n config
| |-- discountCalculator.js # calcBasketCredit() pure function
| +-- query-client.js # @tanstack/react-query client instance
|-- locales/
| |-- en.js
| |-- nl.js
| |-- de.js
| |-- fr.js
| |-- es.js
| +-- it.js
|-- utils/
| |-- encryptionUtils.js # Field-level encryption for PII in applications
| |-- exportToPptx.js # PowerPoint export utility
| |-- exportRetailerPptx.js
| +-- exportTravelPartnerPptx.js
|-- data/
| |-- demoData.js # Mock data for demo dashboards
| +-- devGuideContent.js # This document
+-- App.jsx # React Router config
Route Structure (App.jsx):
/ -> LandingPage (authenticated admin) / Navigate to /dashboard
/get-started -> SelectUserType (5 partner types)
/travel-partner-signup -> AgencySignup (4-step wizard: info -> bank -> PO -> terms)
/hotel-signup -> HotelSignup (2-step: info -> terms)
/car-rental-signup -> CarRentalSignup (2-step: info -> terms)
/cruise-signup -> CruiseSignup (2-step: info -> terms)
/train-signup -> TrainSignup (2-step: info -> terms)
/retailer-signup -> RetailerSignup (3-step: info -> payment -> terms)
/travel-partner-portal -> AgencyPortal
/hotel-portal -> HotelPortal
/car-rental-portal -> CarRentalPortal
/cruise-portal -> CruisePortal
/train-portal -> TrainPortal
/retailer-portal -> RetailerPortal
/demo -> DemoLanding
/demo/travel-partner -> DemoAgencyDashboard
/demo/retailer -> DemoRetailerDashboard
/demo/hotel -> DemoHotelDashboard
/demo/car-rental -> DemoCarRentalDashboard
/demo/cruise -> DemoCruiseDashboard
/demo/train -> DemoTrainDashboard
/demo/admin -> DemoAdminDashboard
/travel-partner-survey (+ /nl /de /fr /es /it variants)
/airport-port-retailer-survey (+ /nl /de /fr /es /it variants)
Public survey routes (no auth required):
/travel-partner-survey, /travel-partner-survey-nl/de/fr/es/it
/airport-port-retailer-survey, /airport-port-retailer-survey-nl/de/fr/es/it
Layout-wrapped routes (require auth, admin only):
/dashboard, /vouchers, /retailers, /travel-partners
/admin, /admin-dashboard, /retailer-dashboard
/scan-redeem, /retailer-scan, /leads-admin, /survey-dashboard
API Client Pattern (replacing Base44 SDK):
// src/api/client.js
import axios from 'axios';
const api = axios.create({ baseURL: 'https://api.dutyboost.com/v1' });
api.interceptors.request.use(config => {
const token = getToken(); // from memory/context
if (token) config.headers.Authorization = 'Bearer ' + token;
return config;
});
export default api;
Replace Base44 entity calls with:
base44.entities.Voucher.list() -> api.get('/vouchers')
base44.entities.Voucher.create(data) -> api.post('/vouchers/generate', data)
base44.entities.Transaction.filter({}) -> api.get('/transactions')
base44.entities.CruiseCredit.create(data) -> api.post('/cruise-credits/generate', data)
base44.entities.TrainCredit.create(data) -> api.post('/train-credits/generate', data)
base44.auth.me() -> api.get('/auth/me')
================================================================================
8. EMAIL SERVICE
================================================================================
Provider: Postmark (already in use - keep same credentials)
Emails sent by the platform:
1. DutyCredit email - traveler receives QR code + discount info
2. Welcome email - new partner onboarded after approval
3. Magic link - passwordless login
4. Flight reminder - day before departure
5. Monthly invoice - sent to retailers
6. Voucher redemption - confirmation to agency
7. Lead confirmation - auto-reply when lead submits form
8. Commission alert - notify agency of large commission earned
9. Signup welcome - sent after application approved + invited
10. Purchase order email - sent to partner when PO is generated
Postmark integration:
npm install postmark
import { ServerClient } from 'postmark';
const client = new ServerClient(process.env.POSTMARK_API_KEY);
await client.sendEmail({
From: 'hello@dutyboost.com',
To: recipientEmail,
Subject: subject,
HtmlBody: htmlBody,
});
================================================================================
9. FILE STORAGE (AWS S3)
================================================================================
Buckets:
dutyboost-public - logos, public assets (CloudFront CDN)
dutyboost-private - signed documents, invoices (no public access)
S3 SDK usage:
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'eu-west-1' });
// Upload file
await s3.send(new PutObjectCommand({
Bucket: 'dutyboost-private',
Key: 'invoices/' + invoiceId + '.pdf',
Body: pdfBuffer,
ContentType: 'application/pdf',
}));
// Generate short-lived download URL (300 seconds)
const url = await getSignedUrl(s3, new GetObjectCommand({
Bucket: 'dutyboost-private',
Key: 'invoices/' + invoiceId + '.pdf',
}), { expiresIn: 300 });
Encrypted fields:
signup_applications.bank_iban, bank_account_name are encrypted client-side
using AES-256-GCM before storage (see utils/encryptionUtils.js).
Decryption only via getSignupApplicationDecrypted backend function.
================================================================================
10. ROLE-BASED ACCESS CONTROL (RBAC)
================================================================================
Role Permissions
-----------------------------------------------------------------------------
admin All endpoints. Manage users, applications, invoices, analytics.
agency Generate DutyCredits. View own bookings, vouchers, transactions.
View own commission data.
retailer Scan QR codes (POS redemption). View own transactions and stats.
Access own retailer profile.
user (public) Submit applications, surveys, contact forms. No dashboard access.
Routed to ComingSoonPage for all app routes.
-----------------------------------------------------------------------------
Row-level data filtering:
- agency users: always filter transactions/vouchers by agency_email = currentUser.email
- retailer users: always filter transactions by retailer_id = currentUser.retailerId
- admin: no filter, sees everything
RLS rules on sensitive entities:
Transaction: read allowed if data.agency_email = user.email OR user.role = admin
Lead: read/update/delete requires admin role
Data the POS webhook reads without a user login (authenticated by shared secret):
- Voucher lookup and update
- Transaction creation
================================================================================
11. POS WEBHOOK INTEGRATION
================================================================================
Endpoint: POST /pos/redeem
Authentication: Shared secret in header X-POS-Secret: <secret>
(retailer receives this during onboarding)
Request body:
{
"voucher_code": "DB-A3X7KQ2P",
"transaction_value": 150.00,
"retailer_id": "uuid-of-retailer",
"store_name": "World Duty Free - Gate B22",
"airport": "AMS"
}
Success response:
{
"success": true,
"transaction_id": "uuid",
"voucher_code": "DB-A3X7KQ2P",
"gross_value": 150.00,
"effective_discount_pct": 20.00,
"discount_amount": 30.00,
"amount_due": 120.00,
"platform_fee": 3.75, // 2.5% of gross, max EUR 25.00
"agency_commission": 0.75, // 20% of platform_fee, max EUR 5.00
"message": "DutyCredit applied. Traveler saves EUR 30.00."
}
Error scenarios:
401 - Invalid POS secret
404 - Voucher not found
409 - Voucher already redeemed or expired
400 - Transaction value below EUR 70 minimum
Additional POS-style endpoints:
POST /cruise/redeem - Cruise credit redemption at port retailer
POST /train/redeem - Train credit redemption at station retailer
All follow same authentication pattern with appropriate shared secrets.
================================================================================
12. ENVIRONMENT VARIABLES / SECRETS
================================================================================
Store all secrets in AWS Secrets Manager or SSM Parameter Store.
Never commit secrets to source control.
Required environment variables:
DATABASE_URL PostgreSQL connection string
JWT_SECRET Secret for signing JWT tokens (min 32 chars)
POSTMARK_API_KEY Postmark server token (env var: PostMark)
MOLLIE_API_KEY Mollie live API key (for payments and payouts)
POS_WEBHOOK_SECRET Shared secret distributed to POS retailers
KVK_API_KEY Dutch Chamber of Commerce validation API
ENCRYPTION_KEY AES-256 key for encrypting PII fields (bank details)
AWS_REGION e.g. eu-west-1
AWS_S3_BUCKET_PUBLIC dutyboost-public
AWS_S3_BUCKET_PRIVATE dutyboost-private
FRONTEND_URL https://app.dutyboost.com (for CORS)
FROM_EMAIL hello@dutyboost.com
Optional / feature flags:
QR_SERVICE_URL https://api.qrserver.com/v1/create-qr-code (or self-hosted)
ENABLE_CRUISE true|false
ENABLE_TRAIN true|false
================================================================================
13. DEPLOYMENT ARCHITECTURE (AWS)
================================================================================
[Browser]
|
+---- HTTPS ---> [CloudFront CDN]
| |
| +-----+-------+
| | |
| [S3 bucket] [API Gateway]
| (React app files) |
| [Lambda / ECS]
| (Node.js API)
| |
| +--------+--------+
| | |
| [RDS PostgreSQL] [S3 Private]
| (eu-west-1) (invoices etc.)
|
+---- POS Terminal ---> [API Gateway /pos/redeem]
Recommended AWS services:
Compute: AWS Lambda (API) or ECS Fargate (if persistent connections needed)
Database: RDS PostgreSQL (Multi-AZ for production)
CDN/Hosting: CloudFront + S3
Auth: AWS Cognito or custom JWT
Scheduler: EventBridge Scheduler (for cron jobs)
Secrets: AWS Secrets Manager
Monitoring: CloudWatch + X-Ray
DNS: Route 53
SSL: AWS Certificate Manager (ACM)
Recommended regions: eu-west-1 (Ireland) - closest to Dutch/European users
Domain structure:
app.dutyboost.com -> Frontend (CloudFront + S3)
api.dutyboost.com -> Backend API (API Gateway + Lambda)
dutyboost.com -> Landing page / marketing
================================================================================
14. CI/CD PIPELINE
================================================================================
Recommended: GitHub Actions
Frontend pipeline (on push to main):
1. npm ci
2. npm run build
3. aws s3 sync ./dist s3://dutyboost-public --delete
4. aws cloudfront create-invalidation --distribution-id XXXXX --paths '/*'
Backend pipeline (on push to main):
1. npm ci
2. Run tests (jest or vitest)
3. Deploy Lambda: serverless deploy or AWS CDK deploy
4. Run DB migrations: npx prisma migrate deploy
Environments:
staging -> staging.dutyboost.com / staging-api.dutyboost.com
production -> app.dutyboost.com / api.dutyboost.com
Branch strategy:
main -> production deploy
develop -> staging deploy
feature/* -> PR -> develop
================================================================================
15. KEY USER FLOWS (END-TO-END)
================================================================================
FLOW 1: Travel Partner generates a DutyCredit (flight)
-------------------------------------------------------
1. Agency user logs in -> JWT issued
2. Opens BookingForm component
3. Fills in: booking_id, ticket price, # travelers, travel type,
departure airport, travel date, traveler email
4. Submits -> POST /vouchers/generate
5. Backend: calculateDutyCredit() -> generate voucher code (DB-XXXXXXXX)
-> set expiration = travelDate + 30 days
-> save Booking + Voucher to DB
-> generate QR image URL
-> send DutyCredit email to traveler via Postmark
6. Frontend: shows success + voucher code to agency user
FLOW 2: Traveler redeems DutyCredit at airport
-----------------------------------------------
1. Traveler arrives at participating store, shows QR on phone
2. Cashier scans QR on POS terminal
3. POS sends POST /pos/redeem with voucher_code + basket value
4. Backend: validates secret -> looks up voucher -> checks status + expiry
-> calcBasketCredit(basketValue) -> calculates financials
-> platformFee = min(grossValue * 0.025, 25.00)
-> agencyCommission = platformFee * 0.20
-> creates Transaction record -> marks Voucher as 'redeemed'
-> returns discount breakdown to POS
5. POS applies discount, prints receipt showing EUR X saved
FLOW 3: New partner onboarding
--------------------------------
1. Partner visits /get-started -> chooses their vertical (5 options)
2. Fills signup form for their type (AgencySignup, HotelSignup, etc.)
3. Application saved to signup_applications (POST /applications)
4. Admin reviews in admin panel -> approves (POST /applications/:id/approve)
5. Backend: sends invite email with magic link
6. Partner clicks link -> authenticated -> role set
7. Partner can now access their portal
FLOW 4: Monthly retailer billing
---------------------------------
1. EventBridge Scheduler triggers cron on 1st of month
2. Lambda POST /invoices/generate runs:
- Sums transactions per retailer for previous month
- Creates MonthlyInvoice records
- Sends invoice emails via Postmark
3. Retailer pays via Mollie payment link
4. Mollie webhook -> update invoice status to 'paid'
FLOW 5: Agency commission payout
---------------------------------
1. EventBridge Scheduler triggers cron on 1st of month
2. Lambda aggregates agency_commission per agency
3. Initiates Mollie bank transfer to agency IBAN
4. Logs payout in system
FLOW 6: Cruise credit redemption at port
------------------------------------------
1. Cruise line generates CruiseCredit at booking
2. Traveler arrives at port duty-free store
3. Retailer scans token -> POST /cruise/redeem
4. CruiseTransaction created, credit balance reduced
FLOW 7: Lead capture and follow-up
------------------------------------
1. Potential partner fills GetInTouch/WeCallYou form
2. Lead saved to leads table (POST /leads)
3. Auto-reply email sent via sendLeadConfirmationEmail
4. Admin reviews leads in /leads-admin
5. Weekly cleanup removes leads older than 90 days
================================================================================
16. BACKEND FUNCTIONS REFERENCE
================================================================================
All backend functions run as serverless handlers (Deno-based on Base44, Lambda on AWS).
Function Trigger Description
------------------------------------------------------------------------------
generateDutyCredit HTTP POST Generate flight DutyCredit voucher
generateTrainCredit HTTP POST Generate train credit token
posWebhook HTTP POST POS redemption webhook handler
approveApplication HTTP POST Approve signup + invite user
autoApproveApplication HTTP POST Auto-approve with role assignment
createSignupApplicationEncrypted HTTP POST Create application with encrypted PII
getSignupApplicationDecrypted HTTP GET Retrieve application with decrypted PII
inviteUser HTTP POST Invite user by email + role
adminSetUserRole HTTP POST Change user role (admin only)
assignRoleOnUserCreate Entity trigger Auto-assign default role on user create
ensureUserRole HTTP POST Ensure user has correct role
ensureBothAdmins HTTP POST Ensure both admin accounts exist
fixUserRole HTTP POST Repair corrupted user role
createTestAdmin HTTP POST Create test admin user (dev only)
sendSignupWelcomeEmail HTTP POST Send welcome email to new partner
sendLeadConfirmationEmail HTTP POST Auto-reply to lead form submissions
sendMagicLink HTTP POST Send passwordless magic link
sendFlightReminders Scheduled daily Find tomorrow's departures + email travelers
sendRedemptionNotification HTTP POST Notify agency of voucher redemption
sendCommissionAlert HTTP POST Alert agency of large commission
sendPurchaseOrderEmail HTTP POST Email purchase order to partner
notifyVoucherRedeemed Entity trigger Trigger on Voucher status -> redeemed
sendTestEmail HTTP POST Send test email (dev/admin only)
generateMonthlyInvoices Scheduled 1st Generate retailer invoices for prior month
generateMonthlyRetailerPayouts Scheduled 1st Process retailer payouts
generateMonthlyAgencyPayouts Scheduled 1st Process agency commission payouts
executeAgencyPayouts HTTP POST Manual trigger for agency payouts
executeBatchPayouts HTTP POST Batch payout execution
setupAgencyPayout HTTP POST Configure Mollie payout for agency
checkOverdueInvoices Scheduled daily Mark overdue invoices
generateDailyMetrics Scheduled daily Aggregate daily transaction metrics
createMolliePayment HTTP POST Create Mollie payment session
mollieInvoiceWebhook HTTP POST Handle Mollie payment webhook
mollieCardManager HTTP POST Manage Mollie card payment methods
processRetailerCardPayment HTTP POST Process retailer card payment
validateKvk HTTP POST Validate KVK number via Dutch API
importAirports HTTP POST Bulk import airport data
cleanupOldLeads Scheduled weekly Delete leads older than 90 days
deleteAllApplications HTTP POST Delete all applications (admin/dev only)
deleteUserOnApplicationDelete Entity trigger Clean up user when application deleted
gmailAutoReply Connector Auto-reply to inbound Gmail messages
------------------------------------------------------------------------------
Scheduled function cron schedule:
sendFlightReminders -> daily 07:00 UTC
checkOverdueInvoices -> daily 08:00 UTC
generateDailyMetrics -> daily 02:00 UTC
generateMonthlyInvoices -> 1st of month 06:00 UTC
generateMonthlyRetailerPayouts -> 1st of month 07:00 UTC
generateMonthlyAgencyPayouts -> 1st of month 08:00 UTC
cleanupOldLeads -> weekly Monday 03:00 UTC
================================================================================
17. AUDIT FIXES (v2.0 Updates)
================================================================================
Critical bugs fixed in current build:
BUG 1: Missing translation keys in signup forms
Issue: AgencySignup & RetailerSignup referenced non-existent i18n keys
(e.g., t('getstarted.full_name') -> undefined label)
Fix: Hardcoded English labels directly in form components
BUG 2: Broken sign-in redirects
Issue: AgencySignup redirected to /agency-portal (non-existent route)
Fix: Changed to /travel-partner-portal (correct route)
BUG 3: Homepage routing logic
Issue: Authenticated admin users still saw LandingPage; non-admins got ComingSoonPage
Fix: Home route now checks user?.role === 'admin'; non-admin redirects to /dashboard
BUG 4: Survey Dashboard missing imports
Issue: SurveyDashboard used FileText icon without importing
Fix: Added FileText to lucide-react import
BUG 5: Undefined variable in App.jsx
Issue: Lint error - 'authedUser' is not defined
Fix: Changed to 'user' (correct destructured variable from useAuth)
Feature: Fee cap enforcement (v2.0)
- Platform fee capped at EUR 25.00 per transaction (2.5% of gross, max EUR 25)
- Agency commission capped at EUR 5.00 per transaction (20% of platform fee)
- Cap applied in: posWebhook, generateMonthlyInvoices, generateMonthlyAgencyPayouts,
executeAgencyPayouts, RetailerCalculator, SurveyDashboard RevenueCalculator,
DemoAdminDashboard, RetailerDashboard
Feature: Survey Dashboard enhancements (v2.0)
- Added expandable response cards showing full survey answers per respondent
- Added CSV + Excel export for all responses
- Added individual response download buttons (CSV/Excel per response)
- Searchable survey data with language detection flags
- Supports: Travel Partner & Airport Retailer surveys in 6 languages (EN, NL, DE, FR, ES, IT)
Role consistency notes:
- 'agency' role in code = 'Travel Partner' in UI (display via getRoleLabel())
- Terminology: 'DutyCredit' for flights, 'CruiseCredit' for cruises, 'TrainCredit' for trains
- Non-admin users routed to ComingSoonPage for all routes except public surveys
================================================================================
18. MIGRATION CHECKLIST (Base44 -> AWS)
================================================================================
[ ] Export all production data from Base44 (CSV/JSON for all entities)
[ ] Set up RDS PostgreSQL with full schema (section 3)
[ ] Import data into PostgreSQL tables
[ ] Build Express.js API with all endpoints in section 5
[ ] Replace base44.auth.me() with JWT middleware
[ ] Replace base44.entities.X.list() with API calls
[ ] Replace base44.integrations.Core.SendEmail() with Postmark SDK
[ ] Port encryption logic (utils/encryptionUtils.js) to backend
[ ] Set up AWS Cognito (or JWT) auth
[ ] Set up S3 buckets
[ ] Deploy backend to Lambda (or ECS)
[ ] Deploy frontend to S3 + CloudFront
[ ] Configure EventBridge cron jobs for all scheduled functions (section 16)
[ ] Configure custom domains + SSL
[ ] Move all secrets to AWS Secrets Manager
[ ] Migrate Mollie customer/payment IDs to new system
[ ] Configure POS webhook secret for all active retailers
[ ] Run end-to-end test: generate DutyCredit -> redeem at POS
[ ] Test cruise credit flow end-to-end
[ ] Test train credit flow end-to-end
[ ] Enable monitoring (CloudWatch alarms on Lambda errors)
[ ] Verify i18n works for all 6 languages (EN, NL, DE, FR, ES, IT)
================================================================================
APPENDIX: KNOWN LIMITATIONS & TECH DEBT (v2.0)
================================================================================
Frontend Maintenance (post-launch items):
- SurveyDashboard.jsx is 865+ lines; consider splitting into sub-components
- Signup wizard components (AgencySignup, RetailerSignup, etc.) repeat code
-> Refactor: create reusable <SignupWizard> wrapper component
- Translation keys scattered across multiple locale files (currently 6 languages)
-> Best practice: organize by page/section rather than flat key names
Backend Functions (v2.0):
- All backend functions currently Deno-based (Base44 platform)
- Migration to AWS Lambda will require: Node.js 20 + Express/Hono adaption
- POS webhook secret distribution needs secure channel setup
Security considerations:
- Bank IBAN/account names encrypted client-side before transmission
- PII (name, email, phone) not encrypted at rest (consider client-side encryption for sensitive apps)
- Survey responses stored with language flags for audit trail
- All RLS rules enforced at database layer (see section 10)
Performance notes:
- Survey Dashboard loads up to 500 responses at once (max pagination)
-> Consider lazy-loading if dataset grows >1000 records
- QR codes generated server-side (via external QR service or sharp)
- Chart rendering with Recharts; consider virtualization if 1000+ data points
================================================================================
END OF DOCUMENT
DutyBoost Development Wireframe v2.1 (Updated May 2026)
Includes audit fixes, survey dashboard updates, and v2.0 architectural notes
================================================================================We use cookies to improve your experience. You decide which cookies to allow — essential cookies are always required for the platform to function. Cookie Policy