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:

PartWhat it doesAdmin screen
ContactsEvery address, its sources, lists, tags, fields and consentAudience, Contacts (/admin/contacts)
ListsGroups that broadcasts, sequences and announcements send toAudience, Lists (/admin/lists), and one page per list
TagsLabels on a contact that cross every list and form, such as vipAudience, Tags (/admin/contact-tags)
FieldsCustom data on a contact, such as company or role, usable in any emailAudience, Fields (/admin/contact-fields)
App usersAnother app's users, read live, when an app is connectedAudience, 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:

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:

  1. 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 pending contact for every unverified form member. It is safe to run again.

  2. Open Audience, Lists and click Sync form lists. It creates the list of any signup form that has none.

  3. 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

SourceWhenLists joinedStarting consent
signupA user record is created (any sign-in method)All Userssubscribed
customerA payment succeeds (the Payments feature)All Customerssubscribed
waitlistSomeone submits a signup formThe form's own list, plus every list the form feedspending until they verify or the form confirms them, then subscribed
importAn admin imports a CSVThe list chosen for the importsubscribed only if the admin ticks "These contacts have consented to marketing", else pending
manualAn admin clicks Add contactNonesubscribed only if the admin ticks "I have consent to email them", else pending
manualAn automation rule adds someone to a listThe rule's listssubscribed

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

ValueMeaning
subscribedGets marketing email (broadcasts, sequences, announcements by email)
pendingSigned up but not confirmed, or added without affirmed consent. Never gets marketing email
unsubscribedOpted 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

  1. On Audience, Contacts click Import CSV and paste the file. Each row is email or email,name. A header row is optional.

  2. Click Preview. It counts valid rows, rows with a bad address and addresses repeated in the file. Nothing is written yet.

  3. Choose the list to add them to. Tick the consent box only if these people agreed to marketing email, then import.

Lists

TypeWho is in itCan 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 codeYes: rename, delete, and remove members
SystemAll 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 usedOnly 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:

SettingWhat it does
LabelThe 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.
TypeText, 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 emailsShown 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:

ActionWhat happensUse when
SuppressConsent 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 emailsBlocks 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 contactRemoves 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

FirestoreHolds
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_fieldsThe 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):

KindFunctions
TriggersonUserCreateContact, onUserDeleteContact (on users); onWaitlistUserCreateContact, onWaitlistVerifiedContact, onWaitlistUserDeleted (on form members); onContactTagDelete
Contact callablesadminAddContact, adminSetContactConsent, adminSetContactDisabled, adminDeleteContact, adminUpdateContactLists, adminSetContactTags, adminSetContactFields
Import and fieldspreviewContactImport, importContacts, adminUpsertContactField, adminDeleteContactField
One-time buildersbackfillContacts, backfillPendingContacts, backfillFormLists, migrateTagsToContacts, migrateFormDataToContactFields
App audienceListed in App audience

Who can do what

DataAdminsEveryone else
Contacts, Suppression, ErasureLogRead. Changes go through the admin callablesNothing
Lists, ContactTags, Settings/contact_fieldsRead and writeNothing

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

SymptomCause and fix
A contact stays pending and gets no broadcastsThey 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 ListsClick Sync form lists on the Lists page.
"Remove from list" is missing on a list pageIt 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 codeTheir 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.