🗄️ Parse Server¶
Backend-as-a-Service, multi-tenant data management, and API platform
Overview¶
The Parse Server serves as the primary backend-as-a-service platform for the Appgain ecosystem. It provides multi-tenant data management, RESTful APIs, real-time subscriptions, file storage, and user authentication across all Appgain applications.
🏗️ Architecture¶
Core Responsibilities¶
- Multi-tenant Data Management: Isolated data storage for different applications
- RESTful API: Comprehensive REST API for all data operations
- Real-time Subscriptions: Live data updates via WebSocket connections
- File Storage: Cloud file storage with CDN integration
- User Authentication: Built-in user management and authentication
- Push Notifications: Native push notification support
- Analytics: Built-in analytics and usage tracking
Technology Stack¶
- Node.js: Runtime environment
- MongoDB: Primary database
- Redis: Session management and caching
- AWS S3: File storage
- WebSocket: Real-time subscriptions
- Express.js: Web framework
🔧 Configuration¶
Server Details¶
- Server:
ovh-parse-server - Port: 1337 (HTTP), 1338 (HTTPS)
- Environment: Production, Staging, Development
Environment Variables¶
# Parse Server Configuration
PARSE_SERVER_APPLICATION_ID=appgain_parse_app
PARSE_SERVER_MASTER_KEY=ask your direct manager for the access
PARSE_SERVER_CLIENT_KEY=ask your direct manager for the access
PARSE_SERVER_JAVASCRIPT_KEY=ask your direct manager for the access
PARSE_SERVER_REST_API_KEY=ask your direct manager for the access
PARSE_SERVER_DOTNET_KEY=ask your direct manager for the access
# Database Configuration
PARSE_SERVER_DATABASE_URI=mongodb://ovh-mongo-master:27017/parse
PARSE_SERVER_REDIS_URL=redis://ovh-redis:6379
# File Storage
PARSE_SERVER_FILES_ADAPTER=@parse/s3-files-adapter
PARSE_SERVER_S3_BUCKET=appgain-parse-files
PARSE_SERVER_S3_REGION=us-east-1
PARSE_SERVER_S3_ACCESS_KEY=ask your direct manager for the access
PARSE_SERVER_S3_SECRET_KEY=ask your direct manager for the access
# Push Notifications
PARSE_SERVER_PUSH_ADAPTER=@parse/push-adapter
PARSE_SERVER_PUSH_APNS_P12=pa../apns.p12
PARSE_SERVER_PUSH_APNS_PASSPHRASE=ask your direct manager for the access
PARSE_SERVER_PUSH_GCM_SENDER_ID=ask your direct manager for the access
PARSE_SERVER_PUSH_GCM_API_KEY=ask your direct manager for the access
# Security
PARSE_SERVER_SESSION_LENGTH=31536000
PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET=true
PARSE_SERVER_EXPIRE_INVALID_SESSIONS=true
# Performance
PARSE_SERVER_MAX_UPLOAD_SIZE=100mb
PARSE_SERVER_CACHE_MAX_SIZE=1000
PARSE_SERVER_CACHE_TTL=300
# Monitoring
PARSE_SERVER_LOGGER_ADAPTER=@parse/logger-adapter
PARSE_SERVER_LOGGER_LEVEL=info
SENTRY_DSN=ask your direct manager for the access
📊 Multi-tenant Architecture¶
Application Isolation¶
// Parse Server Configuration
const api = new ParseServer({
databaseURI: process.env.PARSE_SERVER_DATABASE_URI,
appId: process.env.PARSE_SERVER_APPLICATION_ID,
masterKey: process.env.PARSE_SERVER_MASTER_KEY,
clientKey: process.env.PARSE_SERVER_CLIENT_KEY,
javascriptKey: process.env.PARSE_SERVER_JAVASCRIPT_KEY,
restAPIKey: process.env.PARSE_SERVER_REST_API_KEY,
dotNetKey: process.env.PARSE_SERVER_DOTNET_KEY,
// Multi-tenant configuration
allowCustomObjectId: true,
allowClientClassCreation: false,
enableAnonymousUsers: false,
// Security settings
sessionLength: 31536000,
revokeSessionOnPasswordReset: true,
expireInvalidSessions: true,
// File storage
filesAdapter: new S3Adapter({
bucket: process.env.PARSE_SERVER_S3_BUCKET,
region: process.env.PARSE_SERVER_S3_REGION,
accessKey: process.env.PARSE_SERVER_S3_ACCESS_KEY,
secretKey: process.env.PARSE_SERVER_S3_SECRET_KEY
}),
// Push notifications
push: {
adapter: '@parse/push-adapter',
ios: {
p12: process.env.PARSE_SERVER_PUSH_APNS_P12,
passphrase: process.env.PARSE_SERVER_PUSH_APNS_PASSPHRASE
},
android: {
senderId: process.env.PARSE_SERVER_PUSH_GCM_SENDER_ID,
apiKey: process.env.PARSE_SERVER_PUSH_GCM_API_KEY
}
}
});
Database Schema¶
// Core Collections
- _User: User accounts and authentication
- _Role: Role-based access control
- _Session: User sessions
- _Installation: Device installations for push notifications
- _PushStatus: Push notification delivery status
- _GlobalConfig: Global configuration settings
// Application Collections
- Suits: Application configurations
- Accounts: Account management
- Analytics: Analytics data
- Notifications: Notification records
- SmartLinks: URL shortening and tracking
- LandingPages: Landing page configurations
🔐 Authentication & Authorization¶
User Authentication¶
// User Registration
const user = new Parse.User();
user.set('username', 'john_doe');
user.set('email', 'john@example.com');
user.set('password', 'ask your direct manager for the access');
user.set('firstName', 'John');
user.set('lastName', 'Doe');
try {
await user.signUp();
console.log('User created successfully');
} catch (error) {
console.error('Error creating user:', error);
}
// User Login
try {
const user = await Parse.User.logIn('john_doe', 'ask your direct manager for the access');
console.log('User logged in:', user.get('username'));
} catch (error) {
console.error('Login error:', error);
}
// Session Management
const session = await Parse.Session.current();
console.log('Current session:', session.get('sessionToken'));
Role-Based Access Control¶
// Create Role
const role = new Parse.Role('Admin', new Parse.ACL());
await role.save();
// Add User to Role
const user = Parse.User.current();
role.getUsers().add(user);
await role.save();
// Check User Role
const userRoles = await Parse.Query(Parse.Role)
.equalTo('users', user)
.find();
const isAdmin = userRoles.some(role => role.get('name') === 'Admin');
Access Control Lists (ACL)¶
// Object-level permissions
const object = new Parse.Object('CustomObject');
const acl = new Parse.ACL();
// Public read, authenticated write
acl.setPublicReadAccess(true);
acl.setRoleWriteAccess('Admin', true);
object.setACL(acl);
await object.save();
📊 API Endpoints¶
User Management¶
# User Registration
POST /parse/users
{
"username": "john_doe",
"email": "john@example.com",
"password": "ask your direct manager for the access",
"firstName": "John",
"lastName": "Doe"
}
# User Login
POST /parse/login
{
"username": "john_doe",
"password": "ask your direct manager for the access"
}
# User Logout
POST /parse/logout
Headers: X-Parse-Session-Token: <session_token>
# Get Current User
GET /parse/users/me
Headers: X-Parse-Session-Token: <session_token>
# Update User
PUT /parse/users/<user_id>
Headers: X-Parse-Session-Token: <session_token>
{
"firstName": "John Updated"
}
Object Operations¶
# Create Object
POST /parse/classes/CustomObject
Headers: X-Parse-Session-Token: <session_token>
{
"field1": "value1",
"field2": "value2"
}
# Get Object
GET /parse/classes/CustomObject/<object_id>
Headers: X-Parse-Session-Token: <session_token>
# Update Object
PUT /parse/classes/CustomObject/<object_id>
Headers: X-Parse-Session-Token: <session_token>
{
"field1": "updated_value"
}
# Delete Object
DELETE /parse/classes/CustomObject/<object_id>
Headers: X-Parse-Session-Token: <session_token>
# Query Objects
GET /parse/classes/CustomObject?where={"field1":"value1"}
Headers: X-Parse-Session-Token: <session_token>
File Operations¶
# Upload File
POST /parse/files/filename.jpg
Headers:
X-Parse-Session-Token: <session_token>
Content-Type: image/jpeg
Body: <file_binary_data>
# Get File URL
GET /parse/files/<file_name>
Headers: X-Parse-Session-Token: <session_token>
# Delete File
DELETE /parse/files/<file_name>
Headers: X-Parse-Session-Token: <session_token>
Push Notifications¶
# Send Push Notification
POST /parse/push
Headers: X-Parse-Session-Token: <session_token>
{
"channels": ["general"],
"data": {
"alert": "Hello World!",
"badge": 1,
"sound": "default"
}
}
# Send to Specific Users
POST /parse/push
Headers: X-Parse-Session-Token: <session_token>
{
"where": {
"user": {
"$inQuery": {
"where": {
"username": "john_doe"
},
"className": "_User"
}
}
},
"data": {
"alert": "Hello John!"
}
}
🔄 Real-time Subscriptions¶
Live Query Setup¶
// Client-side subscription
const query = new Parse.Query('CustomObject');
query.equalTo('status', 'active');
const subscription = await query.subscribe();
subscription.on('open', () => {
console.log('Subscription opened');
});
subscription.on('create', (object) => {
console.log('Object created:', object);
});
subscription.on('update', (object) => {
console.log('Object updated:', object);
});
subscription.on('delete', (object) => {
console.log('Object deleted:', object);
});
subscription.on('close', () => {
console.log('Subscription closed');
});
Server-side Live Query¶
// Server-side subscription
Parse.LiveQuery.on('connect', () => {
console.log('LiveQuery connected');
});
Parse.LiveQuery.on('disconnect', () => {
console.log('LiveQuery disconnected');
});
Parse.LiveQuery.on('error', (error) => {
console.error('LiveQuery error:', error);
});
// Subscribe to class events
Parse.LiveQuery.on('create', 'CustomObject', (object) => {
console.log('Object created:', object);
});
Parse.LiveQuery.on('update', 'CustomObject', (object) => {
console.log('Object updated:', object);
});
Parse.LiveQuery.on('delete', 'CustomObject', (object) => {
console.log('Object deleted:', object);
});
📈 Performance & Scaling¶
Database Optimization¶
// Indexes
db.users.createIndex({ "email": 1 }, { unique: true });
db.users.createIndex({ "username": 1 }, { unique: true });
db.users.createIndex({ "createdAt": -1 });
db.users.createIndex({ "updatedAt": -1 });
db.suits.createIndex({ "accountId": 1 });
db.suits.createIndex({ "status": 1 });
db.suits.createIndex({ "createdAt": -1 });
db.analytics.createIndex({ "suitId": 1, "date": -1 });
db.analytics.createIndex({ "eventType": 1, "timestamp": -1 });
Caching Strategy¶
// Redis Caching
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
// Cache user sessions
client.setex(`session:${sessionToken}`, 3600, JSON.stringify(userData));
// Cache query results
client.setex(`query:${queryHash}`, 300, JSON.stringify(results));
// Cache file URLs
client.setex(`file:${fileId}`, 86400, fileUrl);
Load Balancing¶
// Horizontal scaling
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
// Worker process
require('./server');
}
🔍 Monitoring & Observability¶
Health Checks¶
# Parse Server Health
GET /parse/health
# Response Format
{
"status": "healthy",
"timestamp": "2024-01-01T00:00:00Z",
"version": "5.0.0",
"services": {
"mongodb": "connected",
"redis": "connected",
"s3": "connected"
},
"metrics": {
"activeConnections": 150,
"totalRequests": 10000,
"averageResponseTime": 120
}
}
Prometheus Metrics¶
// Custom metrics
const prometheus = require('prom-client');
const requestDuration = new prometheus.Histogram({
name: 'parse_request_duration_seconds',
help: 'Parse Server request duration',
labelNames: ['method', 'endpoint', 'status'],
buckets: [0.1, 0.5, 1, 2, 5, 10]
});
const activeConnections = new prometheus.Gauge({
name: 'parse_active_connections',
help: 'Number of active connections'
});
const totalRequests = new prometheus.Counter({
name: 'parse_requests_total',
help: 'Total number of requests',
labelNames: ['method', 'endpoint', 'status']
});
Error Tracking¶
// Error handling middleware
app.use((error, req, res, next) => {
console.error('Parse Server error:', error);
// Send to monitoring
if (process.env.SENTRY_DSN) {
Sentry.captureException(error, {
tags: {
endpoint: req.path,
method: req.method
},
extra: {
headers: req.headers,
body: req.body
}
});
}
// Return error response
res.status(500).json({
error: 'Internal server error',
code: error.code || 141
});
});
🚀 Deployment¶
Docker Configuration¶
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 1337
CMD ["npm", "start"]
Docker Compose¶
# docker-compose.yml
version: '3.8'
services:
parse-server:
build: .
ports:
- "1337:1337"
environment:
- NODE_ENV=production
- PARSE_SERVER_DATABASE_URI=mongodb://mongo:27017/parse
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
networks:
- appgain-net
deploy:
replicas: 3
mongo:
image: mongo:6.0
volumes:
- mongo-data:/data/db
networks:
- appgain-net
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- appgain-net
volumes:
mongo-data:
redis-data:
networks:
appgain-net:
driver: bridge
Kubernetes Deployment¶
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: parse-server
spec:
replicas: 3
selector:
matchLabels:
app: parse-server
template:
metadata:
labels:
app: parse-server
spec:
containers:
- name: parse-server
image: appgain/parse-server:latest
env:
- name: NODE_ENV
value: "production"
- name: PARSE_SERVER_DATABASE_URI
value: "mongodb://mongo-cluster:27017/parse"
- name: REDIS_URL
value: "redis://redis-cluster:6379"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /parse/health
port: 1337
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /parse/health
port: 1337
initialDelaySeconds: 5
periodSeconds: 5
🔧 Development¶
Local Development Setup¶
# Clone repository
git clone <repository>
cd parse-server
# Install dependencies
npm install
# Environment setup
cp .env.example .env
# Edit .env with local configuration
# Start development server
npm run dev
# Run tests
npm test
# Run linting
npm run lint
API Testing¶
# Test user registration
curl -X POST http://localhost:1337/parse/users \
-H "Content-Type: application/json" \
-H "X-Parse-Application-Id: appgain_parse_app" \
-d '{
"username": "testuser",
"email": "test@example.com",
"password": "ask your direct manager for the access"
}'
# Test user login
curl -X POST http://localhost:1337/parse/login \
-H "Content-Type: application/json" \
-H "X-Parse-Application-Id: appgain_parse_app" \
-d '{
"username": "testuser",
"password": "ask your direct manager for the access"
}'
# Test object creation
curl -X POST http://localhost:1337/parse/classes/CustomObject \
-H "Content-Type: application/json" \
-H "X-Parse-Application-Id: appgain_parse_app" \
-H "X-Parse-Session-Token: <session_token>" \
-d '{
"field1": "value1",
"field2": "value2"
}'
🔒 Security¶
Input Validation¶
// Parse Schema Validation
const CustomObject = Parse.Object.extend('CustomObject', {
// Instance methods
validate: function(attrs, options) {
if (!attrs.field1) {
throw new Error('field1 is required');
}
if (attrs.field2 && attrs.field2.length > 100) {
throw new Error('field2 must be less than 100 characters');
}
}
}, {
// Class methods
beforeSave: function(request) {
const object = request.object;
// Additional validation logic
}
});
Rate Limiting¶
// Express rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/parse', limiter);
Data Encryption¶
// Field-level encryption
const crypto = require('crypto');
function encryptField(value, key) {
const algorithm = 'aes-256-cbc';
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(algorithm, key);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
function decryptField(encryptedValue, key) {
const algorithm = 'aes-256-cbc';
const [ivHex, encrypted] = encryptedValue.split(':');
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipher(algorithm, key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
📞 Support & Resources¶
Documentation¶
Monitoring Tools¶
- Sentry: Error tracking and performance monitoring
- Prometheus: Metrics collection and alerting
- Grafana: Dashboard visualization
- Parse Dashboard: Web-based admin interface
Development Resources¶
- GitHub Repository: Source code and issues
- Postman Collection: API testing
- Swagger Documentation: Interactive API docs
- Development Environment: Docker setup
Last updated: January 2024
Ask Chehab GPT