Skip to main content

Server Implementation Guide

This guide shows you exactly how to build the server-side API to receive and store DataGlue journey data.

Database Setup

You need 2 collections/tables:

MongoDB Collections

// Collection: users
{
  glue_user_id: String (indexed, unique),
  email: String (indexed),
  traits: Object,
  attribution: {
    first_touch: Object,
    last_touch: Object
  },
  first_seen: Date,
  last_seen: Date,
  identified: Boolean,
  identified_at: Date
}

// Collection: events
{
  event_id: String (indexed, unique),
  glue_user_id: String (indexed),
  session_id: String (indexed),
  event: String (indexed),
  timestamp: Date (indexed),
  properties: Object,
  email: String (indexed, optional)
}

PostgreSQL Tables

-- Table: users
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  glue_user_id VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255),
  traits JSONB,
  attribution JSONB,
  first_seen TIMESTAMP,
  last_seen TIMESTAMP,
  identified BOOLEAN DEFAULT FALSE,
  identified_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_users_glue_user_id ON users(glue_user_id);
CREATE INDEX idx_users_email ON users(email);

-- Table: events
CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  event_id VARCHAR(255) UNIQUE NOT NULL,
  glue_user_id VARCHAR(255) NOT NULL,
  session_id VARCHAR(255),
  event VARCHAR(100) NOT NULL,
  timestamp TIMESTAMP NOT NULL,
  properties JSONB,
  email VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_events_glue_user_id ON events(glue_user_id);
CREATE INDEX idx_events_session_id ON events(session_id);
CREATE INDEX idx_events_event ON events(event);
CREATE INDEX idx_events_timestamp ON events(timestamp);
CREATE INDEX idx_events_email ON events(email);

Complete Server Implementation

Node.js + Express + MongoDB

const express = require('express');
const { MongoClient } = require('mongodb');

const app = express();
app.use(express.json());

// MongoDB connection
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('dataglue');
const users = db.collection('users');
const events = db.collection('events');

// Create indexes
async function createIndexes() {
  await users.createIndex({ glue_user_id: 1 }, { unique: true });
  await users.createIndex({ email: 1 });
  await events.createIndex({ event_id: 1 }, { unique: true });
  await events.createIndex({ glue_user_id: 1 });
  await events.createIndex({ session_id: 1 });
  await events.createIndex({ event: 1 });
  await events.createIndex({ timestamp: -1 });
}

/**
 * POST /glue/identify
 * Links glue_user_id to email address
 */
app.post('/glue/identify', async (req, res) => {
  try {
    const { glue_user_id, email, traits, context } = req.body;

    // Validate required fields
    if (!glue_user_id || !email) {
      return res.status(400).json({
        success: false,
        error: 'glue_user_id and email are required'
      });
    }

    // Upsert user profile
    const result = await users.updateOne(
      { glue_user_id },
      {
        $set: {
          email,
          traits: traits || {},
          last_seen: new Date(),
          identified: true,
          identified_at: new Date(),
          'attribution.last_touch': {
            utm_source: context?.utm_source,
            utm_medium: context?.utm_medium,
            utm_campaign: context?.utm_campaign,
            referrer: context?.referrer,
            landing_page: context?.page?.url,
            timestamp: new Date()
          }
        },
        $setOnInsert: {
          glue_user_id,
          first_seen: new Date(),
          'attribution.first_touch': {
            utm_source: context?.utm_source,
            utm_medium: context?.utm_medium,
            utm_campaign: context?.utm_campaign,
            referrer: context?.referrer,
            landing_page: context?.page?.url,
            timestamp: new Date()
          }
        }
      },
      { upsert: true }
    );

    console.log(`User identified: ${email} (${glue_user_id})`);

    res.json({
      success: true,
      user_id: glue_user_id,
      new_user: result.upsertedCount > 0
    });

  } catch (error) {
    console.error('Identify error:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

/**
 * POST /glue/track
 * Records individual events
 */
app.post('/glue/track', async (req, res) => {
  try {
    const {
      glue_user_id,
      session_id,
      email,
      event,
      properties,
      timestamp,
      context
    } = req.body;

    // Validate required fields
    if (!glue_user_id || !event) {
      return res.status(400).json({
        success: false,
        error: 'glue_user_id and event are required'
      });
    }

    // Insert event
    const eventDoc = {
      event_id: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
      glue_user_id,
      session_id,
      email,
      event,
      properties: properties || {},
      timestamp: new Date(timestamp || Date.now()),
      context: context || {},
      created_at: new Date()
    };

    await events.insertOne(eventDoc);

    // Update user last_seen
    await users.updateOne(
      { glue_user_id },
      {
        $set: { last_seen: new Date() },
        $inc: { total_events: 1 }
      }
    );

    console.log(`Event tracked: ${event} for ${glue_user_id}`);

    res.json({
      success: true,
      event_id: eventDoc.event_id
    });

  } catch (error) {
    console.error('Track error:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

/**
 * POST /glue/sync
 * Receives complete user data (bulk sync)
 */
app.post('/glue/sync', async (req, res) => {
  try {
    const { glue_user_id, data, synced_at } = req.body;

    if (!glue_user_id || !data) {
      return res.status(400).json({
        success: false,
        error: 'glue_user_id and data are required'
      });
    }

    // Update user profile
    await users.updateOne(
      { glue_user_id },
      {
        $set: {
          ...data.profile,
          last_synced: new Date(synced_at),
          total_sessions: data.sessions?.length || 0,
          total_events: data.events?.length || 0
        }
      },
      { upsert: true }
    );

    // Bulk insert events (ignore duplicates)
    if (data.events && data.events.length > 0) {
      try {
        await events.insertMany(data.events, { ordered: false });
      } catch (error) {
        // Ignore duplicate key errors (events already synced)
        if (error.code !== 11000) {
          throw error;
        }
      }
    }

    console.log(`Data synced for ${glue_user_id}: ${data.events?.length || 0} events`);

    res.json({
      success: true,
      events_synced: data.events?.length || 0,
      sessions_synced: data.sessions?.length || 0
    });

  } catch (error) {
    console.error('Sync error:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

/**
 * GET /glue/user/:glue_user_id
 * Retrieve complete user journey
 */
app.get('/glue/user/:glue_user_id', async (req, res) => {
  try {
    const { glue_user_id } = req.params;

    // Get user profile
    const user = await users.findOne({ glue_user_id });

    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }

    // Get all events for this user
    const userEvents = await events
      .find({ glue_user_id })
      .sort({ timestamp: 1 })
      .toArray();

    res.json({
      success: true,
      user,
      events: userEvents,
      summary: {
        total_events: userEvents.length,
        first_event: userEvents[0]?.timestamp,
        last_event: userEvents[userEvents.length - 1]?.timestamp
      }
    });

  } catch (error) {
    console.error('Get user error:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

/**
 * GET /glue/user/email/:email
 * Retrieve user by email
 */
app.get('/glue/user/email/:email', async (req, res) => {
  try {
    const { email } = req.params;

    const user = await users.findOne({ email });

    if (!user) {
      return res.status(404).json({
        success: false,
        error: 'User not found'
      });
    }

    // Get all events
    const userEvents = await events
      .find({ glue_user_id: user.glue_user_id })
      .sort({ timestamp: 1 })
      .toArray();

    res.json({
      success: true,
      user,
      events: userEvents
    });

  } catch (error) {
    console.error('Get user by email error:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => {
  await createIndexes();
  console.log(`DataGlue API server running on port ${PORT}`);
});

Deploy to Production

Option 1: Vercel/Netlify Functions

Create api/glue/identify.js:
const { MongoClient } = require('mongodb');

let cachedDb = null;

async function connectToDatabase() {
  if (cachedDb) return cachedDb;

  const client = await MongoClient.connect(process.env.MONGODB_URI);
  cachedDb = client.db('dataglue');
  return cachedDb;
}

module.exports = async (req, res) => {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const db = await connectToDatabase();
  const { glue_user_id, email, traits, context } = req.body;

  await db.collection('users').updateOne(
    { glue_user_id },
    {
      $set: {
        email,
        traits,
        identified: true,
        last_seen: new Date()
      }
    },
    { upsert: true }
  );

  res.json({ success: true });
};

Option 2: Railway/Render

# Deploy Express app directly
npm install express mongodb
node server.js

Option 3: Cloudflare Workers

export default {
  async fetch(request, env) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const data = await request.json();

    // Store in D1 (Cloudflare's database)
    await env.DB.prepare(
      'INSERT INTO users (glue_user_id, email, traits) VALUES (?, ?, ?)'
    ).bind(data.glue_user_id, data.email, JSON.stringify(data.traits)).run();

    return Response.json({ success: true });
  }
};

Environment Variables

# .env file
MONGODB_URI=mongodb://localhost:27017/dataglue
PORT=3000

# Production (Vercel/Netlify)
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/dataglue

Testing Your Server

Test Identify Endpoint

curl -X POST http://localhost:3000/glue/identify \
  -H "Content-Type: application/json" \
  -d '{
    "glue_user_id": "test-123",
    "email": "test@example.com",
    "traits": {
      "fname": "Test",
      "lname": "User"
    },
    "context": {
      "utm_source": "google",
      "session_id": "session-456"
    }
  }'

Test Track Endpoint

curl -X POST http://localhost:3000/glue/track \
  -H "Content-Type: application/json" \
  -d '{
    "glue_user_id": "test-123",
    "session_id": "session-456",
    "event": "page_view",
    "properties": {
      "url": "/landing",
      "title": "Landing Page"
    },
    "timestamp": 1704376800000
  }'

Test Get User

curl http://localhost:3000/glue/user/test-123

Query Examples

Get User Journey

// Get complete user journey
const user = await users.findOne({ email: 'user@example.com' });
const journey = await events
  .find({ glue_user_id: user.glue_user_id })
  .sort({ timestamp: 1 })
  .toArray();

console.log(`${user.email} journey:`, journey);

Conversion Funnel

const funnel = await events.aggregate([
  {
    $match: {
      event: { $in: ['page_view', 'form_submit', 'purchase'] }
    }
  },
  {
    $group: {
      _id: '$event',
      count: { $sum: 1 }
    }
  }
]).toArray();

Attribution Report

const sources = await users.aggregate([
  {
    $group: {
      _id: '$attribution.first_touch.utm_source',
      count: { $sum: 1 },
      conversions: {
        $sum: { $cond: ['$identified', 1, 0] }
      }
    }
  },
  { $sort: { count: -1 } }
]).toArray();

Security Best Practices

  1. Validate requests
    if (!glue_user_id || !email) {
      return res.status(400).json({ error: 'Invalid request' });
    }
    
  2. Rate limiting
    const rateLimit = require('express-rate-limit');
    app.use('/glue', rateLimit({
      windowMs: 60 * 1000, // 1 minute
      max: 100 // 100 requests per minute
    }));
    
  3. CORS configuration
    app.use(cors({
      origin: ['https://yoursite.com'],
      methods: ['POST']
    }));
    
  4. API key authentication (optional)
    app.use('/glue', (req, res, next) => {
      const apiKey = req.headers['x-api-key'];
      if (apiKey !== process.env.API_KEY) {
        return res.status(401).json({ error: 'Unauthorized' });
      }
      next();
    });
    

Next Steps

  1. ✅ Deploy server to production
  2. ✅ Add profile-endpoint to your DataGlue script tag
  3. ✅ Test with a real form submission
  4. ✅ Query your database to see user journeys
  5. ✅ Build analytics dashboards on top of this data