Audience
Audience is the one list of everyone Arc CMS can email: each person is a contact, grouped into lists, labelled with tags and carrying custom fields and their marketing consent. Sign-ups, signup forms, payments and imports feed it on their own.
What it is
A contact is one email address. The same address is always the same contact, however many forms, lists or imports it came through. Everything else hangs off the contact:
| Part | What it does | Admin screen |
|---|---|---|
| Contacts | Every address, its sources, lists, tags, fields and consent | Audience, Contacts (/admin/contacts) |
| Lists | Groups that broadcasts, sequences and announcements send to | Audience, Lists (/admin/lists), and one page per list |
| Tags | Labels on a contact that cross every list and form, such as vip | Audience, Tags (/admin/contact-tags) |
| Fields | Custom data on a contact, such as company or role, usable in any email | Audience, Fields (/admin/contact-fields) |
| App users | Another app's users, read live, when an app is connected | Audience, App users (/admin/app-users), see App audience |
Contacts decide who may get marketing email. Sending itself (providers, templates, logs, the unsubscribe link) is the Email engine. Broadcasts and sequences are Email marketing. Forms that collect contacts are Signup forms.
Turn it on or off
The feature id is audience. It is on by default. Signup forms (forms) and email marketing (email-marketing) need it, so turning it off means turning those off too, in src/custom/features.ts:
export const CUSTOM_FEATURES: FeatureChoice = {
off: ['audience', 'forms', 'email-marketing'],
};
Leaving forms or email-marketing on without audience stops npm run dev and npm run build with a message saying what to change.
When it is off:
- The Audience menu, its pages and the Settings, App audience tab are gone. Their addresses show the not-found page.
- The functions in
functions/src/features/audience.tsare not built, so the next deploy of all functions removes them. New users, form sign-ups and payments do not create contacts. - Email ignores contacts: a contact's consent or disabled switch does not block a send. An unsubscribe still stops marketing email through the Suppression list.
- The data stays. Turn the feature back on, build and deploy, and it all comes back.
See Feature ids for every feature, and Choose your app's features for choosing them in an app.
Set it up
A fresh install needs nothing: the first sign-up creates the first contact and the All Users list. On an install that already has users or form sign-ups from before the audience existed, build the audience from them once:
Open Audience, Contacts and click Backfill. It makes a contact for every user (and joins paying ones to All Customers), for every verified form member, and a
pendingcontact for every unverified form member. It is safe to run again.Open Audience, Lists and click Sync form lists. It creates the list of any signup form that has none.
If your forms carried their own tags, open Audience, Tags and click Import waitlist tags. If forms collected extra answers, open Audience, Fields and click Import past form data after creating the fields.
To send email at all, set up a provider first: see Email engine.
Use it
How people become contacts
| Source | When | Lists joined | Starting consent |
|---|---|---|---|
signup | A user record is created (any sign-in method) | All Users | subscribed |
customer | A payment succeeds (the Payments feature) | All Customers | subscribed |
waitlist | Someone submits a signup form | The form's own list, plus every list the form feeds | pending until they verify or the form confirms them, then subscribed |
import | An admin imports a CSV | The list chosen for the import | subscribed only if the admin ticks "These contacts have consented to marketing", else pending |
manual | An admin clicks Add contact | None | subscribed only if the admin ticks "I have consent to email them", else pending |
manual | An automation rule adds someone to a list | The rule's lists | subscribed |
Consent is set only when a contact is first created. A returning address keeps the consent it has: someone who unsubscribed stays unsubscribed when they join a second form. Deleting a user takes their contact off All Users and All Customers but keeps the contact.
Consent
| Value | Meaning |
|---|---|
subscribed | Gets marketing email (broadcasts, sequences, announcements by email) |
pending | Signed up but not confirmed, or added without affirmed consent. Never gets marketing email |
unsubscribed | Opted out. Never gets marketing email |
Transactional email (a sign-in code, a receipt) ignores consent. The counts at the top of the Contacts page filter the table by consent when clicked.
Import contacts from a CSV
On Audience, Contacts click Import CSV and paste the file. Each row is
emailoremail,name. A header row is optional.Click Preview. It counts valid rows, rows with a bad address and addresses repeated in the file. Nothing is written yet.
Choose the list to add them to. Tick the consent box only if these people agreed to marketing email, then import.
Lists
| Type | Who is in it | Can you edit it |
|---|---|---|
| Manual (Contacts) | People you import into it, collect with a form that feeds it, or add with an automation rule or your own code | Yes: rename, delete, and remove members |
| System | All Users (all-users), All Customers (all-customers), and one list per signup form, named after the form (waitlist- followed by the form id) | No: they follow users, payments and form members on their own |
| App users (live) | Whoever in a connected app matches its conditions, each time it is used | Only its conditions: see App audience |
Click a list to open its page. It has three tabs: Members, Broadcasts sent to it, and its Sequence (the last two need Email marketing). On a form's list, members cannot be removed, because the form decides who is in it. Use Disable emails on a member instead.
Tags
Create a tag on Audience, Tags with a label and a color. Its id is the label in lowercase with dashes (VIP Customer becomes vip-customer), so two admins adding "VIP" get one tag. Put tags on a contact in the contact's panel; changes save at once. Filter the Contacts table by tag to see everyone carrying it. Deleting a tag takes it off every contact. A signup form can give every new member a default tag: see Signup forms.
Sends target lists, not tags. To email everyone with a tag, add them to a list.
Custom fields
Create a field on Audience, Fields:
| Setting | What it does |
|---|---|
| Label | The name. Its key is the label in lowercase with underscores, at most 40 characters (Company Name becomes company_name). Keys the contact already uses, such as email, name, tags or consent, are refused. |
| Type | Text, Number, Date, Yes / no, or Choice (with comma-separated choices) |
| On re-submit | "Keep the existing value" (the default) or "Overwrite with the newer value", for when a second form sends the same field |
| Fallback for emails | Shown in an email when a contact has no value |
Use a field in any email as ##FIELD:company##, or with an inline fallback as ##FIELD:company|your company##. Values arrive from form answers mapped to the field (see Signup forms) or from an admin editing the contact's panel, which always overwrites. Deleting a field keeps the values already on contacts; creating it again shows them again. All merge tags are listed in Email merge tags.
Stop emailing someone
Three actions look alike and are not interchangeable. Suppress and Erase are in the contact's panel on the Contacts page; Disable is on a list's page:
| Action | What happens | Use when |
|---|---|---|
| Suppress | Consent becomes unsubscribed and the address goes on the Suppression list, so no later import or form can make it mailable again. Subscribe reverses it. | They opted out of marketing |
| Disable emails | Blocks every email to them, transactional included, and keeps them visible and counted. Reversible. | You must stop all mail, for example to someone on a form's list |
| Erase contact | Removes them from every list and sequence, deletes their form member records and any open verification code, removes their Suppression entry, then deletes the contact. Leaves a receipt in ErasureLog keyed by the address's hash, with no address in it. Irreversible. | They asked to be deleted |
Erasing does not touch the email logs, which keep the address until they are purged (see Email engine), and does not delete a user account. For "delete my account", delete the user as well.
Extend it
Your own Cloud Functions in functions/src/custom/ can add people to the audience with the same helpers core uses, from functions/src/email-core/contacts.ts. Check that the feature is on first:
import { isFeatureOn } from '../feature-flags.js';
import { upsertContact, removeContactFromLists } from '../email-core/contacts.js';
if (isFeatureOn('audience')) {
// Creates the contact if new, then joins the lists (and their active sequences).
// A list's id is the last part of its page address, /admin/lists/{listId}.
await upsertContact({ email, name, source: 'manual', addLists: [BETA_LIST_ID] });
}
Always join and leave lists through upsertContact, addContactToLists and removeContactFromLists. They keep each list's member count right and start or end that list's sequences. Writing a contact's listIds yourself skips both.
Without code, an automation rule can add or remove people on lists when an event happens: see Automations. To use your own app's users without copying them, see App audience.
What it stores
| Firestore | Holds |
|---|---|
Contacts/{emailHash} | One document per address (id: SHA-256 of the lowercased address): email, name, sources, listIds, tags, fields, consent.marketing, disabled, userId when they have an account |
Lists/{listId} | Name, description, type (manual, system or app), memberCount, formId on a form's list, conditions on a live list |
ContactTags/{tagId} | Label, color and usageCount |
Settings/contact_fields | The field definitions |
Suppression/{emailHash} | Addresses never to send marketing email to, with a reason (unsubscribe, manual) |
ErasureLog/{emailHash} | The receipt of an erasure: who, when, and counts of what was removed |
Every collection is also in Data model. App audience state (AppAudience) is described in App audience. The feature uses no Storage paths.
Cloud Functions (each deploys as arccms- followed by its name; see Cloud Functions):
| Kind | Functions |
|---|---|
| Triggers | onUserCreateContact, onUserDeleteContact (on users); onWaitlistUserCreateContact, onWaitlistVerifiedContact, onWaitlistUserDeleted (on form members); onContactTagDelete |
| Contact callables | adminAddContact, adminSetContactConsent, adminSetContactDisabled, adminDeleteContact, adminUpdateContactLists, adminSetContactTags, adminSetContactFields |
| Import and fields | previewContactImport, importContacts, adminUpsertContactField, adminDeleteContactField |
| One-time builders | backfillContacts, backfillPendingContacts, backfillFormLists, migrateTagsToContacts, migrateFormDataToContactFields |
| App audience | Listed in App audience |
Who can do what
| Data | Admins | Everyone else |
|---|---|---|
Contacts, Suppression, ErasureLog | Read. Changes go through the admin callables | Nothing |
Lists, ContactTags, Settings/contact_fields | Read and write | Nothing |
Only Cloud Functions write contacts. Every callable above refuses anyone without the admin role (the arccms_role claim); see Roles and claims. Visitors and members never read the audience.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
A contact stays pending and gets no broadcasts | They signed up on a form and never entered the code, or were added without affirmed consent. Pending contacts never get marketing email. If you do have their consent, click Subscribe in their panel. |
| A signup form has no list under Lists | Click Sync form lists on the Lists page. |
| "Remove from list" is missing on a list page | It is a form's list: the form decides who is in it. Use Disable emails, or delete the person from the form's members. |
| Adding a contact or importing fails with "is an App users (live) list" | A live list has no stored members, so contacts cannot be put on it. Choose a Contacts list. |
| A contact gets no email at all, not even a sign-in code | Their emails are disabled. The email log shows contact_disabled. Re-enable them on the list page. |
The Contacts table loads the 500 most recently updated contacts, and its search, consent and tag filters work on those 500 only. On a larger audience a search can miss someone who exists. Open the list they are on instead, or look them up in the Firebase console by the hash of their address.