App audience

Connect the collection where an app keeps its users, and Arc CMS can email those people, group them into live lists, run sequences for them and react when their data changes. Their data stays in the app's own collection: Arc CMS keeps no copy of their documents, only its own records about them and the name, email and phone carried by each event and email log.

What it is

The host app keeps one document per person in one Firestore collection, such as users/{id}, and writes to it as it always has. Arc CMS reads that collection live and watches it with a trigger. The people in it are app users. When the host is another app, they are not contacts, never sign in to Arc CMS and get no user record there. Arc CMS keeps only what is its own about them: their email consent and their place in sequences.

WhereWhat the admin gets
Settings, App audienceHow to read one person: unique key, email, phone and name fields, and the fields to watch
Audience, App usersEvery app user, live, with search and a panel showing all their fields, email status and recent events
Audience, ListsApp users (live) lists: conditions on the app's fields, no stored members
Email, Broadcasts and Drip CampaignsSend to live lists, alone or with contact lists; sequences that start when someone starts matching
Settings, AutomationsRules on app user events, such as "isPro becomes true sends the upgrade email"
Any template##APP.<field>## merge tags, such as ##APP.plan.tier|free##

The host can be another app sharing the Firebase project (see Sharing a Firebase project), or the install's own users collection, for a site or app built on Arc CMS that wants live lists and automations on its own members' data. Connect your app's users is the short path through this page.

Turn it on or off

App audience is part of the audience feature (Audience): with audience off, the App users page, the Settings, App audience tab and every function on this page are gone. Broadcasts and sequences to live lists also need email-marketing (Email marketing).

Within the feature it is off until a host collection is configured. An install with none points the trigger at a collection nothing writes to, so it costs nothing.

Set it up

Check the host fits

Connect it

  1. Name the host database and collection. The path is <collection>/{id} exactly; the wildcard name does not matter:

    npm run arc:configure -- --app-users-database='(default)' --app-users-path='users/{id}'

    For the install's own users instead, use --app-users=own. Add --project= with an alias for another project, and --dry-run to see the change first. This writes ARC_APP_USERS_DATABASE and ARC_APP_USERS_PATH to the project's functions environment file (see Configure a project).

  2. Deploy the functions. The trigger is bound to the collection at deploy time:

    npm run deploy -- --only functions

    If only the collection changed on an install that is already deployed, deploying the trigger alone is enough (Deploy only what changed):

    npm run deploy -- --only functions:arccms:arccms.onAppUserWritten
  3. Open Settings, App audience. The top shows the database and collection from the deploy. Choose the Unique key: the document id or a field. Never assume it is the email; apps with phone sign-in have none.

  4. Pick the Email field, Phone field and Name field. The choices come from a sample of 20 real documents; nested fields are dot paths such as profile.name.

  5. Under Watch for changes, tick the fields whose changes should become events, such as isPro or plan.tier. Leave out counters and timestamps: they change all the time and mean nothing.

  6. Click Test with a document (with a document id, or none for the first document) to see the person as Arc CMS reads them, then Save.

  7. Open Audience, App users. It lists the app's people with the right names and addresses. Change a watched field in the app, then open the person: the change shows under Recent activity.

With --app-users=own, the key, email, name and phone fields default to the document id, email, name and phone until you save others.

Fields that are never shown

A field whose name looks like a credential is shown as (hidden) and never goes into an email, an event or a merge tag. A name counts when it contains a word such as password, passcode, secret, token, credential or jwt; when one of its words is a credential word such as otp, pin, pwd, hash, salt, cvv or ssn; or when it is a key, code, id or cookie of a kind such as apiKey, resetCode or sessionId. Everything under such a field is hidden too. A yes or no value stays visible (passwordless: true). If the app keeps a secret under another name, rename it in the app.

Use it

Events and automations

Every write to a host document runs the trigger once. It turns the write into events:

EventWhen
app_user.createdA new document appears. Documents that existed before the deploy never fire it, and a document with no value in the unique key field fires no event
app_user.changed.<field>A watched field (or the unique key field) changes, one event per field, with its old and new value
app_user.deletedA document is deleted

Any other write produces nothing. Set what each event does in Settings, Automations: rules with an optional condition on the old and new value ("was" and "becomes": exactly, any of, none of) and an email, transactional or marketing. See Automations for the rules. App user events send email only: they never create contacts or change lists.

Values compare as text, ignoring case, and empty, missing and null are the same empty value. So isPro becoming true is "becomes exactly true", and a free plan written as either "" or "free" is "any of , free".

Live lists

On Audience, Lists, click Create list and choose App users (live) (offered only when a host is connected). Add conditions on the app's fields; all must match:

OperatorMatches when the value
is, is notEquals, or differs from, the text (ignoring case)
is any ofEquals one of a comma-separated list
containsContains the text
is greater than, is less thanCompares as a number, or as a date written like 2026-10-01; a number never compares with a date
is empty, is not emptyHas no value, or has one

The count updates as you edit, and the list's page shows who matches at that moment. Values compare as the admin sees them: true and false for booleans, ISO text for dates. Contacts can never be added to a live list.

"isPro is false" does not match a person with no isPro field, because a missing value is empty, not false. For everyone who is not Pro, use "isPro is not true".

Broadcasts and sequences

See Email marketing for both.

Merge tags

TagValue
##APP.<path>##The host field when the email is queued, such as ##APP.plan.tier##
##APP.<path>|fallback##The same, with a fallback when it is empty or missing
##NAME##The name field from Settings, App audience, or the part of the address before the @ when it is empty
##FIELD##, ##FROM##, ##TO##In an email from a field-change rule: the field, its old value and its new value

Values are plain text: markup in a value is shown as typed, never rendered. An email log keeps only the host fields that email's tags use, never the whole document. All other tags are in Email merge tags.

Unsubscribes

Every app user is subscribed until they unsubscribe. The unsubscribe link and the preferences page put the address on the suppression list, record the choice on each AppAudience record behind that address (without creating a contact) and end their sequences. Their status shows in the Email status column on the App users page. As for everyone, a suppressed address gets no marketing email and still gets transactional email (see Email engine). When the address is also a contact, the unsubscribe applies to both; if the two ever differ, the contact's choice decides.

What the host app must keep doing

Extend it

Nothing to code: the host app keeps writing its own documents, and everything above reacts. To act on a change in your own code rather than with a rule, write a Firestore trigger on the host collection in functions/src/custom/ (Write your own Cloud Functions). For an app built on Arc CMS that uses its own users collection, the account rules in Follow the account contract still apply: Arc CMS reads those records, it never replaces them.

With the install's own users as the audience, those people are also contacts (every sign-up joins All Users), and one sign-up fires both user.signed_up and app_user.created. Put a welcome email on one of them, not both.

What it stores

WhereHolds
ARC_APP_USERS_DATABASE, ARC_APP_USERS_PATHDeploy-time settings: the host database and collection
Settings/app_audienceHow to read a document: key, emailField, phoneField, nameField, watchedFields
AppAudience/{id}One record per person, the id being a SHA-256 hash of their unique key, so no host identifier is stored in the clear: consent, and deleted after the host deletes them. A person with no record is subscribed
AppEvents/{id}Every event, with appUserId, its data and what the rules did
Lists/{listId}A live list: type: 'app' and its conditions
DripEnrollments/{campaignId}_app_{id}A person's place in a sequence on a live list
Settings/event_mappingsThe automation rules (Automations)

Every collection is also in Data model. Nothing is written to the host collection, and no host document is copied; events and email logs keep only the person's name, email, phone and the values they use. Cloud Functions (each deploys as arccms- followed by its name):

FunctionKindDoes
onAppUserWrittenTrigger on the host collectionEmits the events, keeps AppAudience in step, joins and leaves live-list sequences. A failed run is retried for up to an hour
appAudienceStatus, sampleAppUsers, testAppUserAdmin callablesBehind Settings, App audience
listAppUsers, getAppUserAdmin callablesBehind Audience, App users
previewAppListAdmin callableWho a live list matches

Who can do what

DataAdminsEveryone else
Settings/app_audience, ListsRead and writeNothing
AppEventsRead; Cloud Functions writeNothing
AppAudienceNothing directly: the App users page reads it through the callablesNothing
The host collectionRead through the callables, with the functions' own accessWhatever the host app's own rules say

Every callable refuses anyone without the Arc CMS admin role (Roles and claims). People from another app never sign in to Arc CMS. On a shared project, keep Arc CMS sign-ups off (the default for an install that shares its project) so they cannot: see Share a Firebase project.

Troubleshooting

SymptomCause and fix
"No host collection is configured"The functions were deployed without ARC_APP_USERS_PATH. Run npm run arc:configure with --app-users-path and deploy the functions.
The App users page fails with "internal"The callables are missing or still starting. Deploy the functions again and reload after a minute.
"N documents have no value in the unique key field"Those documents lack the key field and are left out. Pick another key, or fix the documents.
Recent activity stays empty after a change in the appThe field is not watched, or the trigger is bound to another database or collection. Check Settings, App audience and the project's functions environment file.
An event says "No rule set up for this event"Add a rule for it in Settings, Automations.
A rule ran but no email arrivedEmail is off, the template is inactive, the person has no address, or a marketing email met an unsubscribe. Email Logs gives the skip reason; with no address, no log is written.
A live list matches nobodyA condition names a field most documents lack, or a value in another format. Compare with the values on the list's page.
"Only the first 2,000 users in your app are checked"The host collection has more than 2,000 documents; people after them are left out of lists, counts and broadcasts.

A trigger stays bound to the database it was deployed against. If the host database, or the Arc CMS database, is deleted and created again, the triggers never fire. Delete the affected arccms- triggers (for the host, arccms-onAppUserWritten) and deploy them again; an update does not rebind them.