# Apps and sources

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

An **app** is one of your products, and the only attribution unit: every event and every profile carries its `appId`. A portfolio of SaaS products streams into one org, and the app is the line between them: a profile, a segment and an automation each belong to exactly one app, and an automation only ever sees its own app's events and people.

A **source** is one inbound pipe into an app. Every app is created with one source of kind `api`, the first-party SDK and HTTP path, and may hold more of them, one per caller you want to tell apart; a provider that pushes into the app (Clerk today) is a source of its own kind alongside them. Every source lands on its app, so "storefront via Clerk" and "storefront via the SDK" are one app and one set of profiles.

## You hold two ids per product

This is worth saying plainly, because the two are not interchangeable:

| Id | Shape | Where it goes |
| --- | --- | --- |
| `sourceId` | `src_` plus an opaque TypeID | SDK client config (or per call), and on every raw `identify`, `track`, and `batch` request |
| `appId` | `app_` plus a readable slug | `cow.json`, and the dashboard and CLI filters |

Ingestion names the **pipe** so that deactivating a source actually switches something off: the id a caller was configured with stops working until the source is activated again. The server derives the app from the source and stamps it on the row, which is why everything downstream filters by the app without your calls ever mentioning it.

## Set up an app

Apps are managed in the dashboard, the CLI, or the MCP server, same API either way:

```sh
cow apps create --name "storefront"          # prints the new app, e.g. app_storefront
cow apps list
cow apps sources list app_storefront          # prints its api source, e.g. src_01j…
```

The app id is a readable slug derived from the name at creation (`storefront` becomes `app_storefront`), unique per org: a name whose slug is taken is a 409 asking for a different name. A rename never changes the id.

Creating the app creates its `api` source in the same call, but the create response does not carry that id, so read it back with `cow apps sources list` (or from the app's page in the dashboard, where the row has a copy button). That `src_` id is what the SDK is configured with. Keep it in the environment next to your key, here `COW_SOURCE_ID`:

```ts
import { Cow } from "@cowliss/sdk";

const cow = new Cow({
  apiKey: process.env.COW_KEY!,
  sourceId: process.env.COW_SOURCE_ID!,
});

await cow.identify({
  identifiers: { userId: "user_1" },
  traits: { email: "ada@acme-demo.io" },
});
```

One client per app is the shape to reach for: the source is a fact about where
the code runs, so it belongs in the client's configuration rather than in every
call. Pass
`sourceId` on a call to override it, which is what a process writing into two
apps does.

API keys stay org-level: the source comes from the payload, never the key, so one key serves every app you point at Cowliss.

### Brand

An app's **Brand** tab in the dashboard (right after Health) is the one place you set how the app looks to the people it mails. It applies to every broadcast, to the [signup](/reference/api/subscribe/) confirm email, and to every hosted page: the signup form, check your inbox, confirm, subscribed, the unsubscribe pages and the [preferences page](/emails/#email-preferences). Each group saves on its own, and a preview beside them shows the signup email and page with your unsaved changes.

- **Business name and website.** The pages and the confirm email name the app by its business name (its own name when that is empty, which is yours to tell apps apart) and link "Back to" its website (https only) when one is set.
- **Look.** A logo, six colours (background, text, headings, links, button, button text), a font and the postal address under a broadcast's footer. The logo shows at its own size, scaled down to fit 200 by 64 pixels and never up, so a small logo stays sharp.
- **Sender.** The address the confirm email and new broadcasts come from, and a reply-to. Until it is set, a signup sends no confirm email and the signup page says the list cannot take signups right now; [app health](/reference/api/apps/) warns while the app has a list and no sender.
- **Language.** English, French, Romanian or Spanish, for the confirm email and the hosted pages. Broadcasts are in whatever language you write them.

From the CLI, the business name and website are the app's `publicName` and `returnUrl`, and the rest is its look:

```sh
cow apps update app_storefront --public-name "Acme" --return-url "https://acme.example/account"
cow apps update app_storefront --public-name "" --return-url ""   # clear both
cow apps look get app_storefront
```

See [Unsubscribe links](/emails/#unsubscribe-links).

Archiving an app stops it taking new sources and freezes its settings, but it keeps ingesting through the sources it already has, so a backfill against an archived app still lands. Events already stored keep their `appId` and `sourceId` forever.

## Sources

`cow apps sources list <appId>` lists an app's pipes; each is addressed by its own id afterwards:

```sh
cow apps sources add app_storefront --data '{"kind":"api","name":"backend"}'
cow sources get src_…
cow sources update src_… --name worker
cow sources deactivate src_…
cow sources activate src_…
```

Every source has a name, unique among its app's active sources. An app may hold any number of `api` sources: the one created with the app is named `api`, and each one you add is named by you, so the events feed can say which of your callers sent what. A Clerk source is one per app and named `clerk`. An `api` source has an empty config and no credential of its own, because ingestion authenticates with the org API key; sending it a `config` with anything in it is a 422 rather than a silent no-op.

### Deactivating switches a pipe off until you activate it

Deactivating a source refuses every call carrying its id with a **403** `source_inactive`, "This source is inactive. Activate it to send events again.", and a Clerk source's webhook URL stops accepting deliveries. Activating it takes the same id back, so nothing has to be reconfigured. To move a caller to a new id instead, add a second `api` source, point the caller at it, then deactivate the first.

Inactive sources are kept rather than deleted, because events and profiles already carry their ids. The app page lists them with their status, and the events feed's source filter keeps listing them: historical events are exactly what you would filter for.

What a mistyped or borrowed id does:

| What you sent | Answer |
| --- | --- |
| A `sourceId` no source has | 422, because a bad id is a configuration error, not data |
| A provider source's id (a Clerk source) | 422: well formed, but the wrong kind of pipe for `identify` |
| An inactive source's id | 403 `source_inactive` |
| Another app's `api` source id, in your organization | **200**, and the profiles land in that app |

The last row is the one to watch: an API key is org-scoped, so every active `api` source in the organization is a legitimate destination and nothing in the answer says which app it was. `cow whoami --key ak_… --source src_…` reads the pair back before the first write.

## Connecting Clerk

There is one provider kind today, `clerk`: your auth provider's user lifecycle lands in profiles with no integration code. An app holds at most one active Clerk source (one Clerk instance pushes into one app); a second is a **409**.

Setup runs in the provider's order, not Cowliss's. Clerk only issues a signing secret once the endpoint exists, and that endpoint's URL carries the source id, so the source is created first:

```sh
# 1. Create the Clerk source on the app it feeds. No secret yet.
cow apps sources add app_storefront --data '{"kind":"clerk"}'

# 2. Register https://<your-api>/v1/sources/<src_id>/webhook in the Clerk
#    dashboard, subscribing to the user and organizationMembership events.

# 3. Paste back the signing secret Clerk issued for that endpoint.
cow sources update src_… --config '{"signingSecret":"whsec_…"}'
```

Between steps 1 and 3 the source is inert, not open: with no stored secret no signature can verify, so every delivery is rejected with a 400. The dashboard shows that state as **Needs signing secret**, then **Awaiting first delivery**, then **Connected** once a delivery verifies. An `api` source, which has nothing to paste, reads simply **Active**, and a deactivated source of either kind reads **Inactive**.

`user.created` and `user.updated` become an identify with `identifiers: { "clerkId": … }` and canonical traits. The account lifecycle also lands as three [system events](/automations/#the-system-namespace): `user.created` writes `system.signed_up` right after its identify, so a welcome automation can trigger on it and still read the traits; `session.created` writes `system.logged_in`; `user.deleted` writes `system.profile_deleted`. And `organizationMembership.created`, `.updated`, and `.deleted` are mapped onto the user's `orgs` trait, a map of Clerk org id to role. Because the Clerk source identifies by `clerkId`, a delivery and your own SDK call naming the same `clerkId` land on one profile. Subscribe your Svix endpoint to all seven if you want the session and membership lenses; unhandled event types are acked and ignored.

One caveat on `system.signed_up`: a bulk user import fires `user.created` once per user, so an automation triggered on it enrolls every imported profile. Gate it on a trait if that is not what you want.

The signing secret is write-only: it is stored on the source row and never appears in any DTO, which is why the API reports a `configured` boolean instead. Rotate it by setting the config again; the kind itself can never change.

## What the app id is used for downstream

- **Attribution**: every event and every profile carries `appId` and `sourceId`. The events feed and external BI group by the app. The `sourceId` column answers "which pipe did this arrive through", the dashboard shows it by the source's name, and the events feed takes it as a filter.
- **Identity**: identifiers are unique per `(org, app, kind, value)`, so a person known to two of your products is two profiles. See [Identity](/tracking/identity/).
- **Automations and segments**: each belongs to one app, and reads that app's events and people only. Nothing in an automation file or a segment definition names an app: `cow.json` names the app the repo pushes to, and every row it creates inherits it. See [Your repo](/guides/repo/).
- **The event list**: the app's `cow.json` may list the events it sends, and each name the app receives is then tagged as listed or not in `cow.json`. The list tags and never refuses: an unlisted event is stored, counted and acted on like any other. See [`events` in cow.json](/guides/repo/#events).
- **Filters**: every data list (users, segments, automations, events, executions, deliveries) takes `?appId=`, in the dashboard, the CLI and the API.

An automation trigger and a segment definition cannot filter by app, and cannot filter by source either. The app is not a filter but the row's own boundary, and a stored definition naming a pipe that was later deactivated would parse fine and evaluate empty forever, which is a failure nobody sees. Read-time filters are safe where stored definitions are not, so the events feed has one and definitions do not.
