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:
- Its own page at
/waitlist/{form}, which Arc CMS renders from the form's settings. - Your own markup on any page of your site: a plain HTML
<form>marked withdata-waitlist-form, which Arc CMS takes over when the page loads.
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).
| Address | What 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/waitlists | Signup 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:
- The Signup Forms menu item and every form page (
/waitlist,/leaderboard,/user/{form}/{member},/admin/waitlists) are gone and show the not-found page. - Form markup on your pages stays as you wrote it, but nothing reads, counts or submits it.
- The functions in
functions/src/features/forms.tsare not built, so the next deploy of all functions removes them. - The forms, members and templates stay in Firestore. Turn the feature back on to have them again.
See Feature ids and Choose your app's features.
Set it up
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.
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.Creating the form also creates its list under Audience, Lists and its two email templates. Open the form's Email Templates to adjust them.
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>
| Markup | Meaning |
|---|---|
data-waitlist-form | Arc CMS handles this form. A <form> on the home page without it gets a warning overlay |
data-waitlist-id | The 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-email | The address (required) |
name="firstName", name="name" or data-waitlist-name | The name |
name="source" or data-waitlist-source | Where they came from (default direct) |
| Any other named input | Saved with the member as a form answer, and on the contact if mapped to a field |
data-waitlist-count | Shows the number of confirmed members |
data-legal-notice | Your 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
The visitor submits. The
joinFormfunction finds or creates their member record on that form. Submitting the same address again returns the same record, never a second one.The member record creates a
pendingcontact, which joins the form's list and the lists it feeds, gets the form's default tag and its mapped answers.requestFormOtpemails a 6-digit code. It is valid for 15 minutes, can be resent once a minute, and allows 5 wrong tries.The visitor enters it.
verifyFormOtpchecks it andfinalizeFormSignupconfirms the member and counts them in the form'stotalSignups.The contact becomes
subscribed, thewaitlist.joinedevent fires (see Automations), and the welcome email goes out.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):
| Setting | What it does |
|---|---|
| Name, Description, Cover Image | Shown on the form's page. Renaming the form renames its list |
| Active | Off: the form shows your message instead of accepting sign-ups |
| Default Tag | Given to every new member |
| Feeds lists | Other Contacts lists every member joins. The form's own list is always included |
| Save form answers as | Maps 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}):
| Template | Kind | Sent |
|---|---|---|
Verification code (waitlist_verify_otp_email) | Transactional | On every sign-up that needs a code. Uses ##OTP## |
Welcome (waitlist_welcome_email) | Marketing | When 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:
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).
Run
npm run forms:remove-referralsto count the old referral records, thennpm run forms:remove-referrals -- --applyto delete them. Each one holds the address of the person referred, and nothing else removes it when that person is deleted.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.
- Deleting a member takes the contact off the form's own list. The contact itself stays, and so do the other lists the form fed.
- Deleting a form deletes all its members and their notes, takes every contact off the form's list and deletes that list.
- To remove a person everywhere, erase the contact (see Audience): that deletes their member records too.
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
| Firestore | Holds |
|---|---|
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 |
WaitlistedUsers | A 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):
| Kind | Functions |
|---|---|
| Public callables | joinForm, requestFormOtp, verifyFormOtp, finalizeFormSignup, ensureWaitlistExists |
| Admin callables | getWaitlistTemplateDefaults, backfillWaitlistTemplates, and the one-time upgrade helpers normalizeWaitlistTemplateIds, stampFormTargetLists |
| Triggers | onWaitlistsCreate, onWaitlistsUpdate, onWaitlistsDelete (forms); onWaitlistUserCreate, onWaitlistUserUpdate (members); syncOtpEnabledFlag (on templates) |
See Cloud Functions for the full list.
Who can do what
| Data | Visitors | Admins |
|---|---|---|
Waitlists/{form} | Read (it renders the page) | Read and write. The signup count is written by Cloud Functions |
| Members | Create their own record; change only their name, tags and subscription. No reads: joining and confirming go through the public callables | Read, write, delete |
| Old referral records | Nothing | Nothing (deleted by npm run forms:remove-referrals) |
Notes, WaitlistUserTags_{form}, WaitlistedUsers | Nothing | Read and write |
form_otps | Nothing | Nothing (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
| Symptom | Cause and fix |
|---|---|
| "Please wait 42s before requesting another code" | The one-code-a-minute limit per address and form. Wait and resend. |
| No code arrives | Open 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 nothing | It 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 nowhere | It 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 settings | It 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. |