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 │
└─────────────────────────────────────────────────────────────────┘ 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
- Log into Domo as an Admin.
- Go to Admin > Authentication > Client IDs.
- Click + New Client ID.
- Name it something like Replit Development.
- Select scope: at minimum
data(for dataset access). -
After creating, immediately copy:
- Client ID
- Client Secret (visible only once!)
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
distdirectory (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.domoavailable) - 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 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 intodist
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_IDDOMO_CLIENT_SECRETDOMO_INSTANCE(optional, defaults todomo.com)DOMO_DATASET_ID(primary dataset for testing)
These values are then injected as environment variables or Replit Secrets.
Step 5: Test the Connection
- Run
npm run devornpm startin Replit. - Check logs for messages like: "OAuth token obtained".
- Open the Replit preview URL in the browser.
- Trigger a request to
/api/dataset/<DOMO_DATASET_ID>. -
Confirm that:
- No
401 Unauthorizedor403 Forbiddenerrors. - The response contains real rows from your Domo dataset.
- No
Common Issues and Solutions
Issue: Token error 401
Cause: Invalid OAuth credentials.
Fix:
- Verify
DOMO_CLIENT_IDandDOMO_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
datascope. - 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 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());
}
} Ports 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:
- Build static files:
npm run build(outputs todist/). - Use Domo CLI (
domo publish) or manually upload thedistassets. - In the Domo pro-code editor, ensure
manifest.jsonand assets are aligned.
Your universalGet/Post/Put/Delete helpers will automatically:
- Use
window.domowhen running inside Domo. - Use
fetch()to talk to the proxy when in Replit / external dev.
Summary for AI Agents & Developers
- Check the project has
simple-server.jsandenvironment.js. - Request OAuth credentials from the user:
DOMO_CLIENT_ID,DOMO_CLIENT_SECRET,DOMO_INSTANCE, andDOMO_DATASET_ID. - Configure the port to match the host requirements (Replit uses
process.env.PORT). - Test that proxy endpoints like
/api/dataset/:idreturn real Domo data. - Use
universalGet/Postwrappers 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.