Signup forms

A signup form collects email addresses, checks each one with a code, and puts every person into your audience. Put one on any page: Arc CMS handles joining, the emailed code, the confirmation, the contact and its lists, and the welcome email.

What it is

Each form is a document in Waitlists. Its members live under it, one document per address. A form can be used in two ways:

Every sign-up becomes a contact that joins the form's own list and any other lists the form feeds. From there, broadcasts and sequences reach them (Email marketing).

AddressWhat it shows
/waitlist/{form}The form's own signup page (the form is found by id or slug)
/leaderboard/{form}, /leaderboard/{form}/{member}, /user/{form}/{member}Open /waitlist/{form}. These were the leaderboard and member pages of older versions; their links in emails already sent still lead somewhere
/admin/waitlistsSignup Forms in the admin menu

Turn it on or off

The feature id is forms. It is on by default and needs audience. To turn it off, add it to off in src/custom/features.ts:

export const CUSTOM_FEATURES: FeatureChoice = {
    off: ['forms'],
};

When it is off:

See Feature ids and Choose your app's features.

Set it up

  1. Set up email in Settings, Email Settings (see Email engine). Without a working provider no code can be sent, so every form confirms people without one.

  2. Open Signup Forms and click the add button. Give it a name. The slug is filled in from the name and becomes the form's id, the {form} in every address above. Editing the slug later changes the slug only, never the id, and breaks links that used the old slug.

  3. Creating the form also creates its list under Audience, Lists and its two email templates. Open the form's Email Templates to adjust them.

  4. Link to /waitlist/{form}, or put the form on a page of your site (next section).

Put a form on a page

Every page of your site binds every form that has data-waitlist-form: the home page (src/custom/site/home.html and its translations, see Build your home page), content pages and list pages from your templates, and static pages. It works in the app and on the published page, where public/assets/js/arc-site.js runs the same steps through the same functions:

<form data-waitlist-form data-waitlist-id="beta">
  <input name="firstName" placeholder="Your name">
  <input type="email" name="email" required>
  <input name="company" placeholder="Company">
  <button type="submit">Join the beta</button>
</form>
<p><span data-waitlist-count="beta"></span> people have joined</p>
MarkupMeaning
data-waitlist-formArc CMS handles this form. A <form> on the home page without it gets a warning overlay
data-waitlist-idThe form's id. Without it, the default form waitlist-form. On an install whose default form is older and has another id, waitlist-form uses that form until a form with the id waitlist-form exists.
name="email" or data-waitlist-emailThe address (required)
name="firstName", name="name" or data-waitlist-nameThe name
name="source" or data-waitlist-sourceWhere they came from (default direct)
Any other named inputSaved with the member as a form answer, and on the contact if mapped to a field
data-waitlist-countShows the number of confirmed members
data-legal-noticeYour own terms notice. Without one, a standard notice is added above the submit button. It links to your terms and privacy pages: the published standard pages /info/terms and /info/privacy-policy (see Standard pages), else your own static pages at /p/terms and /p/privacy-policy, else no link

If no form has the given id, the first visit creates one with default settings. The code step, the confirmation and the error messages are drawn inside the form. To reword the confirmation, see Add signup forms to your site.

Put the form's id in data-waitlist-id, not a slug you changed later. The page finds the form by its slug, but sign-ups are sent with the value as written, and a value that is not the id fails with "This form does not exist".

Use it

What happens on a sign-up

  1. The visitor submits. The joinForm function finds or creates their member record on that form. Submitting the same address again returns the same record, never a second one.

  2. The member record creates a pending contact, which joins the form's list and the lists it feeds, gets the form's default tag and its mapped answers.

  3. requestFormOtp emails a 6-digit code. It is valid for 15 minutes, can be resent once a minute, and allows 5 wrong tries.

  4. The visitor enters it. verifyFormOtp checks it and finalizeFormSignup confirms the member and counts them in the form's totalSignups.

  5. The contact becomes subscribed, the waitlist.joined event fires (see Automations), and the welcome email goes out.

  6. The form shows a tick, "You're on the list!" and the address. Someone who was already on the list sees "You're already on the list." instead.

The code step is skipped, and the member confirmed at once, when email is not set up, when the Waitlist emails switch in Settings, Email Settings is off, or when the form's verification template is inactive. Such a member is confirmed but not verified.

Form settings

Edit a form from its row or its dashboard (Settings):

SettingWhat it does
Name, Description, Cover ImageShown on the form's page. Renaming the form renames its list
ActiveOff: the form shows your message instead of accepting sign-ups
Default TagGiven to every new member
Feeds listsOther Contacts lists every member joins. The form's own list is always included
Save form answers asMaps a form input to a contact field, so emails to any list can use the answer (see Audience)

Emails

Each form has its own two templates, on its Email Templates page (/admin/waitlists/templates/{form}):

TemplateKindSent
Verification code (waitlist_verify_otp_email)TransactionalOn every sign-up that needs a code. Uses ##OTP##
Welcome (waitlist_welcome_email)MarketingWhen the member verifies their address. Uses ##WAITLIST##

A form's welcome can instead be the first step of a sequence on its list: the migrateWelcomeToSequences admin callable moves every form's welcome there and marks the form, after which the sequence sends it and the template is not sent directly (see Email marketing).

To turn a form's code step off, make its verification template inactive. For more emails after the welcome, add a sequence on the form's list (Email marketing). Every tag is listed in Email merge tags.

Deleting the verification template does not turn the code step off: the default template is created again the next time a code is needed. Switch it to inactive instead.

The welcome template goes out when an address is verified. A form that confirms people without a code never sends it. For a welcome on such a form, use a sequence on the form's list with a first step at 0 hours.

Older versions seeded a welcome with a referral link button and a leaderboard button. A form whose welcome is still exactly that default gets the plain one the next time a template is needed. A welcome you edited keeps your text, and its two buttons now lead nowhere: open the form's Email Templates and remove them.

Upgrading from referrals and the leaderboard

Older versions gave every member a referral code and link, a queue position and a public leaderboard. Signup forms have none of these now. After updating an install that used them:

  1. Deploy the website first, then all the functions, then the rules. The old form page reads members through a function this version deletes, while the new pages work with the old functions. A deploy of all the functions deletes the referral and leaderboard functions; a targeted deploy never deletes one (see Functions that are deleted).

  2. Run npm run forms:remove-referrals to count the old referral records, then npm run forms:remove-referrals -- --apply to delete them. Each one holds the address of the person referred, and nothing else removes it when that person is deleted.

  3. Check each form's welcome email for referral or leaderboard buttons you added yourself.

The old fields on member records (referralCode, referralLink, leaderboardLink, queuePosition, totalReferrals, referredBy) stay where they are and are not read any more. Old ?ref= links open the page as usual.

Members

View Users on a form lists its members, with notes and the form's own tags. Each form's dashboard shows sign-up numbers, traffic sources, devices and recent sign-ups, and exports the members.

Extend it

Any page that renders your own HTML can collect sign-ups by linking to /waitlist/{form}. To react to a sign-up in code or without code, listen for the waitlist.joined event with an automation rule (Automations), or add a sequence on the form's list.

The public functions are callable from any client, but prefer the built-in page and markup: they already call joinForm, requestFormOtp, verifyFormOtp and finalizeFormSignup in the right order. Calling them from a client uses the prefixed name, such as arccms-joinForm.

What it stores

FirestoreHolds
Waitlists/{form}The form: name, slug, description, isActive, uiConfig, defaultTagId, targetListIds, fieldMap, otpEnabled, totalSignups
Waitlists/{form}/users/{member}One member: email, firstName, formData (the answers), signupMetadata, emailVerified, isConfirmed, verifiedAt, tags
Waitlists/{form}/users/{member}/notes/{id}Admin notes on a member
form_otps/{form}_{emailHash}The pending code, stored hashed, with its expiry and tries
EmailTemplate/{form}_{type}The form's two templates
WaitlistUserTags_{form}The form's own member tags
WaitlistedUsersA member registry from older installs. Nothing writes it

Every collection is also in Data model. The contacts and the form's list belong to Audience. The cover image is picked from the Media library.

Cloud Functions (each deploys as arccms- followed by its name):

KindFunctions
Public callablesjoinForm, requestFormOtp, verifyFormOtp, finalizeFormSignup, ensureWaitlistExists
Admin callablesgetWaitlistTemplateDefaults, backfillWaitlistTemplates, and the one-time upgrade helpers normalizeWaitlistTemplateIds, stampFormTargetLists
TriggersonWaitlistsCreate, onWaitlistsUpdate, onWaitlistsDelete (forms); onWaitlistUserCreate, onWaitlistUserUpdate (members); syncOtpEnabledFlag (on templates)

See Cloud Functions for the full list.

Who can do what

DataVisitorsAdmins
Waitlists/{form}Read (it renders the page)Read and write. The signup count is written by Cloud Functions
MembersCreate their own record; change only their name, tags and subscription. No reads: joining and confirming go through the public callablesRead, write, delete
Old referral recordsNothingNothing (deleted by npm run forms:remove-referrals)
Notes, WaitlistUserTags_{form}, WaitlistedUsersNothingRead and write
form_otpsNothingNothing (Cloud Functions only)

Verification, confirmation and the signup count are worked out by Cloud Functions. The admin callables refuse anyone without the admin role (Roles and claims).

Troubleshooting

SymptomCause and fix
"Please wait 42s before requesting another code"The one-code-a-minute limit per address and form. Wait and resend.
No code arrivesOpen Email Logs: a skipped log names the reason, such as email_disabled or feature_disabled. See Email engine.
"This form has no verification email template, so signups cannot be confirmed"Email is on but the form's code template could not be found or made. Open the form's Email Templates page to restore the defaults.
"This email has not been verified"The form needs a code and none was verified for that address. Request a new code.
A form on the site does nothingIt lacks data-waitlist-form, the published page was built before the form was added (publish it again), the form is not Active, or forms is off.
A welcome email has buttons that lead nowhereIt still has the referral and leaderboard buttons of older versions, edited by someone, so it was not upgraded. Remove them on the form's Email Templates page.
"Save form answers as" is missing from the form's settingsIt lists the input names held in the form document's fields list, and nothing fills that list for you. The mapping itself is the form's fieldMap (input name to contact field key), which an admin can also write directly.