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.
| Where | What the admin gets |
|---|---|
| Settings, App audience | How to read one person: unique key, email, phone and name fields, and the fields to watch |
| Audience, App users | Every app user, live, with search and a panel showing all their fields, email status and recent events |
| Audience, Lists | App users (live) lists: conditions on the app's fields, no stored members |
| Email, Broadcasts and Drip Campaigns | Send to live lists, alone or with contact lists; sequences that start when someone starts matching |
| Settings, Automations | Rules 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
- It is in the same Firebase project as Arc CMS.
- Its users are one top-level collection, one document per person (not a subcollection or a collection group).
- Each person has a unique key: the document id, or a field such as
uid,emailorphonethat no two people share. - It has an email field, if these people are to be emailed. People without an address are listed but never emailed.
- It has at most about 2,000 people. Every read (the App users page, live lists, broadcasts) stops at the first 2,000 documents and says so.
Connect it
-
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-runto see the change first. This writesARC_APP_USERS_DATABASEandARC_APP_USERS_PATHto the project's functions environment file (see Configure a project). -
Deploy the functions. The trigger is bound to the collection at deploy time:
npm run deploy -- --only functionsIf 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 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.
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.Under Watch for changes, tick the fields whose changes should become events, such as
isProorplan.tier. Leave out counters and timestamps: they change all the time and mean nothing.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.
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:
| Event | When |
|---|---|
app_user.created | A 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.deleted | A 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:
| Operator | Matches when the value |
|---|---|
| is, is not | Equals, or differs from, the text (ignoring case) |
| is any of | Equals one of a comma-separated list |
| contains | Contains the text |
| is greater than, is less than | Compares as a number, or as a date written like 2026-10-01; a number never compares with a date |
| is empty, is not empty | Has 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
- Broadcast: pick the live list in Email, Broadcasts. The host collection is read when the send runs, and each person's email carries their own
##APP.*##values. An address on both a contact list and a live list, or on two host documents, gets one email. - Sequence: create a drip campaign on the live list. A person joins when a write makes them start matching (a step at 0 hours goes out at once), and leaves when a write makes them stop matching, when their document is deleted, or when they unsubscribe. Enroll existing members on activate enrolls everyone who matches at activation. Before every step the host document is read again.
See Email marketing for both.
Merge tags
| Tag | Value |
|---|---|
##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
- Keep the email field current: Arc CMS reads it at every send.
- Deleting a user document ends their sequences. If Arc CMS holds a record for them, it is marked deleted and their unsubscribe is kept in case the same unique key returns; the address stays on the suppression list either way.
- Changing a person's unique key moves their consent and sequences to the new key.
- Never write to the Arc CMS database, and never store a secret under a name that does not look like one.
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
| Where | Holds |
|---|---|
ARC_APP_USERS_DATABASE, ARC_APP_USERS_PATH | Deploy-time settings: the host database and collection |
Settings/app_audience | How 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_mappings | The 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):
| Function | Kind | Does |
|---|---|---|
onAppUserWritten | Trigger on the host collection | Emits the events, keeps AppAudience in step, joins and leaves live-list sequences. A failed run is retried for up to an hour |
appAudienceStatus, sampleAppUsers, testAppUser | Admin callables | Behind Settings, App audience |
listAppUsers, getAppUser | Admin callables | Behind Audience, App users |
previewAppList | Admin callable | Who a live list matches |
Who can do what
| Data | Admins | Everyone else |
|---|---|---|
Settings/app_audience, Lists | Read and write | Nothing |
AppEvents | Read; Cloud Functions write | Nothing |
AppAudience | Nothing directly: the App users page reads it through the callables | Nothing |
| The host collection | Read through the callables, with the functions' own access | Whatever 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
| Symptom | Cause 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 app | The 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 arrived | Email 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 nobody | A 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.