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
| Type | Manifest | Installed in |
|---|---|---|
| Client theme | theme.json | themes/{slug} |
| Order form | orderform.json | orderforms/{slug} |
| Add-on | extension.json, type addon | extensions/addons/{slug} |
| Payment gateway | extension.json, type gateway | extensions/gateways/{slug} |
| Server module | extension.json, type server | extensions/servers/{slug} |
| Domain registrar | extension.json, type registrar | extensions/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
- Install Nuvabill on your computer (see For developers in the read me) and load the demo data.
- Put your folder in the place from the table above, for example
themes/mytheme. - Open
/preview/theme/mythemeor/preview/orderform/myformwhile signed in as staff. Only you see the preview./preview/stopends it. - 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": truefor 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.phpand 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 withproduct_id,billing_cycle,addons[],domain_action(register,transfer,ownornone),domainandcoupon.POST /order: the same fields plusgatewayand, 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),textareaandselectwithoptions. Read values with$this->setting('key'). - isConfigured(): the add-on only runs when this is true. By default, every
requiredfield 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) andTicketReplied($ticket,$reply). - headHtml($area): HTML for the
<head>ofclientpages (store and client area) oradminpages, 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']]. Onlyhttps://andwss://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.
| Code | Staff see |
|---|---|
client-area | Changes client area pages |
store | Changes the store and order pages |
client-page | Adds a client area page |
admin-settings | Adds settings for staff |
admin-page | Adds an admin page |
events | Reacts to orders, payments and tickets |
head-script | Adds a script to your pages |
payments | Takes payments |
servers | Manages accounts on your servers |
domains | Registers domains |
database | Adds its own database tables |
http:host.example.com | Connects 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,.htaccessand 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
requiresline. - Risky functions such as
exec(),eval()andunserialize()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.