Signup Manager v1.0

Part of CloudWizard Email Suite

Signup Manager builds self-hosted opt-in forms — full page, popup, embed, or iFrame — with double opt-in confirmation, GDPR consent logging, spam protection, and built-in Bayesian A/B testing. Confirmed signups can route directly into an Audience Manager list, or run fully standalone.

Requirements

RequirementDetail
PHP8.1 or higher
MariaDB / MySQL5.7+ / 10.3+
Web serverNginx or Apache
Outgoing mailReal SMTP credentials (host, port, username, password, encryption) for double opt-in confirmation emails — an unauthenticated local relay is rarely accepted by mail servers, so leave these blank only if you've confirmed your server allows it
PHP intl extensionRecommended. It lets a form slug be made from a name in Arabic, Greek, Hebrew, Chinese, Japanese and other non-Latin scripts. Without it those names get a random slug such as form-a3f9c2. Latin (including accents) and Cyrillic names work either way.
Outbound HTTPSServer must reach cloudwizard.eu for license verification, and api.cloudwizard.eu if you use the built-in language auto-translate feature

Step 1 — Install the Controller

Signup Manager runs inside the CloudWizard Controller. The controller handles authentication, shared database config, and module detection. Install it first.

Upload the controller

Unzip the download. It holds one folder, CW/, with the controller and Signup Manager inside. Upload that whole folder to a directory on your server — typically public_html/CW/. The controller part looks like this:

public_html/CW/
  bootstrap.php
  config_path.php
  index.php          ← sends /CW/ to the admin login
  vendor/            ← bundled PHPMailer, shared across every module
  admin/
    index.php
    login.php
    logout.php
    settings.php
    setup.php
    style.css
    ...

Run the setup wizard

Visit https://yourdomain.com/CW/admin/setup.php to run the wizard. Fill in:

  • Database — host, name, username, password, table prefix (default: cw_)
  • SMTP — outgoing mail server credentials and sender address, used for confirmation and welcome emails. The username/password fields note that blank only works if your server accepts unauthenticated local mail — most don't, so use real credentials for a mailbox on your mail server.
  • Admin password — minimum 8 characters
  • Site URL — your public domain

Click Complete setup. The config file is written outside your webroot where possible.

Log in

You're redirected to the login page. Enter your admin password. You should see the CloudWizard Email Suite dashboard.

Controller ready. Continue to install Signup Manager.

Step 2 — Install Signup Manager

Upload the module

The module is already inside the zip you just uploaded, at CW/SignupManager/. Adding it to an existing CloudWizard install instead? Don't upload the whole CW/ folder again: copy this module's SignupManager/ folder into your existing controller directory, together with the vendor/ folder if you do not already have CW/vendor/, then reload the dashboard. Its file layout:

public_html/CW/SignupManager/
  module.php
  signup.php
  confirm.php
  embed.js.php
  popup_preview.php
  bootstrap.php
  admin/
    _bootstrap.php
    forms.php
    signups.php
    ab_tests.php
    embed.php
    index.php
    style.css

Verify detection

Reload the controller dashboard. The Signup Manager card should show Open →. If it still shows "Not installed", confirm SignupManager/module.php is on the server and readable by PHP.

All module directories must sit directly inside the controller root (CW/SignupManager/), not nested deeper.

Step 3 — Activate your license

Get your license key

Whether you purchase through cloudwizard.eu or via Gumroad, your license key is shown on the confirmation page and emailed to you. Format: XXXX-XXXX-XXXX.

Enter the key

In the controller go to Settings → License key, paste the key, click Save settings. The key is verified against the CloudWizard license server immediately.

Verification

Once active, your install domain is registered. The license is re-verified automatically in the background every few days — no manual action required.

License active. Go to Signup Manager and build your first form.

Overview

Signup Manager → Overview shows six stat cards — Total signups, Confirmed, Pending, Suppressed, Forms, and Active A/B tests — followed by the Signup funnel and Recent Signups (the last 8 across all forms).

Signup funnel

The funnel shows, for each form and for all forms together, how many people went from seeing the form to becoming a confirmed subscriber, so you can see which form actually works. Use the 7, 30 and 90 day buttons to change the period.

ColumnMeaning
ShownA full-page form was loaded, or a popup opened. Bots, link checkers and your own previews from the admin are not counted.
Signed upSignups on the form. Addresses that were already on the suppression list are left out.
ConfirmedSignups that are confirmed: the person clicked the confirmation link, or the form has double opt-in switched off.
Shown → signed upSigned up ÷ shown, in percent.
Signed up → confirmedConfirmed ÷ signed up. Recent signups may not have had time to confirm yet.
OverallConfirmed ÷ shown.
Views are counted as one number per form per day. No IP address, no cookie and nothing else about the visitor is stored. Counting starts on the day the funnel was installed, so earlier days show "—" and are not included in the rates.
Forms embedded with the inline JavaScript embed don't make a request when they are shown, so their views aren't counted. Full-page forms, popups and iFrames are.

Building your first form

From Signup Manager → Forms, click + New form. Every form is built from three pieces: global settings (name, type, status, language detection), the fields you want to collect, and a settings-and-content table with one column per language.

Name it and pick a type

Enter a form name — the slug is auto-generated from it if left blank (e.g. "Newsletter Popup" → newsletter-popup; accents and non-Latin letters are converted, so Månadsvis gives manadsvis). Choose a form type.

Choose fields

Tick which fields to collect beyond email — first name, last name, full name, phone, company, website, message. Email is always required and always last.

Fill in the default (EN) column

Set field labels, button text, messages, confirmation email content, GDPR checkbox text, redirect and webhook URLs, and the target Audience Manager list. If the form type is Popup, also fill in the headline, subtext, image, and trigger settings.

Save

Click Save form. You're returned to the Forms list, where the new form shows its type, slug, status, languages, and signup count.

Form saved. Use Embed, Preview, or Signups from the Forms list to continue.

Form types

TypeBehaviour
Full pageA complete, standalone landing page served at signup.php?form=your-slug.
JS embedDrop a script snippet into any existing page's HTML. Inherits the host page's styling context.
iFrameEmbed on any third-party page or platform that allows iFrames — no code integration required on their end.
PopupTriggered on scroll position, a timer, or exit intent — configured per form. Also the only type with scroll/timer/exit-intent triggers, since it's the only one shown as an interruption rather than a page a visitor navigates to directly.

Get the exact embed code for a form from Forms → Embed. Preview any form — including how a popup will actually appear — from Forms → Preview before publishing it.

Adding a form to your website

What you paste depends entirely on the form's type. Every code snippet is generated for you on the Forms → Embed page for that specific form — copy it exactly as shown rather than retyping it, since it includes your form's real slug and domain.

Full page

You get a single URL:

https://yourdomain.com/CW/SignupManager/signup.php?form=your-slug

Nothing to embed — link to this URL from an ad, a newsletter, a QR code, or your site's own navigation. Visiting it shows the complete signup page.

JS embed

Two snippets, both required, pasted into your own page's HTML:

  1. Place a marker <div> exactly where you want the form to appear on the page:
    <div data-cw-form="your-slug"></div>
  2. Add this script once, anywhere before the closing </body> tag:
    <script src="https://yourdomain.com/CW/SignupManager/embed.js.php?form=your-slug"></script>

The script finds the div by its data-cw-form attribute and renders the form inside it, inheriting your page's own fonts and surrounding layout.

Headline, sub-text, and image — any form type

Every form, regardless of type, can show a headline, sub-text, and an image above the fields — set once per language under Headline & image in the settings table. On a full-page form these render centered above the card; on a popup, above the fields inside it.

This used to be popup-only in earlier builds. If you're on an older version and don't see these fields on a non-popup form, update the module.

Background — full page and iFrame

Under Visual style, set a Background color or image URL. On full-page and iFrame forms, this fills the entire page behind the card as a cover-fit backdrop — independent of, and stackable with, the Image URL above (you can have a full-page background image and a small logo above the card at the same time). JS embed forms ignore this setting, since an embedded form shouldn't override the host page's own background.

Back link — full page only

A full-page form automatically shows a small "← Back to [site]" link above the card whenever the visitor arrived from a real referring page — clicking a link on your own site, for instance. It's never shown for a direct visit, a bookmark, a popup, or an embedded form, since there's nothing to accurately point back to in those cases.

iFrame

One block, works anywhere iframes are allowed — including email builders and page editors that block JavaScript:

<iframe src="https://yourdomain.com/CW/SignupManager/signup.php?form=your-slug"
  width="100%" height="520" frameborder="0"
  style="max-width:500px;border:none;display:block">
</iframe>

Adjust height if your form has more fields than fit in 520px — there's no auto-resize, so a tall form (many fields, a popup-style image) may get clipped or scroll inside the frame.

Popup

A single script tag, pasted before </body> — no div needed:

<script src="https://yourdomain.com/CW/SignupManager/embed.js.php?form=your-slug&mode=popup"></script>

It fires once per browser session according to whichever triggers you enabled on the form (scroll, timer, exit intent — see Form types), and remembers a dismissal in the visitor's localStorage so it won't reappear and annoy someone who already closed it that session.

Test a popup's actual trigger behavior from Forms → Preview → Preview popup rather than the live page — it's the fastest way to confirm scroll/timer/exit-intent settings are working as configured before publishing.
Pair a popup with a permanent option. A popup fires once per session and won't reappear once dismissed, so someone who closes it has no way back in unless you give them one. Create a second, simpler form — type JS embed or Full page — targeting the same Audience Manager list, and place it in your site footer, sidebar, or at the end of a blog post. A visitor who closed the popup, or who only decides to subscribe after reading more, still has a quiet, always-available way to sign up.

Fields & custom data

Available fields: email (always on), first name, last name, full name, phone, company, website, message. Each active field gets its own label row in the settings table, translatable per language.

Fields are per-form, not global — a newsletter popup can collect just email while a full-page lead form collects name, phone, and company.

Multi-language forms

The settings table has one column per language. The default (EN) column is always present; click + Language to add another. Every content row — labels, messages, confirmation email content, the post-confirm page, GDPR text, popup headline/subtext, redirect and webhook URLs — can be set per language, or left as "Same as default" for settings that don't vary (opt-in requirements, spam protection, triggers, visual style).

Detection methodHow the visitor's language is chosen
<html lang="">Reads the host page's declared language attribute
?lang= URL paramReads a query string parameter, e.g. ?form=your-slug&lang=ru
Browser languageReads the visitor's browser Accept-Language setting

The language a visitor signs up in is saved with their signup record, so the confirmation email and the page shown after they click the confirm link — days or weeks later — both render in the same language they started in, not whatever language happens to be active when they click.

Use ↻ Translate from default on any language column to auto-fill it from the English content via CloudWizard's translation API. Review and edit the result — machine translation is a starting point, not a final copy.

Removing a language column deletes its content when you save — there's a confirmation prompt, but it isn't recoverable afterward.

Double opt-in & GDPR

Enable Double opt-in under Audience in the form editor to require email confirmation before a signup counts. Enable GDPR required to show a consent checkbox using the GDPR text you configured (leave GDPR text blank to omit the checkbox entirely).

Every confirmed opt-in is logged with a timestamp, the visitor's IP address, and the form identifier — a real audit trail rather than just a status flag. Confirmed signups routed to an Audience Manager list carry that consent record with them.

If Audience Manager is not installed, Signup Manager still records signups and consent locally, and — since v1.0's later builds — sends its own confirmation emails using its own bundled PHPMailer, with no dependency on Audience Manager or Mail Engine. Export from the Signups page.

What the visitor sees, in order

StepField(s) that control it
1. Right after submittingSuccess message — the headline, always shown. If Double opt-in is on, Double opt-in detail text also appears as a smaller line underneath it (e.g. "Check your inbox to confirm your subscription").
2. The confirmation emailSubject and Body, under "Confirmation email" in the editor. {{first_name}} and {{confirm_url}} are replaced automatically — if you forget to include {{confirm_url}} anywhere in the body, Signup Manager appends a working "Confirm my subscription" link automatically so the email is never sent without a way to actually confirm.
3. Below the email bodyEmail footer text — a short line at the bottom of every confirmation email (default: "You received this because you signed up for updates.").
4. After clicking the confirm linkTitle and Message, under "Page shown after clicking the confirm link" — what the visitor sees on the landing page once their subscription is confirmed.
All four of these are per-language, like everything else in the settings table. A visitor who signs up in French gets a French confirmation email and a French confirmed-page, based on the language they were in when they submitted — not the site's current language.

Spam protection

ProtectionWhat it does
HoneypotHidden field that only bots fill in — a submission with it populated is silently discarded
Time checkRejects submissions completed faster than a human could plausibly fill the form
Block disposableRejects known disposable/temporary email domains
Verify MXRejects addresses whose domain has no valid mail exchanger record

These are on by default and require no CAPTCHA, so there's no added friction for real visitors.

Audience Manager routing

Set Target AM list per language column to route confirmed signups from that language variant into a specific Audience Manager list — useful for running the same form in two languages into two separate lists with separate senders.

When both modules share the same database and table prefix, the handoff is automatic — no manual import step.

A/B testing

Signup Manager includes built-in A/B testing scoped specifically to form and popup variants, measured against confirmed opt-in — not arbitrary page elements.

Creating a test

Select the form and start a new test

From Signup Manager → A/B Tests, pick the form in the selector, then + New test. Give it a name, a confidence threshold (default 95%), and a minimum sample size per variant (default 100) before a winner can be declared.

Add variants

Variant A — Control is created automatically from your base form, and lists exactly what the form shows today: headline, subtext, button, image and accent colour. Add Variant B (and beyond) with + Add variant: a name, a traffic weight percentage, and optional overrides for headline, subtext, button text, image and accent colour. A field left blank keeps the base form's value, which is shown in grey as a hint. Each variant lists what it shows, with the fields it changes marked changed.

For the accent colour, type or paste a hex code such as #5a3b2c, or use the small colour square. A colour equal to the base colour counts as no override. A variant's text replaces the form's text in every language.

Start the test

Click ▶ Start test. Only one test can run per form at a time, and starting requires at least two variants.

Read the results

Each variant shows Visitors (people shown the form for the first time during the test), Confirmed signups, conversion rate, and probability of being best — a Bayesian estimate, not a p-value. Declare a winner manually, or enable Auto-declare to have it happen automatically once the confidence threshold and minimum sample size are both met.

Declaring a winner marks the test completed. Applying a variant's overrides to the base form is a separate, explicit action — the test result doesn't silently change your live form.

What counts as a conversion

A conversion is a confirmed signup. Without double opt-in it is counted when the visitor submits the form. With double opt-in it is counted when they click the confirmation link, and it is credited to the variant that person saw, once. Signups from addresses on the suppression list, and from bots, are never counted.

Saving

Save test settings saves only the test's own settings: name, confidence threshold, minimum sample size and Auto-declare. Each variant has its own Save variant button, so editing a variant doesn't need a second save on the test. A Save button is grey until you change something in its form, and the note beside it says "Unsaved changes" when it is ready.

How the probability is calculated

Each variant's conversion rate is modeled as a Beta distribution (alpha = conversions + 1, beta = visitors − conversions + 1). A Monte Carlo simulation draws 10,000 samples from each variant's distribution and counts how often each variant comes out ahead — that share becomes its "probability of being best". This is the same approach used by tools like GrowthBook, and it's more intuitive for non-statisticians than a traditional significance threshold.

With few visitors the number stays modest on purpose: two signups out of two visitors doesn't make a variant "best", it gives it about a 70% chance against a variant with two out of three. As a rule of thumb, a clear difference needs dozens of visitors per variant, and a small one (for example 5% against 6%) needs several thousand before the probability passes 95%.

SettingMeaning
Confidence thresholdThe probability-of-best percentage required before a winner can be auto-declared
Min sample sizeMinimum visitors per variant required before a winner can be declared — prevents calling a test on too little data
Auto-declareWhen on, a winner is set automatically once both thresholds above are satisfied. It is checked after every counted conversion, and the test is then completed. When off, you declare manually via the button on each variant.
A visitor is assigned a variant on the first visit and always sees the same one on return. Popups remember the visitor in the browser storage of the site that shows the popup, full-page forms use a cookie. Clearing browser data or testing in a private window counts as a new visitor, so test each variant in a fresh private window.

Signups & export

The Signups page lists every signup across all forms, filterable by form, status (pending / confirmed / suppressed), and email search. Each row shows email, name, form, status, source URL, IP, and signup time.

Filter by a specific form first, then use ↓ Export CSV to download that filtered set — columns are id, form_id, email, name, status, ip, source, created_at, confirmed_at.

Select rows with the checkboxes and click 🗑 Delete to remove pending or suppressed entries — useful for clearing typos and abandoned confirmations. Confirmed rows can't be deleted from here — a confirmed signup is a real Audience Manager subscriber, and deleting only the log entry here would leave that subscriber intact but misleadingly absent from this list. To remove an actual subscriber, unsubscribe or erase them in Audience Manager instead.

Export is only available once a form filter is applied — it exports the current filtered view, not the entire unfiltered signup table.

Security

Troubleshooting

ErrorCauseFix
Confirmation emails not sending, with no error shown anywhere SMTP username/password left blank in setup — an unauthenticated connection to a local mail server is usually rejected silently by the mail log, not the app Go to the controller's Settings and fill in real SMTP username/password for a mailbox on your mail server, then check /var/log/mail.log (or your server's equivalent) after a test signup to confirm an authenticated send actually happens.
Translate button does nothing Server can't reach api.cloudwizard.eu, or no license key set Check outbound HTTPS: curl -s https://api.cloudwizard.eu/translate.php. Confirm your license key is active.
Signups not appearing in Audience Manager Table prefix mismatch, or no list selected for that language column Confirm both modules use the same db_prefix, and that Target AM list is set on the relevant language column of the form.
All submissions rejected as spam Time check threshold too aggressive for a fast-filling autofilled browser, or honeypot field visible/removed by a page template Test the form yourself end to end. If legitimate submissions are consistently rejected, disable time check temporarily to isolate the cause.
Popup never appears No trigger enabled, or trigger conditions never met on a low-traffic/short-visit page Confirm at least one of scroll/timer/exit-intent is checked. Preview the form directly via Forms → Preview to test triggers in isolation.
A/B test won't start Fewer than 2 variants, or another test is already running on that form Add a second variant, or pause/end the currently running test on that form first.
Module not detected on dashboard module.php missing or not readable Confirm CW/SignupManager/module.php exists. Restart PHP-FPM: systemctl restart php8.3-fpm
License key not accepted Wrong key format or server can't reach cloudwizard.eu Key format: XXXX-XXXX-XXXX. Check outbound HTTPS from the server: curl -s https://cloudwizard.eu/license/ping.php
500 error on admin pages DB error, missing table, or file permissions tail -20 /home/yourusername/logs/php_log

CloudWizard Email Suite — Signup Manager v1.0
cloudwizard.eu — support@cloudwizard.eu