Documentation v1.0.0
GhostGrid
GhostGrid is a Laravel backend package for tenant-based reseller commerce. It handles account hierarchy, providers, products, pricelists, storefront display products, order snapshots, provisioning tasks, and services.
Supported Laravel versions: 10, 11, 12, and 13. Laravel 13 is tested on PHP 8.3+.
Installation
composer require ghostcompiler/ghostgrid
php artisan gg:init
php artisan migrate
Manual publish is also available.
php artisan vendor:publish --tag=ghostgrid-config
php artisan vendor:publish --tag=ghostgrid-migrations
User Setup
Add the trait to your Laravel User model.
use GhostCompiler\GhostGrid\Support\Concerns\HasGhostGrid;
class User extends Authenticatable
{
use HasGhostGrid;
}
Now normal controllers can use the short helper.
$priceLists = ghostgrid()->priceLists();
$products = ghostgrid()->visibleStorefrontProducts();
In jobs, seeders, or tests, pass the user manually.
$priceLists = ghostgrid($user)->priceLists();
Simple API
This is the recommended public API for application code. The helper automatically scopes reads and writes to accounts linked to the current user.
$grid = ghostgrid();
$account = $grid->account();
$accounts = $grid->accounts();
$priceLists = $grid->priceLists();
$active = $grid->activePriceList();
$assigned = $grid->assignedPriceLists();
$drafts = $grid->unassignedPriceLists();
$priceList = $grid->priceList('Default VPS Pricing');
account_users.
Mental Model
Your Laravel user logs in. The GhostGrid account owns commerce data.
This is why account_id means accounts.id, not users.id.
Accounts
Accounts are flexible. GhostGrid does not force superadmin -> reseller -> client. You can build any tree.
$operator = ghostgrid()->createAccount([
'name' => 'Operator',
'type' => 'operator',
]);
$client = ghostgrid($clientUser)->createAccount(
attributes: ['name' => 'Client Website', 'type' => 'client'],
parent: $operator,
);
Providers
A provider database record stores provider identity. A provider adapter class executes dynamic provisioning actions.
$provider = ghostgrid()->createProvider('hetzner', 'Hetzner');
php artisan gg:vendor Hetzner
'provider_registry' => [
'hetzner' => App\GhostGrid\Providers\HetznerProvider::class,
],
Products
Products are catalog groups. Variants are the sellable items. Provider mappings connect variants to provider SKUs.
[$product, $variant] = ghostgrid()->createProduct(
product: [
'name' => 'Cloud VPS',
'slug' => 'cloud-vps',
'type' => 'vps',
],
variant: [
'name' => 'Hetzner CX32',
'slug' => 'cx32',
'billing_cycle' => 'monthly',
'specs' => ['ram' => '4 GB', 'disk' => '80 GB SSD'],
],
provider: $provider,
mapping: [
'provider_sku' => 'cx32',
'priority' => 100,
'config' => ['region' => 'fsn1'],
],
);
Pricelists
Create and assign a pricelist to the current user's account.
$priceList = ghostgrid()->createPriceList('Default VPS Pricing', [
[
'product_variant_id' => $variant->id,
'billing_cycle' => 'monthly',
'selling_price' => 1499,
'setup_fee' => 0,
],
]);
Create a draft without assigning it.
$draft = ghostgrid()->createUnassignedPriceList('Draft Sale Pricing', [
[
'product_variant_id' => $variant->id,
'billing_cycle' => 'monthly',
'selling_price' => 1299,
],
]);
Assign it later.
$assignment = ghostgrid()->assignPriceList($draft);
Assign with a date range.
$assignment = ghostgrid()->assignPriceList(
priceList: $draft,
account: $clientAccount,
isDefault: true,
startsAt: '2026-06-01 00:00:00',
endsAt: '2026-06-30 23:59:59',
);
Master Pricelist
A master pricelist is the source of products a tenant may sell. Tenant pricelists can copy those products and activate only the items they price.
$tenantPriceList = ghostgrid()->createPriceListFromParent(
parent: $masterPriceList,
name: 'Tenant VPS Pricing',
itemOverrides: [
$variant->id => [
'selling_price' => 1499,
'enabled' => true,
],
],
);
If a tenant does not set a product price, the item is copied but inactive.
[
'product_variant_id' => $variant->id,
'selling_price' => '1000.00',
'enabled' => false,
'metadata' => [
'inherited_from_parent' => true,
'tenant_price_set' => false,
],
]
$createdInactiveItems = ghostgrid()->syncPriceListWithParent($tenantPriceList);
Storefront Products
Storefront products are frontend display records. They do not replace backend product variants.
$storefrontProduct = ghostgrid()->createStorefrontProduct([
'product_variant_id' => $variant->id,
'price_list_id' => $priceList->id,
'name' => 'Premium VPS 4GB',
'slug' => 'premium-vps-4gb',
'description' => 'Fast cloud server for growing sites',
'actual_price' => 2000,
'price' => 1499,
'popular' => true,
'visible' => true,
'featured' => true,
'features' => [
['label' => '4 GB RAM', 'icon' => 'server'],
['label' => '80 GB SSD', 'icon' => 'hard-drive'],
['label' => 'Free Setup', 'icon' => null],
],
]);
$products = ghostgrid()->visibleStorefrontProducts();
Orders
Orders are created from the active assigned pricelist. GhostGrid snapshots product names, prices, setup fees, and provider mapping data at order time.
use GhostCompiler\GhostGrid\Actions\CreateOrderFromCart;
$order = app(CreateOrderFromCart::class)->handle(
accountId: ghostgrid()->account()->id,
cartItems: [
[
'product_variant_id' => $variant->id,
'billing_cycle' => 'monthly',
'quantity' => 1,
],
],
);
Payment To Provisioning
After payment succeeds, mark the order paid. GhostGrid can create provisioning tasks and dispatch the queue job.
$paidOrder = ghostgrid()->markOrderPaid(
order: $order,
createProvisioningTasks: true,
dispatch: true,
);
Create tasks without dispatching.
$tasks = ghostgrid()->createProvisioningTasksForOrder(
order: $order,
dispatch: false,
);
Services
A successful provisioning create action creates or updates a service. Lifecycle helpers create provider action tasks.
ghostgrid()->suspendService($service, dispatch: true);
ghostgrid()->unsuspendService($service, dispatch: true);
ghostgrid()->renewService($service, dispatch: true);
ghostgrid()->terminateService($service, dispatch: true);
Update And Archive
Most helper methods accept a model instance or ID. For live commerce, prefer archive/disable over hard delete.
ghostgrid()->updatePriceList($priceList, ['name' => 'Published Pricing']);
ghostgrid()->archivePriceList($priceList);
ghostgrid()->deletePriceList($priceList);
ghostgrid()->addPriceListItem($priceList, [
'product_variant_id' => $variant->id,
'billing_cycle' => 'monthly',
'selling_price' => 1600,
]);
ghostgrid()->disablePriceListItem($item);
ghostgrid()->hideStorefrontProduct($storefrontProduct);
Provider Adapters
Adapters define their own capabilities. Core GhostGrid does not need updates for new provider actions.
use GhostCompiler\GhostGrid\DTO\ProvisioningResult;
interface ProvisioningProvider
{
public function code(): string;
public function name(): string;
public function capabilities(): array;
public function supports(string $action): bool;
public function execute(string $action, array $payload): ProvisioningResult;
}
use GhostCompiler\GhostGrid\Services\ProvisioningManager;
$result = app(ProvisioningManager::class)->execute(
providerCode: 'hetzner',
action: 'reboot',
payload: ['remote_id' => 'server-123'],
);
Configuration
GhostGrid exposes only options users should customize.
return [
'id_strategy' => env('GHOSTGRID_ID_STRATEGY', 'uuid'),
'default_currency' => env('GHOSTGRID_DEFAULT_CURRENCY', 'USD'),
'allow_selling_below_parent' => env('GHOSTGRID_ALLOW_SELLING_BELOW_PARENT', false),
'queue_connection' => env('GHOSTGRID_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')),
'provider_registry' => [],
'model_overrides' => [],
'storefront' => [
'default_visible' => true,
'default_featured' => false,
],
'enabled_modules' => [
'accounts' => true,
'providers' => true,
'pricing' => true,
'storefront' => true,
'orders' => true,
'provisioning' => true,
'services' => true,
],
];
ID Strategy
UUID IDs are the default. Choose the ID strategy before running migrations.
GHOSTGRID_ID_STRATEGY=uuid
# uuid, ulid, bigint
uuid: string UUID primary keys.ulid: string ULID primary keys.bigint: auto-incrementing big integer primary keys.- Relationship columns follow the same strategy and are indexed.
No Foreign Keys
GhostGrid does not create database foreign key constraints. This keeps tenant movement, provider imports, recovery, and reseller data operations flexible.
foreign(), constrained(), references(), cascadeOnDelete(), or nullOnDelete().
Commands
php artisan gg:init
php artisan gg:vendor Hetzner
php artisan gg:sync hetzner
php artisan gg:demo
php artisan gg:check
Helper Cheatsheet
$grid = ghostgrid();
$grid->createAccount([...]);
$grid->account();
$grid->accounts();
$grid->createChildAccount([...]);
$grid->archiveAccount($account);
$grid->createProvider('hetzner', 'Hetzner');
$grid->createProduct($product, $variant, $provider, $mapping);
$grid->priceLists();
$grid->activePriceList();
$grid->assignedPriceLists();
$grid->unassignedPriceLists();
$grid->priceList($idOrName);
$grid->createPriceList('Default', $items);
$grid->createUnassignedPriceList('Draft', $items);
$grid->createPriceListFromParent($masterPriceList, 'Tenant Pricing', $overrides);
$grid->syncPriceListWithParent($tenantPriceList);
$grid->assignPriceList($priceList);
$grid->archivePriceList($priceList);
$grid->disablePriceListItem($item);
$grid->createStorefrontProduct([...]);
$grid->visibleStorefrontProducts();
$grid->hideStorefrontProduct($storefrontProduct);
$grid->markOrderPaid($order, createProvisioningTasks: true, dispatch: true);
$grid->createProvisioningTasksForOrder($order);
$grid->suspendService($service);
$grid->unsuspendService($service);
$grid->renewService($service);
$grid->terminateService($service);
Raw Models
Helpers are recommended for tenant-aware app code. Raw Eloquent models and action classes are available for seeders, admin tools, and custom workflows.
use GhostCompiler\GhostGrid\Models\Account;
use GhostCompiler\GhostGrid\Models\PriceList;
$account = Account::create(['name' => 'Acme Hosting']);
$priceList = PriceList::create([
'account_id' => $account->id,
'name' => 'Default Pricing',
'currency' => 'USD',
]);
Factories
use GhostCompiler\GhostGrid\Models\Account;
use GhostCompiler\GhostGrid\Models\Product;
use GhostCompiler\GhostGrid\Models\ProductVariant;
use GhostCompiler\GhostGrid\Models\PriceList;
use GhostCompiler\GhostGrid\Models\StorefrontProduct;
$account = Account::factory()->create();
$product = Product::factory()->create();
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
$priceList = PriceList::factory()->create(['account_id' => $account->id]);
$storefront = StorefrontProduct::factory()->create([
'account_id' => $account->id,
'product_variant_id' => $variant->id,
]);
Database Schema
GhostGrid ships tables for accounts, account users, providers, credentials, products, variants, mappings, pricelists, items, assignments, storefront products, orders, order items, provisioning tasks, and services.
Testing
composer validate --strict
composer test
The repository includes GitHub Actions matrix coverage for Laravel 10, 11, 12, and 13.
FAQ
How do I get pricelists created by the user?
Use ghostgrid()->priceLists(). It returns pricelists from accounts linked to the logged-in user.
How do I create a pricelist but not assign it?
Use ghostgrid()->createUnassignedPriceList(...) or createPriceList(..., assign: false).
How do I assign an existing pricelist?
Use ghostgrid()->assignPriceList($priceList).
Does GhostGrid include a dashboard?
No. GhostGrid is a backend package. Your Laravel app controls UI, routes, policies, and billing UX.
Why no foreign keys?
Reseller systems often need imports, account movement, external provider sync, and flexible recovery. GhostGrid uses indexed relationship columns and leaves enforcement to application logic.