Developer guide

Build and sell themes and extensions

Nuvabill packages are plain folders of Blade views, CSS and PHP. Build one, test it on your own copy, then sell it on the Marketplace. You keep 83% of every sale.

1. What you can build

TypeManifestInstalled in
Client themetheme.jsonthemes/{slug}
Order formorderform.jsonorderforms/{slug}
Add-onextension.json, type addonextensions/addons/{slug}
Payment gatewayextension.json, type gatewayextensions/gateways/{slug}
Server moduleextension.json, type serverextensions/servers/{slug}
Domain registrarextension.json, type registrarextensions/registrars/{slug}

A package is a zip with the manifest at the top, or inside one top folder named after the slug. The built-in Nova theme, gateways, control panel modules and registrars use the same system, so they are complete examples. Read them on GitHub.

2. Try it on your own copy

  1. Install Nuvabill on your computer (see For developers in the read me) and load the demo data.
  2. Put your folder in the place from the table above, for example themes/mytheme.
  3. Open /preview/theme/mytheme or /preview/orderform/myform while signed in as staff. Only you see the preview. /preview/stop ends it.
  4. Switch add-ons and gateways on under Marketplace → Installed or Settings, and fill in their settings.

These folders are ignored by git in the Nuvabill repository, so your package never ends up in a Nuvabill commit.

3. The manifest

{
    "slug": "chat-alerts",
    "type": "addon",
    "name": "Team chat alerts",
    "version": "1.0.0",
    "namespace": "Acme\\ChatAlerts\\",
    "class": "Acme\\ChatAlerts\\ChatAlerts",
    "description": "Posts new orders and tickets to Telegram.",
    "author": "Acme Ltd",
    "requires": ">=0.3.0",
    "permissions": ["events", "admin-settings", "http:api.telegram.org"]
}
  • slug: lowercase letters, numbers, - and _. It is also the folder name and cannot change later.
  • version: every upload must have a higher version than the last one, for example 1.0.1.
  • requires: the oldest Nuvabill it works with, like >=0.3.0. Older sites will not install it.
  • paid: set "paid": true for items you sell. Nuvabill then asks for a license key.
  • namespace and class (extensions only): your PHP classes live in src/ and are loaded from this namespace.
  • permissions: what the package does, in plain codes. See Permissions.

4. Themes

The client area and store are rendered through the theme:: view namespace. Nuvabill looks for each view in the active theme first and falls back to Nova. So a theme only needs the files it changes:

themes/mytheme/
├── theme.json
├── views/
│   ├── layouts/app.blade.php        (optional: your own page frame)
│   ├── client/dashboard.blade.php   (optional: your own dashboard)
│   └── partials/brand-style.blade.php
└── public/
    └── mytheme.css
  • The smallest theme overrides only partials/brand-style.blade.php and links a stylesheet. Paper and Midnight work this way.
  • Files in public/ are served by Nuvabill. Link them with {{ package_asset('themes', 'mytheme', 'mytheme.css') }}. CSS, scripts, fonts and images are allowed.
  • If you write your own layout, keep <x-extension-head area="client" /> in the <head> so add-ons still work, and keep the "Powered by Nuvabill" credit.
  • Load everything from your package. Nuvabill's security headers block scripts, styles and fonts from other sites.
  • Read the brand colour with setting('branding.accent') so your theme matches each host's brand.

5. Order forms

An order form replaces the store views store/index, store/group and store/product. Views it does not include come from the active theme, then Nova.

For a one-page order form, use the ordering API that Swift uses:

  • GET /store-api/domains?q=name: availability and prices for a domain and a few suggestions.
  • POST /store-api/quote: a live total with product_id, billing_cycle, addons[], domain_action (register, transfer, own or none), domain and coupon.
  • POST /order: the same fields plus gateway and, for new clients, the sign-up fields. It creates the account, places the order and starts the payment.

Send the CSRF token with each POST, like any Laravel form.

6. Add-ons

An add-on is a feature that is not a gateway, server module or registrar: chat alerts, a live chat window, reports and so on. Its class extends App\Extensions\Addons\Addon:

namespace Acme\ChatAlerts;

use App\Events\OrderPlaced;
use App\Extensions\Addons\Addon;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;

class ChatAlerts extends Addon
{
    public function settingsFields(): array
    {
        return [
            'bot_token' => ['label' => 'Bot token', 'type' => 'password', 'required' => true],
            'chat_id' => ['label' => 'Chat ID', 'type' => 'text', 'required' => true],
        ];
    }

    public function boot(): void
    {
        Event::listen(OrderPlaced::class, function (OrderPlaced $event): void {
            try {
                Http::timeout(5)->post('https://api.telegram.org/bot'.$this->setting('bot_token').'/sendMessage', [
                    'chat_id' => $this->setting('chat_id'),
                    'text' => 'New order #'.$event->order->number,
                ]);
            } catch (\Throwable $exception) {
                report($exception); // never stop an order because a chat service is down
            }
        });
    }
}
  • settingsFields(): the settings form staff fill in. Types: text, password (stored encrypted), textarea and select with options. Read values with $this->setting('key').
  • isConfigured(): the add-on only runs when this is true. By default, every required field must be filled in.
  • boot(): runs on every request while the add-on is on. Listen to events here.
  • Events in App\Events: OrderPlaced ($order), InvoicePaid ($invoice), ServiceActivated ($service), TicketOpened ($ticket, $message) and TicketReplied ($ticket, $reply).
  • headHtml($area): HTML for the <head> of client pages (store and client area) or admin pages, for example a chat widget. Build it from checked values only; never print a setting as raw HTML.
  • contentSecurityPolicy(): extra hosts your script needs, like ['script-src' => ['https://embed.example.com'], 'connect-src' => ['wss://*.example.com']]. Only https:// and wss:// hosts are accepted.

A broken add-on is reported in the log and skipped, so it cannot take a site down. Still, keep network calls short and wrap them in try/catch.

7. Gateways, server modules and registrars

  • Payment gateways extend App\Extensions\Gateways\Gateway. Record payments only through the gateway's result so each payment is counted once. If your gateway charges in another currency (for example Iraqi dinar), Nuvabill converts with the host's exchange rate.
  • Server modules extend App\Extensions\Servers\Module: create, suspend, unsuspend, terminate and change package.
  • Domain registrars extend App\Extensions\Registrars\Registrar, like the built-in ResellerClub, Namecheap, Enom and OpenSRS modules.

The built-in modules in extensions/ on GitHub are the best starting point: copy one and change it.

8. Permissions

List what your package does in "permissions". Staff see these in plain words before they install, and reviewers check your code does nothing else.

CodeStaff see
client-areaChanges client area pages
storeChanges the store and order pages
client-pageAdds a client area page
admin-settingsAdds settings for staff
admin-pageAdds an admin page
eventsReacts to orders, payments and tickets
head-scriptAdds a script to your pages
paymentsTakes payments
serversManages accounts on your servers
domainsRegisters domains
databaseAdds its own database tables
http:host.example.comConnects to host.example.com

9. Automatic checks and review

When you upload a version, the store checks it at once:

  • Readable code only. ionCube, SourceGuardian and hidden code like eval(base64_decode(…)) are refused.
  • No program files. .phar, .exe, .dll, .so, .sh, .bat, .jar, .htaccess and similar are refused.
  • Size: up to 20 MB zipped, 100 MB and 5,000 files unpacked. No links or paths outside the package folder.
  • A higher version than the one already in the store, and a requires line.
  • Risky functions such as exec(), eval() and unserialize() are flagged for the reviewer.
  • Addresses in your code that are not in your http: permissions are flagged too.

Then a person reviews the code. If something needs to change, you get a message in your developer area and can reply or upload a fixed version. When it is approved, the store signs the package and it appears in every Nuvabill admin area. Updates go through the same steps.

10. Prices, licenses and payouts

  • You set two prices: the price, which includes one year of updates, and a yearly price for more updates. Buyers keep using the version they have if they stop paying for updates.
  • You keep 83% of each sale and each update renewal. The 17% fee pays for payments, license keys, downloads, hosting and reviews.
  • Payouts are monthly by Wayl, FIB, PayPal or bank transfer. Add your details in your developer area.
  • License keys are handled for you. Each buyer gets a key for one website. Test sites (localhost, *.test, *.local, staging.*, dev.*) never use up the key. Every paid download is stamped with the buyer's license, and Nuvabill checks the key about once a day. You do not need to write any license code.
  • Free items are welcome too, and are a good way to get known.

11. Rules

  • Only upload code you wrote or have the right to sell. Include the licenses of any libraries you use.
  • No encoded code, no hidden calls home, and no collecting data that the host has not agreed to in your description and permissions.
  • Themes must keep the "Powered by Nuvabill" credit. Hosts with a White-label License can hide it themselves.
  • Never ask buyers for their Nuvabill passwords, server passwords or API keys outside your settings form.
  • Answer your buyers' questions. Items with serious problems can be hidden from the store until they are fixed.

Ready? Join as a developer on my.nuvabill.com. Questions? Open an issue on GitHub.