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
| Requirement | Detail |
|---|---|
| PHP | 8.1 or higher |
| MariaDB / MySQL | 5.7+ / 10.3+ |
| Web server | Nginx or Apache |
| Outgoing mail | Real 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 extension | Recommended. 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 HTTPS | Server 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.
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.
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.
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.
| Column | Meaning |
|---|---|
| Shown | A full-page form was loaded, or a popup opened. Bots, link checkers and your own previews from the admin are not counted. |
| Signed up | Signups on the form. Addresses that were already on the suppression list are left out. |
| Confirmed | Signups that are confirmed: the person clicked the confirmation link, or the form has double opt-in switched off. |
| Shown → signed up | Signed up ÷ shown, in percent. |
| Signed up → confirmed | Confirmed ÷ signed up. Recent signups may not have had time to confirm yet. |
| Overall | Confirmed ÷ shown. |
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 types
| Type | Behaviour |
|---|---|
| Full page | A complete, standalone landing page served at signup.php?form=your-slug. |
| JS embed | Drop a script snippet into any existing page's HTML. Inherits the host page's styling context. |
| iFrame | Embed on any third-party page or platform that allows iFrames — no code integration required on their end. |
| Popup | Triggered 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:
- Place a marker
<div>exactly where you want the form to appear on the page:<div data-cw-form="your-slug"></div> - 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.
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.
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.
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 method | How the visitor's language is chosen |
|---|---|
<html lang=""> | Reads the host page's declared language attribute |
?lang= URL param | Reads a query string parameter, e.g. ?form=your-slug&lang=ru |
| Browser language | Reads 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.
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.
What the visitor sees, in order
| Step | Field(s) that control it |
|---|---|
| 1. Right after submitting | Success 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 email | Subject 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 body | Email 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 link | Title and Message, under "Page shown after clicking the confirm link" — what the visitor sees on the landing page once their subscription is confirmed. |
Spam protection
| Protection | What it does |
|---|---|
| Honeypot | Hidden field that only bots fill in — a submission with it populated is silently discarded |
| Time check | Rejects submissions completed faster than a human could plausibly fill the form |
| Block disposable | Rejects known disposable/temporary email domains |
| Verify MX | Rejects 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.
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.
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%.
| Setting | Meaning |
|---|---|
| Confidence threshold | The probability-of-best percentage required before a winner can be auto-declared |
| Min sample size | Minimum visitors per variant required before a winner can be declared — prevents calling a test on too little data |
| Auto-declare | When 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. |
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.
Security
- Always use HTTPS. Signups carry personal data (email, name, IP) and consent records — never run this over plain HTTP.
- The controller admin is protected by your admin password. Use a strong password (the setup wizard enforces a minimum length).
- Keep spam protection enabled — honeypot and time check catch the majority of bot submissions with zero cost to real visitors.
- Restrict admin access by IP if possible. In Nginx, inside the
locationblock for your CW directory:
allow 1.2.3.4; deny all; - Review your GDPR text and consent settings with your own legal judgment — Signup Manager logs what it's told to log, but the wording and requirement (checkbox on/off) are yours to configure correctly for your jurisdiction.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 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