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
Copy
// 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
Copy
-- 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
Copy
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
Createapi/glue/identify.js:
Copy
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
Copy
# Deploy Express app directly
npm install express mongodb
node server.js
Option 3: Cloudflare Workers
Copy
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
Copy
# .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
Copy
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
Copy
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
Copy
curl http://localhost:3000/glue/user/test-123
Query Examples
Get User Journey
Copy
// 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
Copy
const funnel = await events.aggregate([
{
$match: {
event: { $in: ['page_view', 'form_submit', 'purchase'] }
}
},
{
$group: {
_id: '$event',
count: { $sum: 1 }
}
}
]).toArray();
Attribution Report
Copy
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
-
Validate requests
Copy
if (!glue_user_id || !email) { return res.status(400).json({ error: 'Invalid request' }); } -
Rate limiting
Copy
const rateLimit = require('express-rate-limit'); app.use('/glue', rateLimit({ windowMs: 60 * 1000, // 1 minute max: 100 // 100 requests per minute })); -
CORS configuration
Copy
app.use(cors({ origin: ['https://yoursite.com'], methods: ['POST'] })); -
API key authentication (optional)
Copy
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
- ✅ Deploy server to production
- ✅ Add
profile-endpointto your DataGlue script tag - ✅ Test with a real form submission
- ✅ Query your database to see user journeys
- ✅ Build analytics dashboards on top of this data