> ## Documentation Index
> Fetch the complete documentation index at: https://domoinc-openapi-sync-dataflows.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Todo App with AppDB

### Intro

***

This tutorial walks through building a shared Todo app with React, TypeScript, Redux Toolkit, and [AppDB](/portal/API-Reference/app-framework-apis/AppDB-API). The app is scaffolded with the [DA CLI](/portal/Apps/App-Framework/Tools/da-cli), loads the current Domo user's identity and avatar, and stamps ownership onto every todo so users can see who added what.

You'll learn the patterns you'll reach for in every real Domo app:

* Scaffold a Vite + React + TypeScript project with `da new`
* Declare an AppDB collection in `manifest.json` and publish an initial design
* Build a typed service layer over `AppDBClient`, `IdentityClient`, and `UserClient`
* Manage async state with Redux Toolkit's `buildCreateSlice` + `asyncThunkCreator`
* Compose small, presentational components that read from and dispatch against the store

The finished code is at [DomoApps/basic-react-app-todo-tutorial on GitHub](https://github.com/DomoApps/basic-react-app-todo-tutorial).

<Note>
  **Prerequisite:** Complete the [Setup and Installation](/portal/Apps/App-Framework/Quickstart/Setup-and-Installation) guide and run `domo login` before starting.
</Note>

### Step 1: Install the DA CLI and scaffold the app

***

The [DA CLI](/portal/Apps/App-Framework/Tools/da-cli) clones the [@domoinc/vite-react-template](https://www.npmjs.com/package/@domoinc/vite-react-template) — a Vite + React + TypeScript project preconfigured with the Domo proxy, ESLint, Prettier, Vitest, Storybook, and `da generate` scaffolding.

Install the CLI globally:

```bash theme={"dark"}
# pnpm (recommended)
pnpm add -g @domoinc/da

# or yarn
yarn global add @domoinc/da

# or npm
npm install -g @domoinc/da
```

Create the project:

```bash theme={"dark"}
da new todo-appdb-demo
cd todo-appdb-demo
```

`da new` prompts for a package manager (pick **pnpm**), clones the template, writes your app name, initializes git, and installs dependencies.

<Tip>
  App names must be lowercase with hyphens only. Capitals, underscores, and periods are rejected.
</Tip>

The scaffold gives you:

* `public/manifest.json` — app metadata, size, collections
* `public/thumbnail.png` — thumbnail shown in the Asset Library
* `src/main.tsx` — React entry point
* `src/manifestOverrides.json` — per-environment manifest overrides (dev/qa/prod)
* A Vite dev server wired through `@domoinc/ryuu-proxy` to your Domo instance
* Scripts: `start`, `build`, `upload`, `generate`, `test`, `storybook`

Add the two runtime dependencies we'll use beyond the scaffold:

```bash theme={"dark"}
pnpm add @reduxjs/toolkit react-redux @domoinc/toolkit
```

### Step 2: Declare the Todos collection

***

Replace `public/manifest.json` with:

```json theme={"dark"}
{
  "name": "Todo AppDB Demo",
  "version": "0.0.1",
  "size": { "width": 5, "height": 3 },
  "fullpage": true,
  "mapping": [],
  "collections": [
    {
      "name": "Todos",
      "syncEnabled": true,
      "schema": {
        "columns": [
          { "name": "title", "type": "STRING" },
          { "name": "completed", "type": "STRING" },
          { "name": "priority", "type": "STRING" },
          { "name": "dueDate", "type": "STRING" },
          { "name": "ownerId", "type": "STRING" },
          { "name": "ownerName", "type": "STRING" }
        ]
      },
      "defaultPermission": [
        "READ",
        "READ_CONTENT",
        "CREATE_CONTENT",
        "UPDATE_CONTENT",
        "DELETE_CONTENT"
      ]
    }
  ]
}
```

<Note>
  **Note:** AppDB columns are always `STRING`. Booleans are stored as `"true"` / `"false"` and parsed at the UI layer. `ownerId` and `ownerName` give us the basis for per-user ownership displays and filters.
</Note>

### Step 3: Publish the initial design and wire the proxy

***

Before local dev can read or write documents, Domo needs to provision the collection and hand you a `proxyId`.

```bash theme={"dark"}
pnpm upload
```

This runs `pnpm build` and `domo publish` from the `build/` folder. The output prints a link to the new App Design.

Then, in the Domo UI:

1. Open the App Design link.
2. Click **New Card** → **Select Collection** → **Create New** → **Create Collection** to provision an empty `Todos` collection.
3. Save the card.

Copy the App Design `id` and the card's `proxyId` from the design page, and add both to `public/manifest.json`:

```json theme={"dark"}
{
  "id": "23307940-6dfe-40c4-86f8-8a7b0f5d8b3a",
  "proxyId": "95bd96f9-0385-465a-b485-c16935cf771a"
}
```

<Tip>
  For multi-environment apps, use `da manifest` to add overrides to `src/manifestOverrides.json` instead of hand-editing `manifest.json`. `da apply-manifest` runs automatically as a `prestart` / `prebuild` hook.
</Tip>

### Step 4: Types and service layer

***

Create `src/services/types.ts`:

```typescript theme={"dark"}
import type { Identity, User } from '@domoinc/toolkit/src/models';

export interface TodoData {
  title: string;
  completed: string;   // "true" | "false"
  priority: string;    // "low" | "medium" | "high"
  dueDate: string;
  ownerId: string;
  ownerName: string;
}

export interface Todo extends TodoData {
  id: string;
  createdOn?: string;
  updatedOn?: string;
}

export interface UserInfo {
  identity: Identity;
  user: User;
  avatarUrl: string;
}
```

Create `src/services/app.ts`:

```typescript theme={"dark"}
import type { AppDBDocument } from '@domoinc/toolkit';
import { AppDBClient, IdentityClient, UserClient } from '@domoinc/toolkit';

import type { Todo, TodoData, UserInfo } from './types';

const TodosClient = new AppDBClient.DocumentsClient<TodoData>('Todos');

const extractDocs = (docs: AppDBDocument<TodoData>[]): Todo[] =>
  docs.map((doc) => ({
    id: doc.id,
    createdOn: doc.createdOn,
    updatedOn: doc.updatedOn,
    ...doc.content,
  }));

export const AppService = {
  async loadUserInfo(): Promise<UserInfo> {
    const identity = (await IdentityClient.get(undefined, true)).data;
    const user = (await UserClient.getUser(identity.userId, true)).data;
    const avatarUrl = `/api/content/v1/avatar/USER/${identity.userId}?size=100${
      user.avatarKey ? `&v=${encodeURIComponent(user.avatarKey)}` : ''
    }`;
    return { identity, user, avatarUrl };
  },

  async loadTodos(): Promise<Todo[]> {
    const response = await TodosClient.get(
      {},
      { orderby: ['createdOn descending'] },
    );
    return extractDocs(response.data);
  },

  async createTodo(data: TodoData): Promise<Todo> {
    const response = await TodosClient.create(data);
    return extractDocs(
      Array.isArray(response.data) ? response.data : [response.data],
    )[0];
  },

  async updateTodo(id: string, data: TodoData): Promise<Todo> {
    const response = await TodosClient.update({ id, content: data });
    return extractDocs([response.data])[0];
  },

  async deleteTodo(id: string): Promise<string> {
    await TodosClient.delete(id);
    return id;
  },
};
```

Two things worth calling out:

* **`IdentityClient` + `UserClient`** give you the current user's ID, email, display name, role, and avatar — everything you need to stamp ownership on documents and render a friendly header.
* **`extractDocs`** flattens the AppDB envelope (`{ id, content, createdOn, ... }`) into plain todo objects, so the rest of the app never has to think about document metadata.

### Step 5: Redux store with `buildCreateSlice`

***

We'll use Redux Toolkit's `buildCreateSlice` with the `asyncThunkCreator` so that async thunks can be declared inline on each slice — no separate `actions.ts` file.

Create `src/reducers/createAppSlice.ts`:

```typescript theme={"dark"}
import { asyncThunkCreator, buildCreateSlice } from '@reduxjs/toolkit';

export const createAppSlice = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator },
});
```

Create `src/reducers/index.ts`:

```typescript theme={"dark"}
import {
  type Action,
  configureStore,
  type ThunkAction,
} from '@reduxjs/toolkit';
import {
  type TypedUseSelectorHook,
  useDispatch,
  useSelector,
} from 'react-redux';

import AppSlice from './app/slice';
import TodosSlice from './todos/slice';

export const store = configureStore({
  reducer: {
    app: AppSlice,
    todos: TodosSlice,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```

**App slice — user identity.** Create `src/reducers/app/slice.ts`:

```typescript theme={"dark"}
import { AppService } from 'services/app';
import type { UserInfo } from 'services/types';

import { createAppSlice } from '../createAppSlice';

interface State {
  userInfo: UserInfo | null;
  loading: boolean;
  error: string | null;
}

const initialState: State = {
  userInfo: null,
  loading: false,
  error: null,
};

export const AppSlice = createAppSlice({
  name: 'app',
  initialState,
  reducers: (create) => ({
    loadUserInfo: create.asyncThunk<UserInfo, void>(
      async () => await AppService.loadUserInfo(),
      {
        pending: (state) => {
          state.loading = true;
          state.error = null;
        },
        fulfilled: (state, { payload }) => {
          state.loading = false;
          state.userInfo = payload;
        },
        rejected: (state, action) => {
          state.loading = false;
          state.error = action.error.message ?? 'Failed to load user';
        },
      },
    ),
  }),
  selectors: {
    selectUserInfo: (state) => state.userInfo,
    selectUserLoading: (state) => state.loading,
    selectUserError: (state) => state.error,
  },
});

export const { loadUserInfo } = AppSlice.actions;
export const { selectUserInfo, selectUserLoading, selectUserError } =
  AppSlice.selectors;

export default AppSlice.reducer;
```

**Todos slice — CRUD thunks.** Create `src/reducers/todos/slice.ts`:

```typescript theme={"dark"}
import { AppService } from 'services/app';
import type { Todo, TodoData } from 'services/types';

import { createAppSlice } from '../createAppSlice';

interface State {
  items: Todo[];
  loading: boolean;
  saving: boolean;
  error: string | null;
}

const initialState: State = {
  items: [],
  loading: false,
  saving: false,
  error: null,
};

export const TodosSlice = createAppSlice({
  name: 'todos',
  initialState,
  reducers: (create) => ({
    loadTodos: create.asyncThunk<Todo[], void>(
      async () => await AppService.loadTodos(),
      {
        pending: (state) => {
          state.loading = true;
          state.error = null;
        },
        fulfilled: (state, { payload }) => {
          state.loading = false;
          state.items = payload;
        },
        rejected: (state, action) => {
          state.loading = false;
          state.error = action.error.message ?? 'Failed to load todos';
        },
      },
    ),

    createTodo: create.asyncThunk<Todo, TodoData>(
      async (data) => await AppService.createTodo(data),
      {
        pending: (state) => {
          state.saving = true;
        },
        fulfilled: (state, { payload }) => {
          state.saving = false;
          state.items = [payload, ...state.items];
        },
        rejected: (state, action) => {
          state.saving = false;
          state.error = action.error.message ?? 'Failed to create todo';
        },
      },
    ),

    updateTodo: create.asyncThunk<Todo, { id: string; data: TodoData }>(
      async ({ id, data }) => await AppService.updateTodo(id, data),
      {
        pending: (state) => {
          state.saving = true;
        },
        fulfilled: (state, { payload }) => {
          state.saving = false;
          state.items = state.items.map((t) =>
            t.id === payload.id ? payload : t,
          );
        },
        rejected: (state, action) => {
          state.saving = false;
          state.error = action.error.message ?? 'Failed to update todo';
        },
      },
    ),

    toggleTodo: create.asyncThunk<Todo, Todo>(
      async (todo) => {
        const data: TodoData = {
          title: todo.title,
          completed: todo.completed === 'true' ? 'false' : 'true',
          priority: todo.priority,
          dueDate: todo.dueDate,
          ownerId: todo.ownerId,
          ownerName: todo.ownerName,
        };
        return await AppService.updateTodo(todo.id, data);
      },
      {
        fulfilled: (state, { payload }) => {
          state.items = state.items.map((t) =>
            t.id === payload.id ? payload : t,
          );
        },
      },
    ),

    deleteTodo: create.asyncThunk<string, string>(
      async (id) => await AppService.deleteTodo(id),
      {
        fulfilled: (state, { payload }) => {
          state.items = state.items.filter((t) => t.id !== payload);
        },
      },
    ),
  }),
  selectors: {
    selectTodos: (state) => state.items,
    selectTodosLoading: (state) => state.loading,
    selectTodosSaving: (state) => state.saving,
    selectTodosError: (state) => state.error,
  },
});

export const { loadTodos, createTodo, updateTodo, toggleTodo, deleteTodo } =
  TodosSlice.actions;
export const {
  selectTodos,
  selectTodosLoading,
  selectTodosSaving,
  selectTodosError,
} = TodosSlice.selectors;

export default TodosSlice.reducer;
```

A few patterns to notice:

* **`loading` vs `saving`** — `loading` covers the initial list fetch; `saving` is toggled by creates, updates, and toggles so the form can disable its submit button without blanking the list.
* **`toggleTodo`** takes the whole todo (not just an ID) so the reducer can build the full `TodoData` payload without refetching.
* **Selectors declared on the slice** — callers never reach into `state.todos.items` directly; they import `selectTodos` and get type safety for free.

### Step 6: Components

***

Each component lives in its own folder under `src/components/` with a matching `.module.scss`. We'll show the TypeScript; grab the SCSS from the [GitHub source](https://github.com/DomoApps/basic-react-app-todo-tutorial).

**UserHeader** renders the current user's avatar, name, role, and email, with a gradient-initials fallback if the avatar URL 404s.

```tsx theme={"dark"}
// src/components/UserHeader/UserHeader.tsx
import { FC, useState } from 'react';
import { useAppSelector } from 'reducers';
import { selectUserInfo, selectUserLoading } from 'reducers/app/slice';

import styles from './UserHeader.module.scss';

export const UserHeader: FC = () => {
  const userInfo = useAppSelector(selectUserInfo);
  const loading = useAppSelector(selectUserLoading);
  const [avatarFailed, setAvatarFailed] = useState(false);

  if (loading && !userInfo) {
    return <div className={styles.header}>Loading user…</div>;
  }
  if (!userInfo) return null;

  const { identity, user, avatarUrl } = userInfo;
  const initials = user.displayName
    ?.split(' ')
    .map((part) => part[0])
    .filter(Boolean)
    .slice(0, 2)
    .join('')
    .toUpperCase();

  return (
    <div className={styles.header}>
      {avatarFailed ? (
        <div className={styles.avatarFallback}>{initials || '?'}</div>
      ) : (
        <img
          src={avatarUrl}
          alt={user.displayName}
          className={styles.avatar}
          onError={() => setAvatarFailed(true)}
        />
      )}
      <div className={styles.info}>
        <div className={styles.name}>{user.displayName}</div>
        <div className={styles.meta}>{identity.userEmail}</div>
        {user.detail?.title && (
          <div className={styles.meta}>{user.detail.title}</div>
        )}
        <span className={styles.role}>{user.role}</span>
      </div>
    </div>
  );
};
```

**TodoForm** is a simple controlled form that stamps `ownerId` / `ownerName` from the store onto every new todo at submit time.

```tsx theme={"dark"}
// src/components/TodoForm/TodoForm.tsx
import { FC, FormEvent, useState } from 'react';
import { useAppDispatch, useAppSelector } from 'reducers';
import { selectUserInfo } from 'reducers/app/slice';
import { createTodo, selectTodosSaving } from 'reducers/todos/slice';

import styles from './TodoForm.module.scss';

export const TodoForm: FC = () => {
  const dispatch = useAppDispatch();
  const userInfo = useAppSelector(selectUserInfo);
  const saving = useAppSelector(selectTodosSaving);

  const [title, setTitle] = useState('');
  const [priority, setPriority] = useState('medium');
  const [dueDate, setDueDate] = useState('');

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!title.trim() || !userInfo) return;

    await dispatch(
      createTodo({
        title: title.trim(),
        completed: 'false',
        priority,
        dueDate,
        ownerId: String(userInfo.identity.userId),
        ownerName: userInfo.user.displayName,
      }),
    );

    setTitle('');
    setDueDate('');
    setPriority('medium');
  };

  return (
    <form className={styles.form} onSubmit={handleSubmit}>
      <input
        type="text"
        className={styles.title}
        placeholder="What needs to be done?"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <select
        className={styles.select}
        value={priority}
        onChange={(e) => setPriority(e.target.value)}
      >
        <option value="low">Low</option>
        <option value="medium">Medium</option>
        <option value="high">High</option>
      </select>
      <input
        type="date"
        className={styles.date}
        value={dueDate}
        onChange={(e) => setDueDate(e.target.value)}
      />
      <button
        type="submit"
        className={styles.submit}
        disabled={saving || !title.trim() || !userInfo}
      >
        {saving ? 'Adding…' : 'Add Todo'}
      </button>
    </form>
  );
};
```

**TodoItem** renders a single row — checkbox, title, owner, due date, and delete button. A priority class on the container drives the colored left border.

```tsx theme={"dark"}
// src/components/TodoItem/TodoItem.tsx
import { FC } from 'react';
import { useAppDispatch } from 'reducers';
import { deleteTodo, toggleTodo } from 'reducers/todos/slice';
import type { Todo } from 'services/types';

import styles from './TodoItem.module.scss';

interface Props {
  todo: Todo;
}

const priorityClass: Record<string, string> = {
  high: styles.priorityHigh,
  medium: styles.priorityMedium,
  low: styles.priorityLow,
};

export const TodoItem: FC<Props> = ({ todo }) => {
  const dispatch = useAppDispatch();
  const isCompleted = todo.completed === 'true';

  return (
    <li className={`${styles.item} ${priorityClass[todo.priority] ?? ''}`}>
      <input
        type="checkbox"
        className={styles.checkbox}
        checked={isCompleted}
        onChange={() => dispatch(toggleTodo(todo))}
      />
      <div className={styles.content}>
        <div className={`${styles.title} ${isCompleted ? styles.completed : ''}`}>
          {todo.title}
        </div>
        <div className={styles.meta}>
          <span>By {todo.ownerName || 'Unknown'}</span>
          {todo.dueDate && <span>Due {todo.dueDate}</span>}
          <span>{todo.priority}</span>
        </div>
      </div>
      <button
        type="button"
        className={styles.delete}
        onClick={() => dispatch(deleteTodo(todo.id))}
        aria-label="Delete todo"
      >
        ×
      </button>
    </li>
  );
};
```

**TodoList** handles filtering (all / active / completed) and the count header.

```tsx theme={"dark"}
// src/components/TodoList/TodoList.tsx
import { TodoItem } from 'components/TodoItem/TodoItem';
import { FC, useState } from 'react';
import { useAppSelector } from 'reducers';
import { selectTodos, selectTodosLoading } from 'reducers/todos/slice';

import styles from './TodoList.module.scss';

type Filter = 'all' | 'active' | 'completed';

export const TodoList: FC = () => {
  const todos = useAppSelector(selectTodos);
  const loading = useAppSelector(selectTodosLoading);
  const [filter, setFilter] = useState<Filter>('all');

  const filtered = todos.filter((t) => {
    if (filter === 'active') return t.completed !== 'true';
    if (filter === 'completed') return t.completed === 'true';
    return true;
  });
  const activeCount = todos.filter((t) => t.completed !== 'true').length;

  if (loading && todos.length === 0) {
    return <div className={styles.count}>Loading todos…</div>;
  }

  return (
    <div>
      <div className={styles.header}>
        <span className={styles.count}>
          {activeCount} active · {todos.length} total
        </span>
        <div className={styles.filters}>
          {(['all', 'active', 'completed'] as Filter[]).map((f) => (
            <button
              key={f}
              type="button"
              className={`${styles.filterBtn} ${filter === f ? styles.active : ''}`}
              onClick={() => setFilter(f)}
            >
              {f}
            </button>
          ))}
        </div>
      </div>
      {filtered.length === 0 ? (
        <div className={styles.count} style={{ padding: '16px 0' }}>
          No todos to show.
        </div>
      ) : (
        <ul className={styles.list}>
          {filtered.map((todo) => (
            <TodoItem key={todo.id} todo={todo} />
          ))}
        </ul>
      )}
    </div>
  );
};
```

**App** is the root: it kicks off both initial loads in a `useEffect`, surfaces any error from either slice in a banner, and renders the header, form, and list.

```tsx theme={"dark"}
// src/components/App/App.tsx
import { TodoForm } from 'components/TodoForm/TodoForm';
import { TodoList } from 'components/TodoList/TodoList';
import { UserHeader } from 'components/UserHeader/UserHeader';
import { FC, useEffect } from 'react';
import { useAppDispatch, useAppSelector } from 'reducers';
import { loadUserInfo, selectUserError } from 'reducers/app/slice';
import { loadTodos, selectTodosError } from 'reducers/todos/slice';

import styles from './App.module.scss';

export const App: FC = () => {
  const dispatch = useAppDispatch();
  const userError = useAppSelector(selectUserError);
  const todosError = useAppSelector(selectTodosError);

  useEffect(() => {
    dispatch(loadUserInfo());
    dispatch(loadTodos());
  }, [dispatch]);

  return (
    <div className={styles.App}>
      <h1 className={styles.title}>My Todos</h1>
      <UserHeader />
      {(userError || todosError) && (
        <div className={styles.errorBanner}>{userError || todosError}</div>
      )}
      <TodoForm />
      <TodoList />
    </div>
  );
};
```

### Step 7: Wire up `main.tsx`

***

Replace `src/main.tsx` with:

```tsx theme={"dark"}
import './index.scss';

import { App } from 'components/App/App';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from 'reducers';

declare const DOMO_APP_NAME: string;
declare const DOMO_APP_VERSION: string;

if (import.meta.env.DEV) {
  console.log(`${DOMO_APP_NAME}@${DOMO_APP_VERSION}`);
}

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>,
);
```

`DOMO_APP_NAME` and `DOMO_APP_VERSION` are injected by the Vite template from `manifest.json` — useful for logging and telemetry.

Your final `src/` tree:

```text theme={"dark"}
src/
├── main.tsx
├── index.scss
├── components/
│   ├── App/
│   │   ├── App.tsx
│   │   └── App.module.scss
│   ├── TodoForm/
│   │   ├── TodoForm.tsx
│   │   └── TodoForm.module.scss
│   ├── TodoItem/
│   │   ├── TodoItem.tsx
│   │   └── TodoItem.module.scss
│   ├── TodoList/
│   │   ├── TodoList.tsx
│   │   └── TodoList.module.scss
│   └── UserHeader/
│       ├── UserHeader.tsx
│       └── UserHeader.module.scss
├── reducers/
│   ├── index.ts
│   ├── createAppSlice.ts
│   ├── app/
│   │   └── slice.ts
│   └── todos/
│       └── slice.ts
└── services/
    ├── app.ts
    └── types.ts
```

### Step 8: Test locally

***

```bash theme={"dark"}
pnpm start
```

The Vite dev server starts on port 3000 (or 3001/3002 if busy). Because `proxyId` is set, `AppDBClient`, `IdentityClient`, and `UserClient` all talk to your real Domo instance — create, toggle, and delete todos and they persist across reloads. Log in as a different user and you'll see your name/avatar in the header and `By <name>` on every new todo.

### Step 9: Publish

***

```bash theme={"dark"}
pnpm upload
```

The script runs `pnpm build` (which lints, tests, and builds), then `domo publish` from the `build/` folder. The new build becomes the active design. Anyone instantiating the app from the Asset Library picks it up.

### Next steps

***

* Run `da generate component` or `da generate reducer` to scaffold new slices that follow the same `createAppSlice` pattern.
* Add a `UserPreferences` collection with a `limitToOwner` filter to persist per-user theme or default filter — see [AppDB filters](/portal/API-Reference/app-framework-apis/AppDB-API).
* Layer dev/qa/prod `id` + `proxyId` values with `da manifest` and `src/manifestOverrides.json` instead of hand-editing.
* Continue with [AI Book Recommender](/portal/Apps/App-Framework/Tutorials/React/AI-Book-Recommender) or [Mapbox World Map](/portal/Apps/App-Framework/Tutorials/React/Mapbox-World-App).
