Skip to content

🗄️ 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