G9Push Documentation
Everything you need to set up G9Push, install the self-hosted push server on your own hosting, manage subscribers across multiple websites, and recover from common issues. Last updated September 23, 2026.
1. What is G9Push?#
G9Push is a complete web-push notification platform you can self-host. You buy a license from G9Push.com, download the push-server software, install it on your own hosting, and use it to collect browser subscribers and send them notifications. Your subscribers and analytics stay on your server β G9Push.com only validates your license.
Two computers are involved: (a) G9Push.com, which we run, where you sign up, pay,
and download the software; and (b) your hosting (e.g. push.yoursite.com),
which you run, where the push notifications are actually sent and where your subscriber list lives.
2. The two parts you'll run#
| Part | What it does | Lives at |
|---|---|---|
| G9Push.com account | Sign up, verify your email, pick a plan, generate license keys, download the software ZIP, and manage your installations. | https://g9push.com |
| Push server | Where browsers subscribe, where notifications are sent from, where the subscriber list lives. Talks to G9Push.com only to validate your license. | https://your-domain.com/ (your hosting) |
3. Quick start checklist#
- Create an account on @/register.php and verify your email.
- Pick the 7-day trial or the paid package.
- Open your dashboard β Installations β Add Installation. Enter the domain your push server will live on. Copy the license key shown.
- From the same page, click Download Software. Upload the ZIP contents to your hosting.
- Open
https://your-domain.com/admin/setup.php. Paste the license key. Create the admin account. - Inside the push admin: Add Website, copy the embed snippet, paste it into your customer-facing site.
- People who click "Allow" become subscribers. Send your first campaign from the admin panel.
4. Create an account#
- Go to https://g9push.com/register.php (also accessible via the Sign Up button in the top right).
- Fill in your name, email, phone (optional unless phone verification is required by the admin), and password (8+ characters).
- Submit. You'll land on a "Check your inbox" page.
If you clicked "Start Free Trial" or "Get $20 Package" on the pricing section first, the same registration form is used and your plan choice is remembered until you confirm it after signing in.
5. Verify your email#
- Open your inbox. Look for an email from G9Push.
- Click "Confirm my email". The link is valid for 24 hours.
- You'll be taken to a confirmation page. Click Sign in.
Check spam first. If it really hasn't arrived after 1β2 minutes, click Resend it from the link below the sign-in form. If multiple resends fail, the SMTP on the G9Push.com side may be misconfigured β contact support@g9push.com.
6. Sign in#
- Go to https://g9push.com/dashboard/login.php.
- Enter your email and password.
- If you have a pending plan choice and haven't activated it yet, you'll land on the Plan confirmation screen. Click Activate.
- If phone verification is required (admin setting), you'll be sent to
/verify-phone.phpnext. - Otherwise you land on the Installations dashboard.
Use Forgot password on the sign-in page. You'll get a reset link by email.
7. Phone (SMS) verification#
If the G9Push.com admin has turned on Require phone verification, the first time you sign in after
verifying your email you'll be sent to /verify-phone.php. Two paths exist depending on what SMS
provider the admin configured:
7a. MSG91 widget (preferred)
- The page shows "Sending code to <your phone>β¦"
- An SMS arrives with a 6-digit code.
- Type it in the input field. Click Verify.
- On success the page jumps to plan-confirm (if you have a pending plan) or to Installations.
7b. Server-side OTP (fallback)
If MSG91's widget isn't configured, the system generates its own 6-digit code and texts it via the configured SMS provider (Twilio / MSG91 sendhttp / etc.). Same UX, same outcome.
Click Resend code. If multiple retries fail, the provider's balance may be exhausted, the sender ID may not be approved, or your number may be outside the provider's covered countries β contact support.
You need three different values from MSG91, all in different places on their dashboard:
- Widget ID (Verify / OTP widget config page, ~24 hex chars)
- Widget tokenAuth (same page, long JWT with dots)
- Account Auth Key (top-right profile menu β Auth Key, ~24 alphanumeric chars, not a JWT)
Pasting the wrong one in the wrong field is the #1 cause of "HTTP 400 Authentication Failure". Use the in-app Test authkey now button under admin β Settings to verify your key directly with MSG91 before launching.
8. Plans & pricing#
| Plan | What you get | How long |
|---|---|---|
| Free Trial | Full access to one push-server installation. | 7 days (admin-configurable) |
| Paid Package | One push-server installation, can host unlimited websites underneath it. Manage up to 6+ websites with one license. | One-time payment, no recurring fee |
You can host multiple customer-facing websites under one push-server installation. Each website you add inside the push admin is a "site" β and they all share the same master VAPID key automatically. See Β§26 Managing 2+ sites on one license.
9. Coupons#
If you have a coupon code, paste it on the Checkout screen for the paid package. Two coupon types:
- Percent off (e.g. 20% off the package price)
- Fixed amount off (e.g. $5 off)
Coupons can be tied to affiliates for revenue sharing β that's handled on the admin side.
10. Download the software#
- Sign in to your G9Push.com dashboard.
- Go to Installations.
- Click Add Installation. Enter the domain your push server will live on (e.g.
push.yoursite.com). Give it a friendly name. - The page shows your license key β copy it now and store it somewhere safe (it's shown once).
- Click Download Software. You'll get a ZIP file (typically ~250 KB).
The license key is displayed in plain text once. After that the server only stores a hash for security. If you lose it, you can either generate a 30-minute installation token on the same page (better for setup), or ask the admin to issue a new license for you β see Β§34.
11. Upload to your hosting#
- Unzip
g9push-software-YYYYMMDD.zipon your computer. - Inside is a folder
g9push-software/. Upload the contents (not the folder itself) to your hosting's web root for the push subdomain.
For example, if your push domain ispush.yoursite.commapped to/var/www/push/, upload there so that/var/www/push/admin/setup.phpexists. - Make sure your hosting has PHP 7.4+ (PHP 8.x recommended), MySQL/MariaDB, and the OpenSSL + cURL + GMP/BCMath PHP extensions enabled (all standard).
12. Database configuration#
- Create a new MySQL database via your hosting control panel (cPanel / Plesk / shell). Note the host, database name, user, password.
- Import
sql/push_notifications_schema.sqlinto the new database (phpMyAdmin β Import). - Rename
api/config.example.phptoapi/config.phpand fill in your credentials:
<?php
define('DB_HOST', 'localhost');
define('DB_NAME', 'your_database_here');
define('DB_USER', 'your_db_user');
define('DB_PASS', 'your_db_password');
// Optional Redis cache (leave host empty to disable).
define('REDIS_HOST', '');
define('REDIS_PORT', 6379);
define('REDIS_PASS', null);
try {
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die(json_encode(['error' => 'Database connection failed']));
}
13. Run the setup wizard#
- Open
https://your-domain.com/admin/setup.phpin a browser. - Step 1: Database check. Should pass instantly if
api/config.phpis correct. If it fails, fix the credentials and reload. - Step 2: License. Two modes:
- License key: paste the long key from your dashboard.
- Installation token: paste a 30-minute one-shot token (better β it can't be replayed). Generate one on the Installations page.
- Step 3: Admin account. Enter an admin email + password (8+ chars). Click Create admin & sign in.
- You're now auto-signed-in to
/admin/index.phpβ the push admin panel.
This means PHP cannot write to the disk path it tried. The wizard now lists candidate paths it tried; the
file ends up in the first writable one. If all candidates fail, set
define('G9PUSH_LICENSE_FILE', '/absolute/path/that/is/writable/license.php');
in api/config.php and re-run. See Β§32.
14. Where the license is stored#
The license cache is a small PHP file that the server checks on every request. It picks the first writable location from:
- An explicit override via
G9PUSH_LICENSE_FILEconstant (set inapi/config.php). <parent-of-webroot>/.g9push-license/license.php(most secure)<install-root>/.storage/license.php<install-root>/api/.g9push-cache/license.php- PHP's
session_save_path()directory (universally writable) - System temp directory (last resort)
The file is auto-created. On most hosting one of the first three works; PHP's session path is the bulletproof fallback because if sessions don't work, nothing on your site works.
15. Sign in to your push admin#
- Open
https://your-domain.com/admin/login.php. - Enter the admin email + password you set in step 13.
- You land on the dashboard.
16. Add a website#
Each "website" inside your push admin is a separate place where you'll collect subscribers (e.g. one site for your blog, another for your shop). One installation can host as many sites as you want.
- In the push admin sidebar, click Manage Sites.
- Click Create Website. Enter:
- Name: e.g. "Main blog"
- Domain: the customer-facing domain where you'll embed the snippet (e.g.
yourblog.com) - Bell widget + Soft-ask: optional opt-in styles
- Initial / retry delay: when to first show the prompt
- Click Create Website. The new site appears in the sidebar.
When you create a new website, the push admin copies the same master VAPID keypair from your oldest site. This is intentional β it means you can freely move/copy subscribers between your own sites (see Β§27) and they keep working.
17. VAPID keys explained#
A VAPID keypair is the cryptographic identity your push server uses to sign every notification. The browser stores the public half against each subscription when it's created; the push service (Google FCM / Mozilla / Apple) verifies the signature against that stored key before delivering. If the keys don't match, the push is rejected with HTTP 403 (or 410 once the subscription is invalidated).
What the panel shows
Each site has three columns in the database: vapid_public, vapid_private_enc, vapid_private_pem. All three are filled automatically when you create a site.
Are all my sites on the same key?
Push admin β Import/Export tab β VAPID key alignment table. Each row shows a site + its key fingerprint + β/β. If everything is β, all sites share one key, and cross-site transfer works.
It is the single most destructive button in the panel. Regenerating instantly invalidates every subscriber
on every site β they have to revisit your site and click Allow again to come back. There is no recovery; the
browser holds the keys, not you. The button is now red, labelled "(DANGER)", and requires typing WIPE ALL SUBSCRIBERS
to proceed. Don't.
18. Embed snippet / WordPress plugin#
You install G9Push on your customer-facing website in one of three ways: a one-click WordPress plugin (recommended for WP), a copy-paste HTML snippet, or a theme-footer include for Shopify and others.
18a. WordPress plugin β recommended for WordPress sites
The plugin generated by your push admin is pre-configured for one specific site β the VAPID public key, site ID, embed snippet, bell-widget choice and other settings are baked in. You don't paste any keys or edit any files. Install, activate, done.
Where to download the plugin
- Sign in to your push-server admin panel (e.g.
https://push.yourdomain.com/admin/). - In the sidebar, click π Manage Sites.
- Find the site you want the plugin for. Click the π¦ WP Plugin button on that row.
- A file like
g9push-yoursite-com.zipdownloads to your computer (typically <100 KB).
Install the plugin in WordPress β Option A (admin upload, fastest)
- Sign in to your WordPress admin:
https://yoursite.com/wp-admin/. - In the WP sidebar, click Plugins β Add New Plugin β Upload Plugin (button at the top of the page).
- Click Choose File β select the
g9push-yoursite-com.zipyou just downloaded. - Click Install Now. WordPress unzips and installs it.
- Click Activate Plugin.
- Visit your site in a fresh incognito tab. After a moment, you should see the bell-widget appear (or the "Allow notifications?" prompt, depending on your site's settings). Click Allow.
- Back in your push admin β π Overview β the row for that site should show 1 live subscriber within a minute. That's you.
Install the plugin in WordPress β Option B (FTP, if you can't upload via admin)
- Unzip
g9push-yoursite-com.zipon your computer. Inside is a folder named e.g.g9push-yoursite-com/. - FTP into your WordPress hosting and navigate to
wp-content/plugins/. - Upload the entire
g9push-yoursite-com/folder there. - Go to WP admin β Plugins. You should see G9Push for yoursite.com in the list (deactivated).
- Click Activate.
- Verify as in steps 6-7 of Option A.
Service worker β critical to verify
Web Push requires a service worker file (sw.js) accessible at the root of your domain. The plugin handles this for you, but on some WordPress hosts (especially behind aggressive caches or with strict .htaccess) it can be blocked. After activating the plugin, open this URL in your browser:
https://yoursite.com/sw.js
You should see a JavaScript file (not a 404 page, not a redirect). If you see a 404:
- If using a caching plugin (WP Rocket, W3 Total Cache, LiteSpeed, etc.) β clear its cache and add
sw.jsto its exclusion list. - If using a CDN (Cloudflare, KeyCDN) β purge cache for
sw.jsand configure the CDN to NOT cache that file. - If
.htaccessblocks unknown files β add an allow rule for/sw.js.
Updating the plugin later
The plugin is pre-configured per site, so it doesn't auto-update via the WordPress repository. To upgrade:
- Download a fresh plugin ZIP from push admin β Manage Sites β π¦ WP Plugin.
- In WordPress admin β Plugins, deactivate the existing one (don't delete β that wipes its options).
- Upload the new ZIP (Option A) β WP will ask to replace; confirm.
- Activate again.
Existing subscribers are NOT affected by a plugin update β only the JS files on disk change.
Removing the plugin
WP admin β Plugins β Deactivate, then Delete. Existing subscribers continue to receive your campaigns until they uninstall the browser-side subscription themselves. To stop sending to them entirely, you can also revoke the corresponding site in your push admin.
If you run WP Multisite, install the plugin per subsite. Each subsite needs its own site row in your push admin (so each gets a separate plugin ZIP) β the plugin doesn't currently auto-route based on hostname.
18b. Plain HTML site / static site
Push admin β click a site β Integration β copy the JavaScript snippet. Paste it just before the closing </body> tag on every page of your customer-facing site:
<script src="https://push.yourdomain.com/sdk.js" data-site-id="3" data-vapid-public="BLβ¦"></script> </body>
Also upload the service worker sw.js to the root of your domain (not in a subfolder β the browser strictly requires this).
18c. Shopify / other CMS
For Shopify, paste the JavaScript snippet into Online Store β Themes β Edit code β theme.liquid, just before </body>. For the service worker, upload sw.js as a theme asset and add a redirect rule that maps /sw.js to that asset URL β or use a third-party "service worker proxy" app from the Shopify app store.
For Wix, Squarespace and similar closed CMSes that don't let you serve sw.js from the root: web push will not work without root-level service worker access. Consider moving to a CMS that does (WordPress, Ghost, custom).
19. How a subscription works (the mailbox analogy)#
The cleanest way to think about a push subscription:
- A person visits your site and clicks Allow. Their browser asks the push service (Google for Chrome, Mozilla for Firefox) to create a mailbox for them.
- The push service creates the mailbox and gives the browser an address (a URL like
https://fcm.googleapis.com/fcm/send/...). The browser hands this address to your server. - Your server writes the address down in the
subscriptionstable. - To send a notification, your server drops a message at that address. The push service receives it and delivers it to the person's browser.
Your database row is just a piece of paper with an address written on it. That's why copying the address between websites doesn't create a new mailbox β only the person, by revisiting your site and clicking Allow, can create one.
20. Live vs dead subscribers#
A subscription becomes dead when the mailbox is destroyed: the user unsubscribed, cleared their browser data, their browser auto-expired the subscription, or the VAPID key changed so the push service no longer trusts your sender. When you try to send to a dead mailbox, the push service replies with an HTTP error and the subscription is auto-marked as expired in your database.
| HTTP code | What it means | Recovery |
|---|---|---|
| 200 / 201 | Delivered β the push reached the user's browser. | β |
| 410 Gone | The mailbox no longer exists. Permanent. | Auto-cleaned. Person must re-subscribe by revisiting your site. |
| 403 Forbidden | VAPID key mismatch β the subscription was created with a different key than the one signing this push. | Auto-cleaned. Recovers when the person revisits (browser re-subscribes with the current key). |
| 401 Unauthorized | JWT rejected (audience, signature, sub claim). | Rare. Often only affects Windows WNS subscribers, which use a different auth model that this software doesn't fully support. |
The push service doesn't tell you "this subscription is dead" until you actually try to send. So a fresh subscriber count is only ever approximately the number of live mailboxes. Every campaign you send refines the count β dead ones get moved to the Dead column automatically.
21. Cleaning up dead subscribers#
Dead subscribers (subscriptions a push service has rejected as 410 / 403 / 401-on-WNS) take up database space and pollute your "Live" count. G9Push gives you three different tools, in different places, for different situations.
21a. Where the controls live
| Where | What you'll see | Scope |
|---|---|---|
| π Overview tab (default landing) | "π All your websites" table with one row per site + grand totals at top | Across all your sites |
| π₯ Subscribers tab | "π Subscriber Health" card showing Live / Dead / % for the currently-selected site | Just the selected site |
21b. The three tools β when to use which
π Validate (Overview β per-site row)
The most thorough option. When you click it on a site's row:
- You're warned: "This will send a REAL push notification to N active subscriber(s)."
- You're prompted for the notification title and body (pre-filled with a neutral "π [Site Name] β Thanks for staying subscribed. (Service validation)").
- The system sends that notification to every active subscriber of the site.
- Push services that respond with 410 / 403 / 401-on-WNS reveal which subscribers are dead.
- The system marks those dead and then physically DELETES every dead row for that site (newly detected + any leftovers).
- A summary alert tells you the final numbers: "Sent to 47, Delivered 30, Newly dead 17, Deleted 17. Now: 30 live, 0 dead."
- The row updates in place; grand totals recompute.
Every active subscriber sees the notification you typed. That's the cost of getting a definitive count β there's no silent way to validate a Web Push subscription (Chrome forces a visible notification on every push). Use Validate sparingly β once a month, after a big VAPID change, after spotting suspicious counts, etc.
π§Ή Delete dead (Overview β per-site row, and Subscribers tab β Health card)
Just removes already-dead rows. Doesn't send anything. Use after a normal campaign already detected the dead subscribers β this physically removes them from the database.
- On the Overview tab: the π§Ή button on each row deletes dead rows for that one site.
- On the Subscribers tab: the Health card has two buttons β "Delete N dead β this site" and "Delete N dead β ALL sites".
π Refresh all (Overview β top right of the All-Websites card)
Re-reads the database for every site and updates the counts in place. Does NOT send anything, does NOT detect new dead. Useful to confirm a refresh after a campaign, or when another admin made changes elsewhere.
Just clicking π doesn't discover newly-dead subscribers β the push services don't expose a "is this alive?" endpoint. The dead count only grows when you actually try to send (a real campaign or a π Validate). Until then, "Live" includes anyone who unsubscribed silently.
21c. Recommended workflows
Routine (do this after every campaign)
- Send your campaign normally.
- The campaign result shows e.g. "30 delivered, 16 expired (auto-cleaned)" β those 16 are now status='expired'.
- On the Overview tab, click π§Ή on that site's row (or on Subscribers β Health β Delete N dead β this site) to remove the 16 expired rows from the database.
Periodic full audit (monthly)
- For each site you manage, click π Validate on its row.
- Customise the notification text or accept the default.
- The send fires, the dead are detected, the dead rows are deleted. Result alert tells you the new authoritative numbers.
- Your Live count is now a definitive, real number.
Bulk reclaim disk space (after a long period of campaign sends)
- On Subscribers tab β π Subscriber Health β click Delete N dead β ALL sites.
- Confirms across all sites in one click. Removes every dead row everywhere.
The cleanup SQL is always DELETE FROM subscriptions WHERE status <> 'active'. Live subscribers are physically impossible to hit β the query has no path to them. Dead rows are useless anyway: the mailbox they point to is permanently gone in the user's browser, and no amount of re-importing brings it back. Only the person revisiting your site and clicking Allow restores a subscription.
21d. Does the dashboard update automatically after a campaign?
Yes. After a campaign send:
- The page fully reloads on POST β all dashboard cards re-query the database β counts are fresh.
- Switching to the Subscribers tab triggers an extra AJAX refresh of the Health card (belt-and-braces).
- Switching back to the Overview tab triggers an AJAX refresh of every row in the All-Websites table.
- Manual π Refresh all is always available for instant re-pull.
No manual page reload is needed in the normal flow.
22. Pagination & country filter#
The Subscribers list paginates 100 rows per page (large totals get a sliding window pager: βΉ Prev 1 β¦ 2 3 4 β¦ 12 Next βΊ). All filters (country, search) survive page navigation; reloading directly to ?page=2 stays on the Subscribers tab automatically.
The Country dropdown uses a timezone-first detection that matches the table column's country, so a subscriber with locale=en-GB and tz=Asia/Kolkata appears under "India" in both the table and the filter.
23. Send a campaign#
- Push admin β Campaigns β Send Campaign.
- Fill in Title, Body, optional Icon URL / Image URL / Click URL.
- Choose target audience: All subscribers, a saved Segment, or specific countries.
- Click Send Now. The page reloads with a result summary.
If you have many subscribers, sending may take a minute. The system sets set_time_limit(0)
+ ignore_user_abort(true) so the send completes even if your browser tab times out. Refresh the page after a couple of minutes to see the final count.
24. Reading campaign results#
After sending, the result line looks like:
Campaign sent! 124 delivered, 18 expired (auto-cleaned), 2 failed.
- delivered β the push service accepted the push. The user will see the notification (if their browser is online; otherwise on next wake).
- expired (auto-cleaned) β 410 / 403 responses. These mailboxes were marked dead in the same operation; they won't be tried again.
- failed β temporary errors (e.g. 5xx). They stay active and can be retried.
Click View failure details to see the breakdown by HTTP status, by push provider (FCM / Mozilla / WNS), and a sample response body per code. See the table in Β§20 for what each status means.
25. A/B testing#
The A/B test sends Variant A to a random 50% of the selected audience and Variant B to the other 50%. Set both titles + bodies, click Run A/B Test. Results show side by side so you can compare delivered + clicks.
26. Managing 2+ sites on one license#
One installation, one license, many websites. When you create a new site, the admin copies the same master VAPID keypair from your oldest site. Whether you add a site on day 1, day 30, or day 365 makes no difference β it always inherits the master key.
- Sign in to the push admin.
- Use the Site dropdown in the top bar to switch between websites.
- The Subscribers, Campaigns, and Settings tabs are per-site. Each site has its own subscriber list and analytics.
- The Import/Export tab's VAPID key alignment table shows that every site is on the same key (β aligned). This is what makes cross-site transfer safe.
27. Transfer subscribers between sites#
The supported, safe way to move subscribers between your own sites:
- Push admin β Import/Export tab β π Transfer subscribers between your own sites card.
- The status table at the top confirms all sites share the same VAPID key (green banner = good to go).
- Pick From site (source) β To site (destination).
- Choose Move (recommended β no duplicates) or Copy (subscriber receives campaigns from both sites).
- Click π Transfer subscribers now.
- The result confirms "β Both sites share the same VAPID key, so these subscribers WILL receive campaigns from the destination site."
Cross-site transfer (between your own sites on this installation) is supported by design β all your sites use the same VAPID keypair, so any subscription works from any of them. Cross-domain transfer (importing from someone else's domain) is not, and never will be β see Β§28.
28. Why cross-domain imports don't work#
A push subscription is cryptographically locked to (a) the browser's view of which domain created it and (b) the VAPID key in use at that moment. Pasting endpoint URLs into the Import tab from someone else's domain just creates dead database rows: the next campaign hits 410/403, the rows get auto-cleaned, your real subscriber count is unchanged.
This is a Web Push security rule (otherwise anyone could steal a website's audience by exporting endpoints). No setting and no code change can override it.
They have to visit your site and click Allow again. Their browser then creates a fresh subscription bound to your domain + your VAPID key. Common ways to reach them: a banner/redirect from the original domain (if you still control it), an email campaign with a link to your new site, or paid retargeting.
29. Moving the push server to new hosting#
You can move the push server to a different host without losing subscribers, as long as you bring three things with you:
| What to move | How |
|---|---|
| The PHP files | FTP / scp the entire push-server directory to the new host, OR re-download a fresh ZIP from G9Push.com. |
| The database | mysqldump on the old host, import on the new host. This carries your subscribers, sites, VAPID keys, admin users, and campaign history. |
| The license | Generate a new installation token on G9Push.com (Installations β your install β "Generate install token"). Paste it during the new host's setup wizard. |
30. Step-by-step migration#
- On the OLD host: run
mysqldump -u USER -p DB_NAME > backup.sql. Downloadbackup.sql. - On the NEW host: create a new MySQL database. Import
backup.sqlinto it. - Upload the push-server PHP files to the new web root.
- Edit
api/config.phpon the new host with the new DB credentials. - On G9Push.com β Installations β click your install β click Generate install token. Copy the token.
- Open
https://new-domain.com/admin/setup.php. Step 2 β paste the install token β Activate Installation. - Step 3 β see Β§31 for what happens here.
- Update the DNS for your push subdomain to point at the new host.
- Test sending a campaign. The result should match what the old host had.
Because you migrated the database, the subscriptions table comes with you intact. As long as the same VAPID keys are in the new sites rows (they will be β you migrated those too), the push service still trusts your sender and delivers normally.
31. "An admin already exists for this installation"#
This message appears in Step 3 of the setup wizard on the new hosting after a migration, because the
admin_users table you brought with you already contains your original admin row. The wizard refuses
to silently overwrite it. Two paths from here:
If you remember the old password
- Go to
/admin/login.php. - Sign in with your existing admin email + password. Done.
If you forgot the old password
The setup wizard now lets you reset it directly (Step 3 changes to Reset your admin password):
- Re-open
/admin/setup.phpon the new host. - Step 3 form says "An admin account already exists in this database⦠If you forgot the password, enter the same email plus a new password."
- Enter your existing admin email (must match exactly, case-sensitive) + a new password.
- Click Reset password & continue. You're auto-signed-in and dropped on the admin dashboard.
The reset runs a single SQL: UPDATE admin_users SET password_hash = ... WHERE email = ?.
Only the admin row's password column changes. Subscribers, sites, campaigns, analytics β none of these tables are touched.
32. License-file troubleshooting#
If activation reports "License accepted by G9Push, but this server could not save the license cache", the file-system on your new hosting is blocking writes to all auto-detected paths. Force a specific writable path:
- SSH or FTP into the new hosting.
- Pick a directory you know is writable by PHP. The safest universal choice is the directory PHP uses for sessions; you can find it by visiting
/api/?path=phpinfoor by runningecho session_save_path();in any small PHP file. Common values:/tmp,/var/lib/php/sessions, your user's home directory. - Open
api/config.phpand add one line near the top:
define('G9PUSH_LICENSE_FILE', '/absolute/path/to/g9push-license.php');
- Save the file. Re-run
/admin/setup.phpand paste a new installation token. - The wizard now writes to your chosen path and proceeds to Step 3.
33. Common errors & fixes#
| Symptom | Most likely cause | Fix |
|---|---|---|
| "Email already registered" on signup | The email is in saas_users. Either you already have an account, or a prior account wasn't fully deleted. |
Sign in instead (forgot password works). If a SaaS admin truly hard-deleted the user, the email is then free for fresh signup. |
| SMS OTP never arrives | SMS provider credentials wrong, balance zero, or sender ID not approved for the recipient's country. | Admin β Settings β SMS provider. For MSG91, use the Test authkey now button to confirm the key is valid. |
| "Authentication Failure (http400)" on MSG91 verifyAccessToken | The Server authkey field has the wrong value (often the widget tokenAuth pasted by mistake). | Use the account-level Auth Key from MSG91 profile menu (~24 alphanumeric chars, no dots). Run Test authkey now. |
| Campaign result is "Unknown error" | send-campaign.php returned a non-JSON body (PHP fatal or 500). | Recent versions surface the real error. Check the PHP error log for the matching [g9push] line. Make sure the latest admin/index.php and api/send-campaign.php are uploaded. |
| Sent "X delivered, Y expired" where Y is large | Either the old subscribers pre-date a VAPID key change (recover by users revisiting), or they were imported from another domain (permanent β see Β§28). | The Y subscribers are now auto-cleaned. Run cleanup (Β§21) to physically reclaim the rows. |
Push server keeps redirecting to /admin/setup.php |
License cache file isn't on disk (write failed silently in an older version) OR no admin user exists. | Update to the latest api/license.php; re-run setup.php; if it errors, set G9PUSH_LICENSE_FILE explicitly. See Β§32. |
| HTTP 401 on a single Windows subscriber | WNS (legacy Windows / old Edge) uses a different auth model than VAPID. | Generally negligible (1β2 subscribers). The auto-clean handles it. |
| Service worker version mismatch in browser console | The SW is cached by the browser; a new version was deployed. | Bump the SDK version on the admin's site config; subscribers' browsers will re-fetch on next visit. |
34. Playbook β license cache won't save on your hosting#
You paste your installation token at /admin/setup.php, click Activate, and the wizard shows:
"License accepted by G9Push, but this server could not save the license cache to /var/www/your-site/.storage/license.php. PHP cannot write to that path."
Why this happens
The push-server software needs to save a small license-cache file so it doesn't have to ask G9Push on every request. On many shared / managed hosts (AWS EC2, DigitalOcean default Apache, cPanel under suEXEC), the install directory and its children are owned by your deploy user (e.g. admin, root, ec2-user) but PHP runs as a different user (typically www-data). PHP can read but can't write into the install β so the cache write silently fails.
Fix
The auto-detect logic already tries several writable locations (above the docroot, inside the install, PHP's session save path, the system temp directory). If none work, give it an explicit path you know is writable:
- SSH into your server.
- Find PHP's session save path (almost always writable):
php -r 'echo session_save_path(), "\n";' # common results: /var/lib/php/sessions, /tmp, or your home directory
- Open
api/config.phpand add ONE line near the top, before thetryblock:define('G9PUSH_LICENSE_FILE', '/var/lib/php/sessions/g9push-license.php'); // β replace path with what session_save_path() returned - Save the file. Re-run
/admin/setup.phpwith a fresh installation token (generate a new one from your G9Push.com dashboard β the old one was single-use). - Activation should now succeed and move to Step 3 (admin password).
How to prevent
The simplest long-term fix is to give PHP write access to the install directory (see Β§38 for the exact chown/chmod commands). With write access, no G9PUSH_LICENSE_FILE override is needed β the auto-detect picks the install's .storage/ automatically and updates work cleanly.
35. Playbook β "An admin already exists for this installation" after hosting migration#
After moving your push server to a new host (uploading the files + importing the database), you re-run /admin/setup.php, activate the license, and Step 3 says:
"An admin already exists for this installation. Sign in to change credentials."
Why this happens
This is actually good news β it means your database migration worked. The admin_users table you brought with you still has your original admin row. The wizard is protecting that row from being silently overwritten. Your subscribers, sites, and campaigns are all intact.
Fix β if you remember the old admin password
- Go to
/admin/login.php. - Sign in with your existing admin email + password. Done.
Fix β if you forgot the password
The setup wizard now doubles as a password recovery tool. After license activation, Step 3 turns into a "Reset admin password" form:
- Re-open
/admin/setup.php. - Step 3 form should now read: "An admin account already exists in this database β most likely because you brought your existing database with you to new hosting. Your subscribers and all other data are safe and will NOT be touched."
- Enter your existing admin email (case-sensitive, must match exactly) + a new password.
- Click Reset password & continue. You're auto-signed-in and dropped on the admin dashboard.
The reset runs UPDATE admin_users SET password_hash = ... WHERE email = ?. Only that single row's password column changes. The subscriptions table is never touched.
36. Playbook β update fails with "Could not open temp file for writing"#
You click Update Now on the push admin's Updates page. The first step shows:
"Downloading https://g9push.com/api/release-download.php?version=β¦ β Could not open temp file for writing."
Why this happens
The updater needs a writable scratch directory to download the new ZIP, extract it for staging, and back up your current install before applying. By default it uses <install>/storage/. On hosts where the install isn't writable by PHP (same root cause as Β§34), that's exactly the file that fails to open.
Fix
The fix has two layers β the updater needs a writable temp area and write access to the install root itself (because it has to overwrite the existing PHP files).
- SSH into your server (or use FTP if SSH isn't available).
- Grant PHP write access to the install. Replace
adminwith YOUR SSH/FTP username (find it in your shell prompt β e.g.admin@host:~$means username isadmin):sudo chown -R admin:www-data /var/www/your-install/ sudo chmod -R 775 /var/www/your-install/
This makes you the owner (so FTP works) and gives the www-data group write access (so PHP can update). - Go back to your push admin's Updates page and click Update Now again.
If the install root really cannot be made writable
Some hosts enforce read-only installs. In that case, the updater shows a detailed "Option 2 β Apply the update manually via FTP" panel with a 7-step procedure: download the ZIP from your G9Push.com dashboard, unzip locally, FTP the contents over your install, preserving api/config.php. That always works.
How to prevent
Set up your install with PHP-writable permissions from day one (per the chown command above). It's a one-time setup that makes every future update click-through.
37. Playbook β update completed but admin panel redirects to setup.php#
The update applied successfully β the green "Update complete" page showed. But when you click Open admin panel, you're redirected to /admin/setup.php asking for an installation token again. Your subscribers and DB look untouched, but you can't get into the admin.
Why this happens
The release ZIP your push server downloaded contains an older copy of api/license.php β older than the customised version you had after fixing Β§34. The update overwrote your good api/license.php with that older copy. The older code doesn't know about the alternate license-storage paths (PHP session dir, system temp), so it looks in only one place, doesn't find the cache file, and concludes "no license configured" β redirect to setup.
Your license file is still on disk in its original location. The new code just doesn't know to look there.
Fix
- FTP into your hosting and navigate to
<install>/api/. - Upload the latest
api/license.phpfrom your developer machine (the version that knows about multiple candidate paths) over the one on the server. If you don't have a developer copy, contact support β they can email you the file. - Refresh
/admin/. The patched code's Pass 1 looks for an already-existing license file across all candidate paths; it finds yours in/tmp(or wherever) and resumes. You should land on the dashboard.
How to prevent
The updater now adds api/license.php to its preservation list (alongside api/config.php). From this update forward, the updater won't overwrite api/license.php automatically β your customised storage logic survives. If a future release genuinely needs to ship a new api/license.php, the release notes will tell you to upload it manually.
After upload, on the server: grep -c session_save_path /var/www/your-install/api/license.php. If it prints 1 or higher, you have the patched version. If 0, the FTP upload didn't actually replace the file β try again.
38. Playbook β FileZilla can't upload files after fixing PHP permissions#
FileZilla used to upload fine. After you ran a chown to give PHP write access (e.g. for the update fix), uploads now fail with "550 Permission denied".
Why this happens
The previous chown transferred ownership entirely to www-data (the PHP user). Your FTP user (likely your SSH login) is no longer the owner, so it can't overwrite files via FTP.
Fix β give both you and PHP write access
- SSH in. Note your username from the prompt β e.g.
admin@ip-172-β¦:~$means your username isadmin. - Run these two commands, replacing
adminwith your actual username:sudo chown -R admin:www-data /var/www/your-install/ sudo chmod -R 775 /var/www/your-install/
- Verify:
ls -la /var/www/your-install/api/license.php # should show: -rwxrwxr-x 1 admin www-data ... license.php # β admin owns, www-data is group, both have write
- Test a write as your user:
touch /var/www/your-install/api/test.txt && rm /var/www/your-install/api/test.txt && echo OK
If that printsOK, FileZilla uploads will work. - Reconnect FileZilla and retry the upload.
How to prevent
Never use chown -R www-data:www-data on the whole install β that takes ownership away from you. The admin:www-data + 775 combination above is the right baseline going forward.
In the commands above, admin is a placeholder. If your username is ec2-user / deploy / myaccount, substitute that. Running the command with the literal string your_ftp_username gives "chown: invalid user".
39. Playbook β campaign send fails or stays silent#
This one has multiple flavours. Match the exact message you see:
39a. "Campaign created but sending may be incomplete. Error: admin login required"
Why: The admin panel calls its own /api/send-campaign.php over loopback. That endpoint enforces admin auth β but the loopback call carries no browser session cookie, so it gets rejected.
Fix: Update to the latest software (use the in-admin updater per Β§36). The loopback path now signs a short-lived internal token that send-campaign.php recognises.
39b. "Campaign created but sending may be incomplete. Error: Unknown error"
Why: send-campaign.php returned an empty body or non-JSON output. Almost always a PHP fatal during a long send β default max_execution_time kills the script halfway through.
Fix: Update to the latest software. The updated send-campaign.php calls set_time_limit(0) and ignore_user_abort(true) after the auth + license gates, so big sends complete even if your browser tab times out.
39c. "send_exception: Call to a member function prepare() on null"
Why: A scoping bug in older versions where $pdo was created in a function scope and never made global.
Fix: Update to the latest software. The fix is in api/_guard.php β it now promotes $pdo into $GLOBALS when needed.
39d. "X delivered, Y expired (auto-cleaned)" β where Y is most of your audience
Why: The Y subscribers were either (a) imported from a different domain (permanent β see Β§28), or (b) created before a VAPID key change (see Β§17). Either way the push services say their mailboxes are gone (HTTP 410/403).
Fix: Nothing to recover. The Y subscribers can never receive notifications again unless they revisit your site and click Allow. Your real audience is now the X "delivered" number, which grows from here as people subscribe organically. See also Β§21 for the cleanup button to remove those dead DB rows.
40. Playbook β MSG91 "Authentication Failure (HTTP 400)" during OTP verify#
During phone verification on /verify-phone.php, the MSG91 widget shows the OTP send succeeded and the user types the right code, but verification fails with: "Server rejected verify token. Authentication Failure (http400)"
Why this happens (admin-side issue, but users see the symptom)
MSG91 has three different secret values in three different places on their dashboard, and they are very easy to mix up:
- Widget ID β short hex (~24 chars), from the OTP widget config page
- Widget tokenAuth β a long JWT (with dots), same widget config page
- Server Auth Key β short alphanumeric (~24 chars, NO dots), found in the MSG91 profile menu (top-right corner), labelled "Auth Key"
Pasting the long Widget tokenAuth into the "Server Auth Key" field is the #1 cause of this exact error.
Fix (if you administer the G9Push install)
- Go to admin β Settings β MSG91 OTP widget card.
- Look at the "Saved values" panel. The Server authkey row should show a short string (~24 chars). If it shows β "Looks like a JWT" or the length is 100+ chars, the wrong value is pasted.
- In MSG91 β click your profile icon (top-right) β click Auth Key. It's a short alphanumeric string, NO dots.
- Use the COPY button next to it (don't double-click + Ctrl-C β that often grabs trailing whitespace).
- Paste into the Server authkey field, save settings.
- Click the π Test MSG91 authkey button at the bottom of the page. A green β confirms the key is valid.
- Retry the OTP flow.
If Test authkey is green but verifies still fail
Your authkey is valid, but the OTP widget was created in a different MSG91 account than this authkey. Either copy the Auth Key from the same account that owns the widget, or create a new widget in the account whose Auth Key you have.
How to prevent
The settings page now shows a fingerprint preview of each saved key (length + first/last chars) and a red warning if the Server Auth Key field looks like a JWT. Glance at the preview after saving β that catches 90% of mistakes before users hit them.
41. Frequently asked questions#
Yes, by design. When you add a new site, the push admin copies the master VAPID keypair from your oldest site. To verify visually, go to push admin β Import/Export β the VAPID key alignment table β every row should show β aligned.
No. Timing doesn't matter. New sites always inherit the same master VAPID key, whether added on day 1 or day 365. They're fully compatible with subscribers collected on any of your other sites.
Those 67 subscriptions were no longer valid in the user's browser (mailbox destroyed β unsubscribed, browser cleared, key changed). The push service rejected them with HTTP 410/403. They've been auto-marked dead in your database and won't be tried again. To physically remove them, use the cleanup button (Β§21).
No. HTTP 410 is terminal β the mailbox is destroyed by the push service. The only way those people can receive notifications again is to revisit your site and click Allow, which makes their browser create a fresh subscription.
No, by design (Web Push security). A subscription is locked to the domain + VAPID key it was created on. Imported endpoints from another domain are dead on arrival β they fail with 410/403 and get auto-cleaned on the first campaign. The only way to reach those people is to get them to visit your site and re-subscribe.
Yes. The moment a campaign send detects a dead subscriber (410/403), the row's status is flipped to expired in the database. On the next dashboard page load, the Live count is already lower and the Dead count is already higher. No manual step required.
By default, just hidden (soft-deleted: status='expired'). They still take up disk space. Use the π§Ή Clean up database button on the Subscribers tab to physically remove them β that's a safe operation that only ever touches non-active rows.
Two separate accounts on two separate servers. G9Push.com is where you manage your license, billing, and downloads. Your push-server admin is on your own hosting (e.g. push.yoursite.com/admin/) and is where you actually send notifications and manage subscribers. They each have their own login.
As many as you want. One installation hosts unlimited sites underneath it. They all share the same VAPID key and subscribers can be freely transferred between them.
The first writable location from a list of candidates: above the web root (most secure), inside the install root, inside api/, PHP's session save path, or system temp. You can override with define('G9PUSH_LICENSE_FILE', '/absolute/path/license.php'); in api/config.php. See Β§14.
No. The only calls to G9Push.com are license validation (sends your license key + install domain + version) and an occasional heartbeat. Subscriber endpoints, push payloads, and analytics never leave your server.
That button instantly invalidates every subscriber on every site. There is no recovery β each person has to revisit your site and click Allow again to come back. (The button is now red, labelled "(DANGER)", and requires typing WIPE ALL SUBSCRIBERS to proceed, exactly to prevent this.) Going forward: never click it.
Your push server caches the last license-valid result for 6 hours, then enters a grace period of several days. During grace, everything keeps working normally. If grace runs out, the admin panel locks until G9Push.com is reachable again. Subscribers and analytics are preserved untouched.
42. Get help & report issues#
- Email support: support@g9push.com
- When asking for help, please include:
- The URL of the page where the issue appeared
- The exact error text (screenshot is fine)
- Whether this is on the G9Push.com side or your own push server
- Browser + OS for client-side issues
- For VAPID / OTP / license issues, the diagnostic panels in the relevant admin tab usually print exactly what's wrong (e.g. MSG91 code 418, the candidate path that failed to write). Paste that text into your support email.