> ## Documentation Index
> Fetch the complete documentation index at: https://dataglue.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-Side Setup Guide

> Complete guide to setting up a backend for DataGlue journey tracking

# Server-Side Setup Guide

This guide shows you how to set up a backend to collect and manage DataGlue attribution and journey data. Choose the approach that best fits your stack.

## Architecture Overview

```mermaid theme={null}
graph TB
    subgraph "User's Browser"
        A[User visits site] --> B[DataGlue tracks]
        B --> C[localStorage: sessions, events, profile]
        C --> D[User fills form]
        D --> E[Form submits with journey data]
    end

    subgraph "Server Options"
        E --> F{Choose Backend}
        F -->|Option 1| G[N8N Webhooks]
        F -->|Option 2| H[Serverless Functions]
        F -->|Option 3| I[Custom Server]
    end

    subgraph "Database"
        G --> J[(Supabase/PostgreSQL)]
        H --> J
        I --> J
        J --> K[users table]
        J --> L[events table]
    end

    subgraph "Query & Analytics"
        K --> M[User Journeys]
        L --> M
        M --> N[Analytics Dashboard]
        M --> O[Attribution Reports]
        M --> P[Conversion Funnels]
    end
```

## Data Flow Diagram

```mermaid theme={null}
sequenceDiagram
    participant Browser
    participant DataGlue
    participant Form
    participant Server
    participant Database

    Browser->>DataGlue: Page load
    DataGlue->>DataGlue: Generate glue_user_id
    DataGlue->>DataGlue: Store in localStorage

    Note over DataGlue: Tracks sessions & events

    Browser->>Form: User fills email
    DataGlue->>DataGlue: Detects email
    DataGlue->>DataGlue: Updates profile

    Browser->>Form: User submits
    DataGlue->>Form: Inject hidden fields
    Note over Form: glue_user_id, session_id,<br/>total_sessions, attribution, etc.

    Form->>Server: POST with journey data
    Server->>Database: Check if email exists

    alt User Exists (Cross-Device)
        Database->>Server: User found
        Server->>Database: Merge glue_user_ids
        Server->>Database: Link devices
    else New User
        Database->>Server: Not found
        Server->>Database: Create new user
    end

    Server->>Database: Insert events
    Server->>Browser: Success response
```

***

## Option 1: N8N Webhooks + Supabase (Easiest)

**Best for:** Non-developers, rapid prototyping, visual workflows

### Architecture

```mermaid theme={null}
graph LR
    A[DataGlue] -->|POST /identify| B[N8N Webhook]
    A -->|POST /track| C[N8N Webhook]
    A -->|POST /sync| D[N8N Webhook]

    B --> E{User Exists?}
    E -->|Yes| F[Merge Devices]
    E -->|No| G[Create User]

    F --> H[(Supabase)]
    G --> H
    C --> H
    D --> H

    H --> I[users table]
    H --> J[events table]
```

### Setup Steps

#### 1. Create Supabase Project

```sql theme={null}
-- Create users table
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  glue_user_id TEXT UNIQUE NOT NULL,
  email TEXT,
  traits JSONB DEFAULT '{}'::jsonb,
  linked_ids TEXT[] DEFAULT ARRAY[]::TEXT[],
  first_seen TIMESTAMPTZ DEFAULT NOW(),
  last_seen TIMESTAMPTZ DEFAULT NOW(),
  first_touch_source TEXT,
  first_touch_medium TEXT,
  first_touch_campaign TEXT,
  last_touch_source TEXT,
  last_touch_medium TEXT,
  last_touch_campaign TEXT,
  total_sessions INTEGER DEFAULT 0,
  total_events INTEGER DEFAULT 0,
  identified BOOLEAN DEFAULT FALSE,
  identified_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

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

-- Create events table
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  event_id TEXT UNIQUE NOT NULL,
  glue_user_id TEXT NOT NULL,
  session_id TEXT,
  event TEXT NOT NULL,
  properties JSONB DEFAULT '{}'::jsonb,
  timestamp TIMESTAMPTZ NOT NULL,
  email TEXT,
  created_at TIMESTAMPTZ 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 DESC);
```

#### 2. Create N8N Workflows

**Workflow 1: Identify User**

```
Webhook (POST /webhook/glue/identify)
  ↓
Supabase: UPSERT users
  SET email, traits, last_seen, identified = true
  ON CONFLICT (glue_user_id) DO UPDATE
  ↓
Check for Email Duplicates
  SELECT * FROM users WHERE email = {{ email }} AND glue_user_id != {{ glue_user_id }}
  ↓
IF duplicates found:
  Supabase: UPDATE users
    SET linked_ids = array_append(linked_ids, {{ new_glue_user_id }})
  ↓
Respond: { success: true }
```

**Workflow 2: Track Event**

```
Webhook (POST /webhook/glue/track)
  ↓
Supabase: INSERT INTO events
  VALUES (event_id, glue_user_id, event, properties, timestamp)
  ↓
Supabase: UPDATE users
  SET last_seen = NOW(), total_events = total_events + 1
  ↓
Respond: { success: true }
```

**Workflow 3: Sync Complete Journey**

```
Webhook (POST /webhook/glue/sync)
  ↓
Parse journey_data JSON
  ↓
Loop through events array
  ↓
Supabase: INSERT INTO events (ON CONFLICT DO NOTHING)
  ↓
Supabase: UPSERT user profile
  ↓
Respond: { success: true, events_synced: X }
```

#### 3. Configure DataGlue

```html theme={null}
<script
  src="https://api.dataglue.io/glue.min.js"
  profile-endpoint="https://your-n8n.app/webhook/glue"
  inject-full-journey="true"
></script>
```

### Pros & Cons

**Pros:**

* ✅ No code required (visual workflows)
* ✅ Built-in error handling & retries
* ✅ Easy debugging (see every request)
* ✅ Can add multiple outputs (Slack, email, etc.)
* ✅ Supabase has generous free tier

**Cons:**

* ❌ N8N costs \$20/month (or self-host)
* ❌ Slightly slower than custom server
* ❌ Limited to N8N's capabilities

***

## Option 2: Serverless Functions (Vercel/Netlify)

**Best for:** Next.js/React apps, developers who want simplicity

### Architecture

```mermaid theme={null}
graph LR
    A[DataGlue] -->|POST| B[API Route]
    B --> C{Function Type}
    C -->|/api/identify| D[Identify Function]
    C -->|/api/track| E[Track Function]
    C -->|/api/sync| F[Sync Function]

    D --> G[(Supabase)]
    E --> G
    F --> G

    G --> H[PostgreSQL]
```

### Setup Steps

#### 1. Install Dependencies

```bash theme={null}
npm install @supabase/supabase-js
```

#### 2. Create API Routes

**File: `pages/api/glue/identify.js` (Next.js)**

```javascript theme={null}
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
)

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  const { glue_user_id, email, traits, context } = req.body

  if (!glue_user_id || !email) {
    return res.status(400).json({ error: 'Missing required fields' })
  }

  try {
    // Check if user with this email already exists
    const { data: existingUser } = await supabase
      .from('users')
      .select('*')
      .eq('email', email)
      .single()

    if (existingUser && existingUser.glue_user_id !== glue_user_id) {
      // Cross-device: merge the IDs
      await supabase
        .from('users')
        .update({
          linked_ids: [...(existingUser.linked_ids || []), glue_user_id]
        })
        .eq('glue_user_id', existingUser.glue_user_id)

      // Update all events to point to primary ID
      await supabase
        .from('events')
        .update({ glue_user_id: existingUser.glue_user_id })
        .eq('glue_user_id', glue_user_id)
    } else {
      // Upsert user
      await supabase
        .from('users')
        .upsert({
          glue_user_id,
          email,
          traits,
          identified: true,
          identified_at: new Date().toISOString(),
          last_seen: new Date().toISOString(),
          last_touch_source: context?.utm_source,
          last_touch_medium: context?.utm_medium,
          last_touch_campaign: context?.utm_campaign,
        }, {
          onConflict: 'glue_user_id'
        })
    }

    res.json({ success: true, glue_user_id })
  } catch (error) {
    console.error('Identify error:', error)
    res.status(500).json({ error: error.message })
  }
}
```

**File: `pages/api/glue/track.js`**

```javascript theme={null}
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
)

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  const { glue_user_id, session_id, event, properties, timestamp } = req.body

  try {
    // Insert event
    await supabase
      .from('events')
      .insert({
        event_id: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
        glue_user_id,
        session_id,
        event,
        properties,
        timestamp: new Date(timestamp)
      })

    // Update user stats
    await supabase
      .from('users')
      .update({
        last_seen: new Date().toISOString(),
        total_events: supabase.raw('total_events + 1')
      })
      .eq('glue_user_id', glue_user_id)

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

#### 3. Configure DataGlue

```html theme={null}
<script
  src="https://api.dataglue.io/glue.min.js"
  profile-endpoint="https://your-app.vercel.app/api/glue"
></script>
```

### Pros & Cons

**Pros:**

* ✅ No server management
* ✅ Auto-scaling
* ✅ Generous free tiers (Vercel: 100GB bandwidth)
* ✅ Fast cold starts
* ✅ Easy to deploy

**Cons:**

* ❌ Requires coding
* ❌ Limited execution time (10-60 seconds)
* ❌ Cold starts can add latency

***

## Option 3: Custom Server (Node.js + Express)

**Best for:** Developers who want full control, high-volume apps

### Architecture

```mermaid theme={null}
graph TB
    A[DataGlue] -->|HTTPS| B[Load Balancer]
    B --> C[Express Server]
    C --> D{Route}
    D -->|POST /identify| E[Identify Handler]
    D -->|POST /track| F[Track Handler]
    D -->|POST /sync| G[Sync Handler]

    E --> H[Business Logic]
    F --> H
    G --> H

    H --> I[(PostgreSQL)]
    H --> J[(Redis Cache)]

    I --> K[users]
    I --> L[events]
```

### Setup Steps

#### 1. Install Dependencies

```bash theme={null}
npm install express pg redis cors dotenv
```

#### 2. Create Server

**File: `server.js`**

```javascript theme={null}
const express = require('express')
const { Pool } = require('pg')
const cors = require('cors')

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

// PostgreSQL connection
const pool = new Pool({
  connectionString: process.env.DATABASE_URL
})

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

  if (!glue_user_id || !email) {
    return res.status(400).json({ error: 'Missing required fields' })
  }

  try {
    // Check for existing user with this email
    const existing = await pool.query(
      'SELECT * FROM users WHERE email = $1 AND glue_user_id != $2',
      [email, glue_user_id]
    )

    if (existing.rows.length > 0) {
      // Cross-device merge
      const primaryUser = existing.rows[0]

      // Add to linked_ids
      await pool.query(
        'UPDATE users SET linked_ids = array_append(linked_ids, $1) WHERE glue_user_id = $2',
        [glue_user_id, primaryUser.glue_user_id]
      )

      // Merge events
      await pool.query(
        'UPDATE events SET glue_user_id = $1 WHERE glue_user_id = $2',
        [primaryUser.glue_user_id, glue_user_id]
      )
    } else {
      // Upsert user
      await pool.query(`
        INSERT INTO users (
          glue_user_id, email, traits, identified, identified_at,
          last_seen, last_touch_source, last_touch_medium
        ) VALUES ($1, $2, $3, true, NOW(), NOW(), $4, $5)
        ON CONFLICT (glue_user_id) DO UPDATE SET
          email = $2,
          traits = $3,
          identified = true,
          identified_at = COALESCE(users.identified_at, NOW()),
          last_seen = NOW(),
          last_touch_source = $4,
          last_touch_medium = $5
      `, [
        glue_user_id,
        email,
        JSON.stringify(traits),
        context?.utm_source,
        context?.utm_medium
      ])
    }

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

// POST /glue/track
app.post('/glue/track', async (req, res) => {
  const { glue_user_id, session_id, event, properties, timestamp } = req.body

  try {
    // Insert event
    await pool.query(`
      INSERT INTO events (event_id, glue_user_id, session_id, event, properties, timestamp)
      VALUES ($1, $2, $3, $4, $5, $6)
    `, [
      `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
      glue_user_id,
      session_id,
      event,
      JSON.stringify(properties),
      new Date(timestamp)
    ])

    // Update user
    await pool.query(
      'UPDATE users SET last_seen = NOW(), total_events = total_events + 1 WHERE glue_user_id = $1',
      [glue_user_id]
    )

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

// POST /glue/sync
app.post('/glue/sync', async (req, res) => {
  const { glue_user_id, data } = req.body

  try {
    // Upsert user
    await pool.query(`
      INSERT INTO users (glue_user_id, email, traits, first_seen, last_seen)
      VALUES ($1, $2, $3, $4, NOW())
      ON CONFLICT (glue_user_id) DO UPDATE SET
        email = COALESCE(users.email, $2),
        traits = $3,
        last_seen = NOW()
    `, [
      glue_user_id,
      data.profile.email,
      JSON.stringify(data.profile.traits),
      data.profile.first_seen
    ])

    // Bulk insert events
    for (const event of data.events) {
      await pool.query(`
        INSERT INTO events (event_id, glue_user_id, session_id, event, properties, timestamp)
        VALUES ($1, $2, $3, $4, $5, $6)
        ON CONFLICT (event_id) DO NOTHING
      `, [
        event.event_id,
        glue_user_id,
        event.session_id,
        event.event,
        JSON.stringify(event.properties),
        event.timestamp
      ])
    }

    res.json({ success: true, events_synced: data.events.length })
  } catch (error) {
    console.error('Sync error:', error)
    res.status(500).json({ error: error.message })
  }
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`DataGlue server running on port ${PORT}`)
})
```

#### 3. Deploy

```bash theme={null}
# Railway
railway up

# Or Render
render deploy

# Or Docker
docker build -t dataglue-server .
docker run -p 3000:3000 dataglue-server
```

### Pros & Cons

**Pros:**

* ✅ Full control over logic
* ✅ Can handle high volumes
* ✅ No cold starts
* ✅ Custom optimizations
* ✅ Direct database access

**Cons:**

* ❌ Requires server management
* ❌ Need to handle scaling
* ❌ More code to maintain
* ❌ Security considerations

***

## Cross-Device Identity Resolution

### The Challenge

```mermaid theme={null}
graph TB
    A[iPhone Safari] -->|glue_user_id: ABC-123| B[localStorage]
    C[Laptop Chrome] -->|glue_user_id: XYZ-789| D[localStorage]
    E[Android Firefox] -->|glue_user_id: DEF-456| F[localStorage]

    B --> G[Session 1, 2, 3]
    D --> H[Session 4, 5]
    F --> I[Session 6]

    G --> J{Same Person?}
    H --> J
    I --> J

    J -->|Email: john@example.com| K[SERVER MERGES]
    K --> L[Primary ID: XYZ-789]
    L --> M[Linked IDs: ABC-123, DEF-456]
```

### Solution: Email-Based Merging

```sql theme={null}
-- When user identifies on any device
-- Step 1: Check if email exists
SELECT * FROM users WHERE email = 'john@example.com';

-- Step 2a: If exists, merge devices
UPDATE users
SET linked_ids = array_append(linked_ids, 'NEW-DEVICE-ID')
WHERE email = 'john@example.com';

-- Step 2b: Unify events under primary ID
UPDATE events
SET glue_user_id = 'PRIMARY-ID'
WHERE glue_user_id = 'NEW-DEVICE-ID';

-- Step 3: Query complete journey
SELECT * FROM events
WHERE glue_user_id IN (
  SELECT glue_user_id FROM users WHERE email = 'john@example.com'
  UNION
  SELECT unnest(linked_ids) FROM users WHERE email = 'john@example.com'
)
ORDER BY timestamp ASC;
```

***

## Analytics Queries

### User Journey

```sql theme={null}
-- Get complete user journey
WITH user_profile AS (
  SELECT * FROM users WHERE email = 'user@example.com'
),
all_ids AS (
  SELECT glue_user_id FROM user_profile
  UNION
  SELECT unnest(linked_ids) FROM user_profile
)
SELECT
  e.timestamp,
  e.event,
  e.properties->>'url' as page,
  e.session_id
FROM events e
WHERE e.glue_user_id IN (SELECT * FROM all_ids)
ORDER BY e.timestamp ASC;
```

### Attribution Report

```sql theme={null}
-- Users by first touch source
SELECT
  first_touch_source,
  COUNT(*) as users,
  COUNT(*) FILTER (WHERE identified = true) as converted
FROM users
GROUP BY first_touch_source
ORDER BY users DESC;
```

### Conversion Funnel

```sql theme={null}
-- Funnel analysis
WITH funnel AS (
  SELECT
    COUNT(DISTINCT CASE WHEN event = 'page_view' AND properties->>'path' = '/landing' THEN glue_user_id END) as landed,
    COUNT(DISTINCT CASE WHEN event = 'form_submit' THEN glue_user_id END) as submitted,
    COUNT(DISTINCT CASE WHEN event = 'page_view' AND properties->>'path' = '/thank-you' THEN glue_user_id END) as converted
  FROM events
)
SELECT
  landed,
  submitted,
  converted,
  ROUND(100.0 * submitted / landed, 2) as submit_rate,
  ROUND(100.0 * converted / submitted, 2) as conversion_rate
FROM funnel;
```

***

## Comparison Table

| Feature             | N8N + Supabase | Serverless  | Custom Server |
| ------------------- | -------------- | ----------- | ------------- |
| **Setup Time**      | 1 hour         | 2-3 hours   | 4-8 hours     |
| **Coding Required** | None           | Moderate    | High          |
| **Cost (monthly)**  | $0-$45         | $0-$20      | $10-$100      |
| **Scalability**     | Good           | Excellent   | Excellent     |
| **Performance**     | Good           | Good        | Best          |
| **Debugging**       | Visual         | Logs        | Full control  |
| **Maintenance**     | Low            | Medium      | High          |
| **Best For**        | Quick start    | Modern apps | High volume   |

***

## Recommended Approach

```mermaid theme={null}
graph TD
    A[Start] --> B{What's your priority?}
    B -->|Speed to market| C[N8N + Supabase]
    B -->|Modern stack| D[Serverless Functions]
    B -->|Full control| E[Custom Server]

    C --> F[Perfect for:<br/>- Non-developers<br/>- MVP/Testing<br/>- Visual workflows]
    D --> G[Perfect for:<br/>- Next.js/React apps<br/>- Moderate traffic<br/>- Developer-friendly]
    E --> H[Perfect for:<br/>- High volume<br/>- Custom logic<br/>- Enterprise apps]

    F --> I[Deploy in 1 hour]
    G --> I[Deploy in 3 hours]
    H --> I[Deploy in 1 day]
```

**Start simple, scale later:**

1. Begin with N8N + Supabase (fastest)
2. As you grow, migrate to Serverless Functions
3. At scale, move to Custom Server
4. Your data stays in Supabase/PostgreSQL throughout

***

## Next Steps

1. **Choose your approach** based on the comparison table
2. **Create database tables** in Supabase/PostgreSQL
3. **Set up your webhook/API endpoints**
4. **Configure DataGlue** with your endpoint URL
5. **Test with a form submission**
6. **Query your data** to see user journeys
7. **Build analytics dashboards** on top of the data

Need help? Check out:

* [Journey Tracking Guide](/docs/journey-tracking)
* [Form Injection Guide](/docs/form-injection)
* [Server Implementation Examples](/docs/server-implementation)
