Domo Custom App Development in Replit – AI Agent Tutorial

This guide explains how to set up a Domo Custom App (pro-code-editor app) so it can run in Replit or any similar Node-based server environment, without the actual Domo app runtime. This is especially useful for AI agents (or human developers) that are:

  • Migrating designs from Figma into Domo apps
  • Building, testing, and debugging Domo custom apps outside of Domo
  • Needing a repeatable dev environment (Replit / Codespaces / local Node server)

Overview: How This Works

A Domo custom app normally runs inside domoapps.*.domo.com and uses window.domo to talk to the Domo platform (datasets, users, AppDB, etc.).

In Replit, we don’t have window.domo. Instead, we create a simple proxy server that talks to the Domo APIs using OAuth 2.0 client credentials. The frontend uses regular fetch() calls against this proxy, and the proxy forwards requests to api.domo.com with a Bearer token.

Architecture Comparison

┌─────────────────────────────────────────────────────────────────┐
│                    INSIDE DOMO ENVIRONMENT                      │
├─────────────────────────────────────────────────────────────────┤
│  Browser (domoapps.*.domo.com)                                  │
│      │                                                          │
│      ▼                                                          │
│  window.domo.get('/data/v1/dataset-alias')                      │
│      │                                                          │
│      ▼                                                          │
│  Domo Platform (automatic auth, data access)                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    REPLIT DEVELOPMENT                           │
├─────────────────────────────────────────────────────────────────┤
│  Browser (localhost or Replit preview)                          │
│      │                                                          │
│      ▼                                                          │
│  fetch('/api/dataset/:id')  (regular HTTP)                      │
│      │                                                          │
│      ▼                                                          │
│  simple-server.js (OAuth proxy)                                 │
│      │                                                          │
│      ▼                                                          │
│  Domo API (api.domo.com) with OAuth Bearer token                │
└─────────────────────────────────────────────────────────────────┘
Goal: Keep your app code mostly unchanged between Replit and Domo. Only the transport layer (how you call the API) should switch automatically.

Required Secrets (Environment Variables)

In Replit (or any similar platform), add the following as Secrets / Environment Variables:

Secret Name Description How to Get It
DOMO_CLIENT_ID OAuth Client ID Domo Admin > Authentication > Client IDs
DOMO_CLIENT_SECRET OAuth Client Secret Generated when you create the Client ID
DOMO_INSTANCE Your Domo domain For example: yourcompany.domo.com
DOMO_DATASET_ID Dataset UUID Copy from the Domo dataset URL

Getting OAuth Credentials from Domo

  1. Log into Domo as an Admin.
  2. Go to Admin > Authentication > Client IDs.
  3. Click + New Client ID.
  4. Name it something like Replit Development.
  5. Select scope: at minimum data (for dataset access).
  6. After creating, immediately copy:
    • Client ID
    • Client Secret (visible only once!)
Important: Store the Client Secret securely (Replit Secrets, Vault, etc.). Treat it like a password.

Key Files Explained

1. simple-server.js – The OAuth Proxy Server

This file is the heart of your Replit / external dev environment. It:

  • Retrieves and caches an OAuth token from Domo
  • Proxies requests to the Domo API with Authorization: Bearer <token>
  • Serves static files from the dist directory (built frontend)

Critical code concepts:

// OAuth Token Retrieval (concept)
async function getToken() {
  // Uses CLIENT_ID:CLIENT_SECRET for Basic Auth header
  const auth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64");

  // POST https://api.domo.com/oauth/token
  // Body: grant_type=client_credentials&scope=data
  // Returns: { access_token, token_type, expires_in, ... }
}

// Dataset Query Proxy (concept)
async function queryDataset(datasetId, sql) {
  const token = await getToken();
  // POST https://api.domo.com/v1/datasets/query/execute/{datasetId}
  // Headers: Authorization: Bearer <token>
  // Body: { "sql": "SELECT * FROM table LIMIT 100" }
}

2. src/api/domo/environment.js – Environment Detection

This module decides whether your code is running:

  • Inside Domo (with window.domo available)
  • Outside Domo (Replit / local / other server)
export function isDomoEnvironment() {
  // Returns true if:
  // 1. window.domo exists with the expected methods, and
  // 2. The hostname looks like domoapps.* or *.domo.com
}

export async function universalGet(path) {
  if (isDomoEnvironment()) {
    // Use Domo SDK inside Domo
    return window.domo.get(path);
  } else {
    // Use proxy or local API for dev
    return fetch(path).then(r => r.json());
  }
}

You can also define universalPost, universalPut, and universalDelete using the same pattern.

3. dev-runner.js – Development Server Orchestrator

This script decides which auth/config mode to run in when you start the app in dev mode.

const HAS_OAUTH =
  process.env.DOMO_CLIENT_ID && process.env.DOMO_CLIENT_SECRET;

if (HAS_OAUTH) {
  startWithOAuth(); // Uses simple-server.js with Domo APIs
} else {
  console.log("No OAuth credentials found. Exiting.");
  process.exit(1);
}

4. manifest.json – Domo App Configuration

The manifest defines how Domo sees your app: datasets, aliases, and collections.

{
  "id": "app-uuid",
  "proxyId": "proxy-uuid",
  "datasetsMapping": [
    {
      "dataSetId": "dataset-uuid",
      "alias": "myDataAlias",
      "fields": []
    }
  ],
  "collections": [
    {
      "name": "Reviews",
      "schema": { /* ... */ }
    }
  ]
}

API Endpoints Exposed by the Proxy

The simple-server.js proxy typically exposes a small set of endpoints the frontend can use:

Endpoint Method Description
/api/dataset/:id GET Query dataset (default ~50 rows)
/api/dataset/:id?limit=N GET Same as above, but with a custom row limit
/api/dataset/:id/metadata GET Dataset info (row count, columns, etc.)
/api/dataset/:id/schema GET Column definitions / schema
/api/appdb/init GET Initialize AppDB collection(s) used by the app

Step-by-Step Setup (Replit or Similar Server)

Step 1: Check Project Structure

project-root/
├── simple-server.js      # OAuth proxy (REQUIRED)
├── dev-runner.js         # Dev orchestrator (REQUIRED)
├── manifest.json         # Domo app config (REQUIRED)
├── src/
│   └── api/
│       └── domo/
│           └── environment.js  # Environment detection (REQUIRED)
├── dist/                 # Built static files (frontend)
└── package.json

Step 2: Install Dependencies

Ensure your package.json includes (minimal examples):

  • http / https (Node.js built-in, no install required)
  • ryuu.js – Domo client library if you use it in browser-side code
  • Bundler such as webpack (or Vite, etc.) for building the app into dist

Step 3: Configure package.json Scripts

{
  "scripts": {
    "dev": "node dev-runner.js",
    "build": "webpack --mode production",
    "start": "node simple-server.js"
  }
}

Step 4: Request OAuth Secrets from User (for AI Agents)

When an AI agent or setup script prepares the environment, it should collect:

  • DOMO_CLIENT_ID
  • DOMO_CLIENT_SECRET
  • DOMO_INSTANCE (optional, defaults to domo.com)
  • DOMO_DATASET_ID (primary dataset for testing)

These values are then injected as environment variables or Replit Secrets.

Step 5: Test the Connection

  1. Run npm run dev or npm start in Replit.
  2. Check logs for messages like: "OAuth token obtained".
  3. Open the Replit preview URL in the browser.
  4. Trigger a request to /api/dataset/<DOMO_DATASET_ID>.
  5. Confirm that:
    • No 401 Unauthorized or 403 Forbidden errors.
    • The response contains real rows from your Domo dataset.

Common Issues and Solutions

Issue: Token error 401

Cause: Invalid OAuth credentials.

Fix:

  • Verify DOMO_CLIENT_ID and DOMO_CLIENT_SECRET.
  • Ensure you are using the correct Auth URL: https://api.domo.com/oauth/token.

Issue: Query error 403

Cause: OAuth scope does not include data access.

Fix:

  • Regenerate OAuth credentials in Domo with the data scope.
  • Confirm that the associated user has access to the dataset.

Issue: CORS Errors in Browser

Cause: Browser blocking cross-origin requests.

Fix: Add CORS headers in simple-server.js:

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
  "Access-Control-Allow-Headers": "*"
};

Issue: "Dataset not found"

Cause: Wrong dataset ID or no access.

Fix:

  • Copy the dataset ID from the Domo dataset URL carefully.
  • Confirm that the OAuth client user has permission to read that dataset.

Domo SQL Query Syntax Reminder

When querying through /v1/datasets/query/execute, Domo always uses a virtual table named table (not the dataset name).

-- Correct
SELECT * FROM table LIMIT 100;
SELECT column1, column2 FROM table WHERE status = 'active';

-- Wrong (will fail)
SELECT * FROM my_dataset_name;

Frontend Integration Pattern

In your React / JS components, use the environment detection helpers to write code that works both inside Domo and in your dev environment:

// Example React/JS data fetcher
import { isDomoEnvironment, universalGet } from "@/api/domo/environment";

async function fetchData() {
  if (isDomoEnvironment()) {
    // Inside Domo: use dataset alias from manifest.json
    return window.domo.get("/data/v1/yourapp");
  } else {
    // Development: use proxy endpoint with dataset ID
    const datasetId = "bbbbb1b-1111-1a11-111c-1d1111111a1";
    return fetch(`/api/dataset/${datasetId}?limit=100`)
      .then(r => r.json());
  }
}

Port Configuration (Replit & Other Hosts)

The proxy server often runs on 3001 locally, but Replit expects your app to listen on its own assigned port (often exposed via process.env.PORT).

const PORT = process.env.PORT || 3001;
  • On Replit: respect process.env.PORT.
  • On local dev: you can default to 3001.

Publishing Back to Domo

When your app is ready for production:

  1. Build static files: npm run build (outputs to dist/).
  2. Use Domo CLI (domo publish) or manually upload the dist assets.
  3. In the Domo pro-code editor, ensure manifest.json and assets are aligned.

Your universalGet/Post/Put/Delete helpers will automatically:

  • Use window.domo when running inside Domo.
  • Use fetch() to talk to the proxy when in Replit / external dev.

Summary for AI Agents & Developers

  1. Check the project has simple-server.js and environment.js.
  2. Request OAuth credentials from the user: DOMO_CLIENT_ID, DOMO_CLIENT_SECRET, DOMO_INSTANCE, and DOMO_DATASET_ID.
  3. Configure the port to match the host requirements (Replit uses process.env.PORT).
  4. Test that proxy endpoints like /api/dataset/:id return real Domo data.
  5. Use universalGet/Post wrappers so the same code works both in Domo and in Replit.

With this pattern, you can build and iterate on Domo custom apps completely outside the Domo runtime while keeping deployment back to Domo smooth and low-friction.