11 KiB
Tour Builder Platform - Backend
Node.js/Express REST API server with Sequelize ORM for the Tour Builder Platform.
Tech Stack
- Runtime: Node.js 18+
- Framework: Express 4.x
- Database: PostgreSQL with Sequelize ORM
- Authentication: Passport.js (JWT, Google OAuth, Microsoft OAuth)
- File Storage: AWS S3 / Google Cloud Storage / Local filesystem
- Email: Nodemailer with AWS SES
- API Docs: Swagger/OpenAPI
Prerequisites
- Node.js 18+
- PostgreSQL 14+
- Yarn package manager
Quick Start
# Install dependencies
yarn install
# Create database (first time only)
yarn db:create
# Start server (runs migrations, seeds, and watches for changes)
export $(cat .env | xargs) && NODE_ENV=production yarn start
The server runs on port 8080 by default.
Environment Variables
Create a .env file in the backend directory:
# Database (required)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app_39215
DB_USER=app_39215
DB_PASSWORD=your_password
# JWT Secret (required)
SECRET_KEY=your-secret-key
# Admin credentials (for seeding)
ADMIN_EMAIL=admin@example.com
ADMIN_PASS=admin_password
USER_PASS=user_password
# AWS S3 (optional - for file storage)
AWS_S3_BUCKET=your-bucket
AWS_S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_S3_PREFIX=your-prefix
# Google OAuth (optional)
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
# Microsoft OAuth (optional)
MS_CLIENT_ID=your-client-id
MS_CLIENT_SECRET=your-client-secret
# Email - AWS SES (optional)
EMAIL_USER=ses-smtp-user
EMAIL_PASS=ses-smtp-password
# OpenAI (optional)
GPT_KEY=your-openai-key
Project Structure
backend/src/
├── index.js # Express app entry point
├── config.js # Environment configuration
├── helpers.js # Utility functions (wrapAsync)
│
├── auth/ # Passport.js authentication strategies
│ └── auth.js # JWT, Google, Microsoft strategies
│
├── db/
│ ├── db.config.js # Database connection config (per environment)
│ ├── models/ # Sequelize model definitions (16 models)
│ ├── api/ # Database access layer (CRUD per model)
│ ├── migrations/ # Database migrations
│ └── seeders/ # Seed data (admin users, permissions, roles)
│
├── routes/ # Express route handlers (22 routes)
│ ├── auth.js # Authentication endpoints
│ ├── projects.js # Project CRUD
│ ├── tour_pages.js # Tour page management
│ ├── assets.js # Asset management
│ ├── file.js # File upload/download, presigned URLs
│ ├── publish.js # Publishing workflow
│ ├── search.js # Global search
│ └── ... # Other entity routes
│
├── services/ # Business logic layer (21 services)
│ ├── auth.js # Auth service (JWT, OAuth)
│ ├── publish.js # Publishing workflow logic
│ ├── file.js # File storage abstraction
│ ├── search.js # Global search service
│ ├── email/ # Email templates and sending
│ ├── notifications/ # Error classes and i18n messages
│ └── ... # Other entity services
│
├── middlewares/
│ ├── check-permissions.js # RBAC permission checking
│ ├── runtime-context.js # Environment detection from headers
│ ├── runtime-public.js # Public runtime access (no auth)
│ ├── upload.js # File upload handling (multer)
│ └── rateLimiter.js # Rate limiting for API endpoints
│
├── factories/
│ ├── router.factory.js # Generate CRUD routes
│ └── service.factory.js # Generate service classes
│
└── utils/
├── env-validation.js # Environment variable validation (Joi)
├── errors.js # Custom error classes
├── logger.js # Pino logger configuration
└── index.js # Utils barrel export
Database Setup
Create Database User and Database
# Connect to PostgreSQL
psql postgres -U postgres
# Create user
CREATE ROLE app_39215 WITH LOGIN PASSWORD 'your-password';
ALTER ROLE app_39215 CREATEDB;
# Create database
CREATE DATABASE app_39215 OWNER app_39215;
GRANT ALL PRIVILEGES ON DATABASE app_39215 TO app_39215;
\q
Available Commands
yarn db:create # Create database
yarn db:drop # Drop database
yarn db:migrate # Run pending migrations
yarn db:migrate:undo # Undo last migration
yarn db:migrate:undo:all # Undo all migrations
yarn db:migrate:status # Show migration status
yarn db:seed # Run all seeders
yarn db:seed:undo # Undo all seeders
yarn db:reset # Drop, create, migrate, and seed
yarn start # Migrate, seed, and start with watch
yarn lint # Run ESLint
API Documentation
Swagger UI available at: http://localhost:8080/api-docs
Core Endpoints
| Endpoint | Description |
|---|---|
POST /api/auth/signin/local |
Email/password login |
POST /api/auth/signup |
User registration |
GET /api/auth/me |
Current user info (JWT required) |
GET /api/auth/signin/google |
Google OAuth login |
GET /api/auth/signin/microsoft |
Microsoft OAuth login |
Entity CRUD Pattern
All entities follow standard REST patterns:
GET /api/{entity} # List with pagination & filters
GET /api/{entity}/:id # Get single record
POST /api/{entity} # Create record
PUT /api/{entity}/:id # Update record
DELETE /api/{entity}/:id # Soft delete record
Main Entities
| Entity | Description |
|---|---|
projects |
Virtual tour projects |
tour_pages |
Pages within a tour (elements, navigation, transitions stored in ui_schema_json) |
assets |
Uploaded media files |
asset_variants |
Resized/optimized asset versions |
element_type_defaults |
Global element default settings |
project_element_defaults |
Project-specific element settings |
project_audio_tracks |
Background audio for projects |
publish_events |
Publishing history and status tracking |
pwa_caches |
PWA cache manifests for offline support |
presigned_url_requests |
S3 presigned URL request tracking |
access_logs |
User access audit trail |
users |
User accounts |
roles |
User roles |
permissions |
Granular permissions |
project_memberships |
Team access per project |
Element Defaults Hierarchy
UI elements use a three-tier defaults system:
element_type_defaults (Global)
│
│ auto-snapshot on project creation
▼
project_element_defaults (Project)
│
│ applied when creating elements
▼
tour_pages.ui_schema_json (Instance)
-
Global (
element_type_defaults) - Platform-wide defaults for 11 element types (navigation, tooltip, gallery, etc.). Auto-seeded on first API access. -
Project (
project_element_defaults) - Per-project overrides. Automatically snapshotted from global when a project is created. Can be customized independently. -
Instance (
tour_pages.ui_schema_json) - Page-specific elements with their settings stored inline. Created in constructor with project defaults applied.
Additional Endpoints:
POST /api/project-element-defaults/:id/reset- Reset to current global defaultGET /api/project-element-defaults/:id/diff- Compare with global default
Publishing Workflow
Three-tier environment model for content: dev → stage → production
POST /api/publish/save-to-stage # Copy dev content to stage (body: { projectId })
POST /api/publish # Copy stage content to production (body: { projectId })
Pages have an environment field (dev, stage, or production) that determines visibility:
- Constructor (
/constructor?projectId=) - Always showsdevenvironment - Stage preview (
/p/[slug]/stage) - Showsstageenvironment - Public runtime (
/p/[slug]) - Showsproductionenvironment
Authentication
JWT Authentication
Protected routes require JWT token in Authorization header:
Authorization: Bearer <jwt-token>
OAuth Providers
- Google:
/api/auth/signin/google - Microsoft:
/api/auth/signin/microsoft
File Storage
Storage provider is auto-detected based on available credentials:
- AWS S3 - If
AWS_S3_BUCKETis configured - Google Cloud Storage - If GCS credentials are available
- Local filesystem - Fallback (files stored in system temp directory)
Upload Flow (Presigned URLs)
POST /api/file/presigned-url # Get upload URL (authenticated)
PUT {presigned-url} # Upload directly to S3
POST /api/assets # Register asset in database
Download Flow (Direct S3 Access)
For runtime asset preloading, the frontend can request presigned download URLs:
POST /api/file/presign # Get download URLs (public endpoint)
Request: { urls: ["assets/img1.jpg", "assets/video.mp4", ...] }
Response: { presignedUrls: { "assets/img1.jpg": "https://s3...", ... } }
- Max URLs per request: 50
- URL expiry: 1 hour
- Public endpoint: No authentication required (for runtime playback)
This allows the frontend to download assets directly from S3, bypassing the backend for better performance.
RBAC (Role-Based Access Control)
Permission Format
{ACTION}_{ENTITY}
Actions: CREATE, READ, UPDATE, DELETE
Example: CREATE_PROJECTS, READ_TOUR_PAGES, UPDATE_ASSETS
Default Roles
| Role | Description |
|---|---|
| Administrator | Full access to all features (user/role/permission management) |
| Platform Owner | Full project access, user management |
| Account Manager | Project and asset management |
| Tour Designer | Create and edit tours, assets, pages |
| Content Reviewer | Review and update content (read/update access) |
| Analytics Viewer | Read-only access for viewing data |
| Public | Minimal access for public users |
Environment Detection
Server Environment (NODE_ENV)
The backend uses NODE_ENV to determine database configuration:
| Value | Database | Description |
|---|---|---|
production |
Production config | Live environment |
dev_stage |
Staging config | Staging environment |
| (other) | Development config | Local development |
Content Environment (tour_pages.environment)
Separate from server environment, tour pages have a content environment field:
| Value | Access | Description |
|---|---|---|
dev |
Constructor only | Editing/draft content |
stage |
Stage preview | Pre-production review |
production |
Public runtime | Published content |
The X-Runtime-Environment header (set by frontend) determines which content environment to query. The runtime-context.js middleware resolves this for API requests.
Docker
See docker/ directory for Docker Compose setup:
cd docker
docker-compose up
Logging
Uses Pino logger with pretty printing in development:
const logger = require('pino')();
logger.info('Server started');
logger.error({ err }, 'Error occurred');