---
title: "Server-Side Overview"
framework: vue
version: "2.1.2"
---

# Server-Side Overview

Server-Side Data moves query execution from the browser to your own backend. Your engine receives queries from Studio, translates them for your backend, and returns results in the format widgets expect.

> **Note**
>
> You may not need Server-Side Data. The built-in engine accepts arrays of rows via [Sync Data](https://www.ag-grid.com/studio/vue/sync-data/) and [Async Data](https://www.ag-grid.com/studio/vue/async-data/). Only adopt Server-Side Data when you need to push query execution to a backend.

> **Warning**
>
> Dashboards can place severe load on data backends. Ensure the backends you communicate with are scaled suitably for your use case.

The example below is using a custom engine that demonstrates how to set up server-side data access.

#### AlaSQL Server Side

```ts
import type {
  AgDataEngine,
  AgDataSourcesDefinition,
  AgExecuteRequest,
  AgExecuteResult,
  AgFieldDefinition,
  AgReportState,
  AgResultShape,
  AgSimpleDataSourceDefinition,
  AgStudioFilterDefinition,
  AgStudioMode,
  AgStudioQuery,
  AgStudioQueryField,
} from "ag-studio";
import { AgStudio } from "ag-studio-vue3";
import { createApp, defineComponent, ref, shallowRef } from "vue";

declare const alasql: any;

// ─── Sample data ────────────────────────────────────────────────────────────

interface SalesRow {
  region: string;
  product: string;
  category: string;
  quantity: number;
  revenue: number;
  cost: number;
  order_date: string;
  [key: string]: string | number;
}

const SALES_DATA: SalesRow[] = [
  {
    region: "North",
    product: "Widget A",
    category: "Widgets",
    quantity: 120,
    revenue: 2400,
    cost: 1200,
    order_date: "2025-01-15",
  },
  {
    region: "North",
    product: "Widget B",
    category: "Widgets",
    quantity: 85,
    revenue: 2550,
    cost: 1275,
    order_date: "2025-01-20",
  },
  {
    region: "North",
    product: "Gadget X",
    category: "Gadgets",
    quantity: 60,
    revenue: 3000,
    cost: 1800,
    order_date: "2025-02-01",
  },
  {
    region: "South",
    product: "Widget A",
    category: "Widgets",
    quantity: 200,
    revenue: 4000,
    cost: 2000,
    order_date: "2025-01-10",
  },
  {
    region: "South",
    product: "Widget B",
    category: "Widgets",
    quantity: 150,
    revenue: 4500,
    cost: 2250,
    order_date: "2025-02-05",
  },
  {
    region: "South",
    product: "Gadget X",
    category: "Gadgets",
    quantity: 90,
    revenue: 4500,
    cost: 2700,
    order_date: "2025-02-10",
  },
  {
    region: "South",
    product: "Gadget Y",
    category: "Gadgets",
    quantity: 45,
    revenue: 3375,
    cost: 2025,
    order_date: "2025-03-01",
  },
  {
    region: "East",
    product: "Widget A",
    category: "Widgets",
    quantity: 75,
    revenue: 1500,
    cost: 750,
    order_date: "2025-01-25",
  },
  {
    region: "East",
    product: "Gadget X",
    category: "Gadgets",
    quantity: 110,
    revenue: 5500,
    cost: 3300,
    order_date: "2025-02-15",
  },
  {
    region: "East",
    product: "Gadget Y",
    category: "Gadgets",
    quantity: 65,
    revenue: 4875,
    cost: 2925,
    order_date: "2025-03-10",
  },
  {
    region: "West",
    product: "Widget A",
    category: "Widgets",
    quantity: 95,
    revenue: 1900,
    cost: 950,
    order_date: "2025-01-30",
  },
  {
    region: "West",
    product: "Widget B",
    category: "Widgets",
    quantity: 130,
    revenue: 3900,
    cost: 1950,
    order_date: "2025-02-20",
  },
  {
    region: "West",
    product: "Gadget X",
    category: "Gadgets",
    quantity: 70,
    revenue: 3500,
    cost: 2100,
    order_date: "2025-03-05",
  },
  {
    region: "West",
    product: "Gadget Y",
    category: "Gadgets",
    quantity: 55,
    revenue: 4125,
    cost: 2475,
    order_date: "2025-03-15",
  },
  {
    region: "North",
    product: "Gadget Y",
    category: "Gadgets",
    quantity: 40,
    revenue: 3000,
    cost: 1800,
    order_date: "2025-03-20",
  },
];

// ─── AlaSQL table setup ─────────────────────────────────────────────────────

function setupAlaSqlTables(): void {
  alasql(
    "CREATE TABLE IF NOT EXISTS sales (region STRING, product STRING, category STRING, quantity INT, revenue NUMBER, cost NUMBER, order_date STRING)",
  );
  alasql("DELETE FROM sales");
  for (const row of SALES_DATA) {
    alasql("INSERT INTO sales VALUES (?, ?, ?, ?, ?, ?, ?)", [
      row.region,
      row.product,
      row.category,
      row.quantity,
      row.revenue,
      row.cost,
      row.order_date,
    ]);
  }
}

// ─── AG Studio source definition ────────────────────────────────────────────

const SALES_FIELDS: AgFieldDefinition[] = [
  { id: "region", format: "textFormat" },
  { id: "product", format: "textFormat" },
  { id: "category", format: "textFormat" },
  { id: "quantity", format: "integerFormat" },
  { id: "revenue", format: "currencyFormat" },
  { id: "cost", format: "currencyFormat" },
  { id: "order_date", format: "dateFormat" },
];

/**
 * Returns an AG Studio source definition with field metadata.
 * The engine handles actual data retrieval via AlaSQL at query time.
 */
function getSalesSource(): AgSimpleDataSourceDefinition {
  return {
    id: "sales",
    data: SALES_DATA,
    fields: SALES_FIELDS,
  };
}

// ─── AgStudioQuery → SQL translation ──────────────────────────────────────

/**
 * Extract the bare column name from a fully-qualified field ID.
 * e.g. `"sales.region"` → `"region"`
 */
function extractColumnName(fieldId: string): string {
  const dotIndex = fieldId.indexOf(".");
  return dotIndex >= 0 ? fieldId.slice(dotIndex + 1) : fieldId;
}

function fieldToSql(ref: AgStudioQueryField): string {
  return `[${extractColumnName(ref.fieldId)}]`;
}

function aggToSql(ref: AgStudioQueryField): string {
  const col = fieldToSql(ref);
  const { aggregation } = ref;
  if (!aggregation) return col;

  switch (aggregation) {
    case "sum":
      return `SUM(${col})`;
    case "avg":
      return `AVG(${col})`;
    case "count":
      return `COUNT(${col})`;
    case "countd":
      return `COUNT(DISTINCT ${col})`;
    case "min":
      return `MIN(${col})`;
    case "max":
      return `MAX(${col})`;
    case "first":
      return `FIRST(${col})`;
    case "last":
      return `LAST(${col})`;
    default:
      return col;
  }
}

/**
 * Translate an {@link AgStudioFilterDefinition} tree into a SQL WHERE
 * clause. Groups use `combinator` (`'and' | 'or' | 'not'`); leaves use
 * `operator`.
 */
function filterToSql(
  filter: AgStudioFilterDefinition,
  params: unknown[],
): string {
  if ("combinator" in filter) {
    const parts = filter.conditions.map((c) => filterToSql(c, params));
    if (filter.combinator === "not") return `NOT (${parts.join(" AND ")})`;
    return `(${parts.join(` ${filter.combinator.toUpperCase()} `)})`;
  }

  const col = `[${extractColumnName(filter.field.fieldId)}]`;
  const { operator, value } = filter;

  switch (operator) {
    case "equals":
      if (Array.isArray(value)) {
        const placeholders = value.map(() => "?").join(", ");
        params.push(...value);
        return `${col} IN (${placeholders})`;
      }
      params.push(value);
      return `${col} = ?`;
    case "notEqual":
      if (Array.isArray(value)) {
        const placeholders = value.map(() => "?").join(", ");
        params.push(...value);
        return `${col} NOT IN (${placeholders})`;
      }
      params.push(value);
      return `${col} != ?`;
    case "greaterThan":
      params.push(value);
      return `${col} > ?`;
    case "greaterThanOrEqual":
      params.push(value);
      return `${col} >= ?`;
    case "lessThan":
      params.push(value);
      return `${col} < ?`;
    case "lessThanOrEqual":
      params.push(value);
      return `${col} <= ?`;
    case "isNull":
      return `${col} IS NULL`;
    case "isNotNull":
      return `${col} IS NOT NULL`;
    case "between": {
      const [from, to] = value as [unknown, unknown];
      params.push(from, to);
      return `${col} BETWEEN ? AND ?`;
    }
    case "isIn": {
      const arr = value as unknown[];
      const placeholders = arr.map(() => "?").join(", ");
      params.push(...arr);
      return `${col} IN (${placeholders})`;
    }
    default:
      params.push(value);
      return `${col} = ?`;
  }
}

/**
 * Translate an engine-ready {@link AgStudioQuery} into an AlaSQL query
 * string. Returns the SQL and a parameter array for prepared-statement
 * binding.
 */
function agStudioQueryToSql(query: AgStudioQuery): {
  sql: string;
  params: unknown[];
} {
  const params: unknown[] = [];
  const { axes, measures, projection, filter, sort, limit } = query;

  const selectCols: string[] = [];
  const groupByCols: string[] = [];

  // Aggregation mode: dimensions from the first axis, plus measures.
  const dimensions = axes?.[0]?.dimensions ?? [];
  for (const dim of dimensions) {
    selectCols.push(`${fieldToSql(dim.field)} AS [${dim.field.key}]`);
    groupByCols.push(fieldToSql(dim.field));
  }
  for (const m of measures ?? []) {
    selectCols.push(`${aggToSql(m.field)} AS [${m.field.key}]`);
  }
  // Projection mode: raw columns, no GROUP BY.
  for (const dim of projection ?? []) {
    selectCols.push(`${fieldToSql(dim.field)} AS [${dim.field.key}]`);
  }

  const anchor =
    dimensions[0]?.field ?? measures?.[0]?.field ?? projection?.[0]?.field;
  const sourceId = anchor?.sourceId ?? "";
  let sql = `SELECT ${selectCols.join(", ")} FROM [${sourceId}]`;

  if (filter) {
    sql += ` WHERE ${filterToSql(filter, params)}`;
  }
  if (groupByCols.length > 0) {
    sql += ` GROUP BY ${groupByCols.join(", ")}`;
  }
  if (sort?.length) {
    const orderClauses = sort.map(
      (s) => `[${s.field.key}] ${s.direction.toUpperCase()}`,
    );
    sql += ` ORDER BY ${orderClauses.join(", ")}`;
  }
  if (limit) {
    sql += ` LIMIT ${limit.count}`;
    if (limit.offset) sql += ` OFFSET ${limit.offset}`;
  }

  return { sql, params };
}

function applyRowId(
  query: AgStudioQuery,
  rows: Record<string, unknown>[],
): void {
  const rowNumberField = query.window?.find(
    (w) => w.functionType === "rowNumber",
  );
  if (rowNumberField?.as) {
    const key = rowNumberField.as;
    const offset = query.limit?.offset ?? 0;
    rows.forEach((row, i) => {
      row[key] = offset + i;
    });
    return;
  }

  const dimensions = query.axes?.[0]?.dimensions ?? [];
  const rowIdField = query.computedFields?.[0];
  if (!dimensions.length || !rowIdField) return;
  const dimensionKeys = dimensions.map((d) => d.field.key);
  for (const row of rows) {
    row[rowIdField.field.key] = dimensionKeys
      .map((key) =>
        row[key] instanceof Date
          ? (row[key] as Date).toISOString()
          : String(row[key]),
      )
      .join("");
  }
}

// ─── AlaSqlDataEngine ─────────────────────────────────────────────────────

class AlaSqlDataEngine implements AgDataEngine {
  private readonly source: AgSimpleDataSourceDefinition = getSalesSource();

  /** Fully-qualified field IDs (e.g. `"sales.order_date"`) whose values are dates. */
  private readonly dateFieldKeys = new Set<string>(
    (this.source.fields ?? [])
      .filter((f) => f.format === "dateFormat")
      .map((f) => `${this.source.id}.${f.id}`),
  );

  // ── Schema declaration ───────────────────────────────────────

  getDataSources(): AgDataSourcesDefinition {
    return { sources: [this.source] };
  }

  // ── Query execution ──────────────────────────────────────────

  /**
   * Execute one or more queries. Each request is translated to SQL and
   * run against AlaSQL. A batch-aware engine could build a UNION ALL here
   * instead of iterating - this example keeps it simple.
   */
  async execute(
    ...requests: AgExecuteRequest<AgResultShape>[]
  ): Promise<AgExecuteResult[]> {
    return Promise.all(requests.map((req) => this.executeOne(req)));
  }

  private async executeOne(
    request: AgExecuteRequest<AgResultShape>,
  ): Promise<AgExecuteResult> {
    const { query, options, info } = request;
    const shape = options?.shape ?? "rows";
    options?.signal?.throwIfAborted();

    const { sql, params } = agStudioQueryToSql(query);
    console.log("[AlaSQL engine] execute:", { ...info, shape, sql, params });

    const rows = alasql(sql, params);

    // AlaSQL returns date values as ISO strings. Coerce them to Date objects
    // so AG Studio's date formatters (e.g. toLocaleDateString) work correctly.
    if (this.dateFieldKeys.size > 0) {
      for (const row of rows) {
        for (const key of Object.keys(row)) {
          if (this.dateFieldKeys.has(key) && typeof row[key] === "string") {
            row[key] = new Date(row[key] as string);
          }
        }
      }
    }
    applyRowId(query, rows);

    if (shape === "columns") {
      const columns = new Map<string, unknown[]>();
      for (const row of rows) {
        for (const [k, v] of Object.entries(row)) {
          let col = columns.get(k);
          if (!col) {
            col = [];
            columns.set(k, col);
          }
          col.push(v);
        }
      }
      return {
        dataShape: "columns",
        columns,
        metadata: { rowCount: rows.length },
      };
    }
    return { dataShape: "rows", rows, metadata: { rowCount: rows.length } };
  }
}

// ─── Studio component ───────────────────────────────────────────────────────

const VueExample = defineComponent({
  template: `
        <div style="height: 100%">
            <div style="display: flex; flex-direction: column; height: 100%">
                <div class="example-controls">
                    <div class="controls-row">
                        <button class="push-right" v-on:click="toggleMode()">
                            {{ mode === 'edit' ? 'View Mode' : 'Edit Mode' }}
                        </button>
                    </div>
                </div>
                <ag-studio
                    style="width: 100%; height: 100%;"
                    class="my-studio-container"
                    :initialState="initialState"
                    :mode="mode"
                    :data="data"></ag-studio>
            </div>
        </div>
    `,
  components: {
    "ag-studio": AgStudio,
  },
  setup() {
    const mode = ref<AgStudioMode>("view");

    // 1. Populate AlaSQL tables with sample data, then 2. create the engine -
    // Studio owns the engine lifecycle: init(context) → execute(requests) → dispose().
    setupAlaSqlTables();
    const data = shallowRef<AgDataEngine>(new AlaSqlDataEngine());

    const initialState = ref<AgReportState>({
      panels: {
        filters: {
          collapsed: true,
        },
      },
      pages: [
        {
          id: "sales-dashboard",
          widgets: {
            "region-filter": {
              type: "list-filter",
              dataMapping: {
                value: [{ id: "sales.region" }],
              },
              format: {
                title: { enabled: true, text: "Filter by Region" },
              },
            },
            "category-filter": {
              type: "list-filter",
              dataMapping: {
                value: [{ id: "sales.category" }],
              },
              format: {
                title: { enabled: true, text: "Filter by Category" },
              },
            },
            "revenue-by-region": {
              type: "bar-chart-grouped",
              dataMapping: {
                categoryKey: [{ id: "sales.region" }],
                valueKey: [{ id: "sales.revenue", aggregation: "sum" }],
              },
              format: {
                title: {
                  enabled: true,
                  text: "Revenue by Region",
                  typography: { fontSize: 16, fontWeight: "bold" },
                },
              },
            },
            "revenue-by-product": {
              type: "bar-chart-grouped",
              dataMapping: {
                categoryKey: [{ id: "sales.product" }],
                valueKey: [{ id: "sales.revenue", aggregation: "sum" }],
              },
              format: {
                title: {
                  enabled: true,
                  text: "Revenue by Product",
                  typography: { fontSize: 16, fontWeight: "bold" },
                },
              },
            },
            "sales-grid": {
              type: "grid",
              dataMapping: {
                cols: [
                  { id: "sales.region" },
                  { id: "sales.product" },
                  { id: "sales.category" },
                  { id: "sales.quantity", aggregation: "sum" },
                  { id: "sales.revenue", aggregation: "sum" },
                  { id: "sales.cost", aggregation: "sum" },
                ],
              },
              format: {
                title: {
                  enabled: true,
                  text: "Sales Summary",
                  typography: { fontSize: 16, fontWeight: "bold" },
                },
                style: {
                  grandTotalRow: { enabled: true },
                  theme: { rowHeight: 28 },
                },
              },
            },
          },
          widgetLayout: {
            "region-filter": { xTrack: 0, yTrack: 0, xSpan: 6, ySpan: 12 },
            "category-filter": { xTrack: 0, yTrack: 12, xSpan: 6, ySpan: 12 },
            "revenue-by-region": { xTrack: 6, yTrack: 0, xSpan: 9, ySpan: 24 },
            "revenue-by-product": {
              xTrack: 15,
              yTrack: 0,
              xSpan: 9,
              ySpan: 24,
            },
            "sales-grid": { xTrack: 0, yTrack: 24, xSpan: 24, ySpan: 14 },
          },
          filter: {
            page: [],
          },
        },
      ],
      selectedPageId: "sales-dashboard",
    });

    const toggleMode = () => {
      mode.value = mode.value === "edit" ? "view" : "edit";
    };

    return {
      mode,
      data,
      initialState,
      toggleMode,
    };
  },
});

const app = createApp(VueExample);
app.mount("#app");
```

[Live example: AlaSQL Server Side](https://www.ag-grid.com/studio/examples/server-side-data/alasql-serverside/vue3/)

## When to Use Server-Side Data

The built-in engine loads all row data into the browser and runs operations in-memory. This works well when the dataset fits in browser memory, you can afford the initial transfer latency, and you want instant filtering and sorting with no server round-trip.

Adopt Server-Side Data when:

- **Dataset is too large:** The data cannot be shipped to the browser and must be queried remotely.
- **Analytics backend:** You already have a system (e.g. ClickHouse, Snowflake, BigQuery, a REST API) that serves aggregated data.
- **Computation delegation:** You want to push aggregation, filtering, and sorting to a database engine rather than compute them client-side.

## The AgDataEngine Interface

Implement the `AgDataEngine` interface. Your engine declares its data sources via `getDataSources()`, executes queries via `execute()`, and optionally performs async setup in `init()`. If your engine discovers its schema from a remote service, await that discovery in `init()` and return the result from `getDataSources()`.

```ts
export class MyDataEngine implements AgDataEngine {
    async init(): Promise<void> {
        // Optional: async setup before the schema is consulted.
    }

    getDataSources(): AgDataSourcesDefinition {
        return {
            sources: [
                {
                    id: 'sales',
                    fields: [
                        { id: 'region', format: 'textFormat' },
                        { id: 'revenue', format: 'numberFormat' },
                    ],
                },
            ],
        };
    }

    async execute(...requests: AgExecuteRequest<AgResultShape>[]): Promise<AgExecuteResult[]> {
        return Promise.all(requests.map((req) => this.runOne(req)));
    }

    private async runOne(request: AgExecuteRequest<AgResultShape>): Promise<AgExecuteResult> {
        const { query } = request;
        const rows = await this.queryBackend(query); // Your backend call
        return { dataShape: 'rows', rows, metadata: { rowCount: rows.length } };
    }
}
```

Properties available on the `AgDataEngine` interface.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `init` | `Function` |  | Optional async lifecycle hook called by Studio before the engine is queried. Use this to bootstrap resources that must resolve before the schema is consulted - wasm compilation, HTTP fetches, database connections. Studio awaits this before calling getDataSources or finalize. Engines with no async setup can omit this entirely. |
| `getDataSources` | `Function` |  | Declare the data sources the engine exposes to Studio. Called once, after init resolves. Studio uses this to build the canonical schema it operates against. The return value is the same AgDataSourcesDefinition shape a caller would pass on the `data` property when using the built-in engine. Engines that know their fields upfront can build this eagerly in a constructor; engines that discover fields from a remote service will typically await that discovery in init and return the resulting definition here. Schema is read once per engine lifecycle; if your underlying schema can change, rebuild the engine on the host application side. |
| `finalize` | `Function` |  | Called after getDataSources once Studio has built its schema view. Use this to perform any one-time post-schema setup. Engines with nothing to do here can omit this method. |
| `execute` | `Function` |  | Execute one or more queries. Results are returned in request order, one AgExecuteResult per AgExecuteRequest. Studio batches requests that arrive together (typically within one render cycle). All requests in a batch share `info.batchId`. Engines that can coalesce backend calls should group by `batchId`. Cancellation: each request carries an optional `options.signal` that Studio aborts when the batch is superseded. Propagate it into your backend call (e.g. pass to `fetch`). |
| `executeCube` | `Function` |  | Execute one or more cube queries, returning one AgCubeResult per request. Optional capability - engines that implement this unlock pivot and nested-tree widgets over server-side data; engines that omit it cannot serve those widget types. |
| `reload` | `Function` |  | Invalidate any cached state so the next query recomputes against the current data. Engines without caches can omit this. |
| `addEventListener` | `Function` |  | Subscribe a listener to validation events the engine emits. Engines that never emit validation events can omit this. If you implement this method, you MUST also implement removeEventListener - Studio calls it on teardown. |
| `removeEventListener` | `Function` |  | Unsubscribe a listener previously registered via addEventListener. |
| `dispose` | `Function` |  | Release large data structures to reduce GC pressure on page unload. |
| `update` | `Function` |  | Apply in-place updates to the engine's data sources. Engines that manage their data externally (read-only backends, on-demand fetchers) can omit this. |

### Using Your Engine

```ts
<ag-studio
    :data="data"
    /* other studio properties ... */>
</ag-studio>

this.data = new MyDataEngine();
```

Studio calls `init()` during startup, then `getDataSources()` once to freeze the schema. From that point on, Studio calls `execute()` as the user interacts with the dashboard.

> **Note**
>
> `getDataSources()` is called once per engine lifecycle. The schema is frozen after that call; Studio will not re-read it. If your underlying schema changes at runtime (e.g. new columns added to a database), destroy the Studio instance and create a new one with a fresh engine.

## Migrating from the Built-In Engine

Swap the `data` property from an inline data definition to an engine instance:

**Before:**

```ts
<ag-studio
    :data="data"
    /* other studio properties ... */>
</ag-studio>

this.data = {
    sources: [{
        id: 'sales',
        fields: [/* ... */],
        data: [/* rows */]
    }]
};
```

**After:**

```ts
<ag-studio
    :data="data"
    /* other studio properties ... */>
</ag-studio>

this.data = new MyDataEngine();
```

Extract the schema from your current config into your engine's `getDataSources()`, then implement query translation in `execute()`. See [Server-Side Implementation](https://www.ag-grid.com/studio/vue/server-side-data-implementation/) for the full query anatomy.
