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

# Build and deploy an app

> Create an app, change its interface, and deploy the change with an API key.

Complete the [authentication setup](https://docs.canary.effectiveai.app/getting-started/authentication.md). You need a user key that can
build apps in your organization; a scoped key needs a grant to build all apps. You also need `git`,
Node 20 or later, and [pnpm](https://pnpm.io/installation).

An app is a Git repository and the deployment Effective runs from it. You'll create an app, change
what its page shows, and deploy the change. The examples use an app called `app-claims-notes` in
the `acme` workspace. IDs, commits, hashes, and URLs in them are illustrative; use the values your
own requests return.

## Create the app

Choose an ID: `app-` followed by lowercase letters, digits, and hyphens. It becomes part of the
app's hostname and can't be changed.

```bash theme={null}
curl --request POST \
  --url "https://api.canary.effectiveai.app/api/v2/apps" \
  --header "Authorization: Bearer $EFFECTIVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "id": "app-claims-notes", "title": "Claims notes" }'
```

Abbreviated `201` response:

```json theme={null}
{
  "id": "app-claims-notes",
  "title": "Claims notes",
  "url": null,
  "source": {
    "gitUrl": "https://api.canary.effectiveai.app/git/effectiveailabs/app-acme-claims-notes.git",
    "defaultBranch": "main"
  }
}
```

You'll clone `source.gitUrl` next. `url` stays null until the first deployment. `409` with
`app_exists` means the ID is taken, possibly by an app you can't see; choose another. See
[Create an app](https://docs.canary.effectiveai.app/api-reference/apps/create-an-app.md) for the description, icon, and visibility.

## Change the interface

Git uses your API key as the password; any username works. Set the header once, then clone
`source.gitUrl` and start a branch:

```bash theme={null}
GIT_AUTH="http.extraHeader=Authorization: Basic $(printf 'x:%s' "$EFFECTIVE_API_KEY" | base64 | tr -d '\n')"
git -c "$GIT_AUTH" clone https://api.canary.effectiveai.app/git/effectiveailabs/app-acme-claims-notes.git
cd app-acme-claims-notes
git checkout -b show-user
```

The starter is in `apps/app-claims-notes/`. Its backend, `worker/app_backend.py`, answers
`GET /api/health` with the signed-in user's ID. Its page, `ui/src/App.tsx`, calls that route and
shows "API Connected".

Make the page show who is signed in. In `ui/src/App.tsx`, replace the `{data && (...)}` block with:

```tsx theme={null}
{
  data && (
    <div className="flex items-center justify-center gap-2">
      <span className="h-2 w-2 rounded-full bg-[var(--success)]" />
      <p className="text-sm text-muted-foreground">Signed in as user {data.userId}</p>
    </div>
  );
}
```

`data` is the JSON that `apiFetch('health')` returned. Use `apiFetch` for any route you add to
`worker/app_backend.py`; it adds `/api/` and the user's credentials.

Install the interface's dependencies, then run its tests and type-check it:

```bash theme={null}
cd apps/app-claims-notes/ui
pnpm install
pnpm test
pnpm exec tsc
cd ../../..
```

The build runs the same checks and stops if either fails. The backend runs only in the deployed
app, so you'll see the page working after you deploy.

`pnpm install` created `ui/pnpm-lock.yaml`, which the build needs. Commit it with your change and
push the branch:

```bash theme={null}
git add apps/app-claims-notes
git commit -m "Show the signed-in user"
git -c "$GIT_AUTH" push origin show-user
```

You can't push to `main`; deploying merges your branch into it.

## Build the release bundle

From the repository root, run the builder that comes with the starter:

```bash theme={null}
node apps/app-claims-notes/build-release.mjs --out ../release.json
```

It builds the last commit and stops if the clone has uncommitted or untracked files, so write the
bundle outside the clone. It installs the locked dependencies, runs the tests, type-checks, and
builds the interface. The first run can take a few minutes. It prints:

```json theme={null}
{
  "appId": "app-claims-notes",
  "sourceSha": "4b1e0c2d9a7f3e8b6c5d4a3f2e1d0c9b8a7f6e5d",
  "bundlePath": "/home/you/release.json",
  "bundleSha256": "aad9ee89066931171bf6ded3bb1876a13125b9753b2f180118e9ac3720148ca1",
  "bundleSizeBytes": 1834217
}
```

`sourceSha` is the head of `show-user`. You'll send it, `bundleSha256`, and `bundleSizeBytes` in
the next two requests.

## Upload the bundle

Request an upload URL:

```bash theme={null}
curl --request POST \
  --url "https://api.canary.effectiveai.app/api/v2/apps/app-claims-notes/release-uploads" \
  --header "Authorization: Bearer $EFFECTIVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "sourceSha": "4b1e0c2d9a7f3e8b6c5d4a3f2e1d0c9b8a7f6e5d",
    "bundleSha256": "aad9ee89066931171bf6ded3bb1876a13125b9753b2f180118e9ac3720148ca1",
    "bundleSizeBytes": 1834217
  }'
```

```json theme={null}
{
  "uploaded": false,
  "upload": {
    "method": "PUT",
    "url": "https://…",
    "headers": { "Content-Type": "application/json" },
    "expiresAt": "2026-10-26T18:19:11.000Z"
  }
}
```

Set `UPLOAD_URL` to `upload.url` and PUT the bundle to it within 15 minutes. Send the header the
response names, and don't send your API key:

```bash theme={null}
curl --request PUT \
  --url "$UPLOAD_URL" \
  --header "Content-Type: application/json" \
  --data-binary @../release.json
```

The upload request is safe to repeat. Once the bundle is stored, it returns `"uploaded": true`; if
the URL expired, it returns a new one.

## Deploy

Deploy the bundle from the branch you pushed:

```bash theme={null}
curl --request POST \
  --url "https://api.canary.effectiveai.app/api/v2/apps/app-claims-notes/deployments" \
  --header "Authorization: Bearer $EFFECTIVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "bundleSha256": "aad9ee89066931171bf6ded3bb1876a13125b9753b2f180118e9ac3720148ca1",
    "sourceSha": "4b1e0c2d9a7f3e8b6c5d4a3f2e1d0c9b8a7f6e5d",
    "sourceBranch": "show-user"
  }'
```

Effective checks the bundle against the files at `sourceSha`, squash-merges `show-user` into
`main`, and deploys. This can take a minute. Abbreviated `201` response:

```json theme={null}
{
  "id": "0f8e2b7c-3d1a-4c5e-9b6f-2a7d8c9e0f1a",
  "sha": "a713a594ead1437645e8eceb2ed4b8f5c80c73fe",
  "current": true
}
```

`current: true` means the app now serves this deployment. `sha` is the merge commit on `main`.

If `main` changed after you branched, the deploy returns `409` with `source_main_changed` or
`source_merge_conflict`. Rebase your branch onto `main`, force-push it, then build, upload, and
deploy again. Do the same after `502` with `deployment_failed`: `main` already holds your change,
but the previous deployment is still current. [Deploy an app](https://docs.canary.effectiveai.app/api-reference/apps/deploy-an-app.md) lists the other
errors.

Start your next change from the updated `main`:

```bash theme={null}
git -c "$GIT_AUTH" fetch origin main
git checkout -b next-change origin/main
```

## Open the deployed app

Get the app's `url`:

```bash theme={null}
curl --request GET \
  --url "https://api.canary.effectiveai.app/api/v2/apps/app-claims-notes" \
  --header "Authorization: Bearer $EFFECTIVE_API_KEY"
```

Abbreviated response:

```json theme={null}
{
  "id": "app-claims-notes",
  "currentDeploymentId": "0f8e2b7c-3d1a-4c5e-9b6f-2a7d8c9e0f1a",
  "url": "https://acme--app-claims-notes.hub.effectiveai.app"
}
```

Open `url` in a browser and [sign it in](https://docs.canary.effectiveai.app/guides/apps/test-an-app.md#sign-a-browser-in). The page
shows "Signed in as user" and your user ID.

Next, [test the app and set its secrets](https://docs.canary.effectiveai.app/guides/apps/test-an-app.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.