> For the complete documentation index, see [llms.txt](https://juber.gitbook.io/bbb-guides/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://juber.gitbook.io/bbb-guides/setup.md).

# Setup

Lets users create Cloudflare DNS subdomains for their Minecraft servers directly from the Pterodactyl dashboard. When a subdomain is created, two Cloudflare records are added automatically: an **A record** and an **SRV record** so players can connect with just `play.yourdomain.com`.

## Requirements

* Pterodactyl Panel **v1.11**
* A Cloudflare account with your domain managed there
* A Cloudflare API token with **Zone › DNS › Edit** permissions
* Node.js / Yarn (to rebuild the frontend)

***

## Installation

{% stepper %}
{% step %}

### Copy files into Pterodactyl

Copy the contents of `pterodactyl/` into your Pterodactyl root (usually `/var/www/pterodactyl`), merging with existing directories. Make sure all Controllers, Models, Jobs, Views, scripts, and API typescript files are placed exactly as structured in the folder.
{% endstep %}

{% step %}

### Edit `routes/api-client.php`

Find:

```php
Route::post('/power', [Client\Servers\PowerController::class, 'index']);
```

Add **below** it:

```php
Route::group(['prefix' => '/subdomainmanager'], function () {
    Route::get('/', [Client\Servers\SubdomainManager\SubdomainManagerController::class, 'index']);
    Route::post('/', [Client\Servers\SubdomainManager\SubdomainManagerController::class, 'store'])->middleware('throttle:5,1');
    Route::patch('/{subdomain}', [Client\Servers\SubdomainManager\SubdomainManagerController::class, 'update'])->middleware('throttle:5,1');
    Route::delete('/{subdomain}', [Client\Servers\SubdomainManager\SubdomainManagerController::class, 'destroy'])->middleware('throttle:5,1');
});
```

{% endstep %}

{% step %}

### Edit `routes/api-application.php`

Add at the **bottom** of the file:

```php
Route::group(['prefix' => '/subdomainmanager'], function () {
    Route::get('/domains', [Application\SubdomainManager\SubdomainManagerApplicationController::class, 'listDomains']);
    Route::get('/servers/{server_id}/subdomains/check', [Application\SubdomainManager\SubdomainManagerApplicationController::class, 'checkAvailability']);
    Route::post('/servers/{server_id}/subdomains', [Application\SubdomainManager\SubdomainManagerApplicationController::class, 'store']);
    Route::delete('/servers/{server_id}/subdomains/{record_id}', [Application\SubdomainManager\SubdomainManagerApplicationController::class, 'destroy']);
});
```

{% hint style="info" %}
**Why `{server_id}` instead of `{server}`?** Pterodactyl's `RouteServiceProvider` globally binds `{server}` to a model lookup by UUID. Passing an integer ID as `{server}` causes it to return 404. Using `{server_id}` bypasses this and lets the controller resolve the server manually via `Server::findOrFail($server_id)`.
{% endhint %}
{% endstep %}

{% step %}

### Edit `routes/admin.php`

Add at the **bottom**:

```php
Route::group(['prefix' => 'subdomainmanager'], function () {
    Route::get('/', [Admin\SubdomainManager\SubdomainManagerAdminController::class, 'index'])->name('admin.subdomainmanager');
    Route::post('/config', [Admin\SubdomainManager\SubdomainManagerAdminController::class, 'saveConfig'])->name('admin.subdomainmanager.config');
    Route::post('/test', [Admin\SubdomainManager\SubdomainManagerAdminController::class, 'testCloudflare'])->name('admin.subdomainmanager.test');
    Route::post('/domains', [Admin\SubdomainManager\SubdomainManagerAdminController::class, 'storeDomain'])->name('admin.subdomainmanager.domains.store');
    Route::put('/domains/{id}', [Admin\SubdomainManager\SubdomainManagerAdminController::class, 'updateDomain'])->name('admin.subdomainmanager.domains.update');
    Route::delete('/domains/{id}', [Admin\SubdomainManager\SubdomainManagerAdminController::class, 'destroyDomain'])->name('admin.subdomainmanager.domains.destroy');
});
```

{% endstep %}

{% step %}

### Edit `resources/scripts/routers/routes.ts`

Find:

```ts
import ServerActivityLogContainer from "@/components/server/ServerActivityLogContainer";
```

Add **below** it:

```ts
import SubdomainManagerContainer from "@/components/server/subdomainManager/SubdomainManagerContainer";
```

Find the Files route object:

```ts
{ path: '/files', permission: 'file.*', name: 'Files', component: FileManagerContainer },
```

Add **below** it:

```ts
{ path: '/subdomains', permission: 'subdomains.read', name: 'Subdomains', component: SubdomainManagerContainer },
```

{% endstep %}

{% step %}

### Edit `resources/views/layouts/admin.blade.php`

Find the Nests sidebar item:

```php
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.nests') ?: 'active' }}">
    <a href="{{ route('admin.nests') }}"><i class="fa fa-th-large"></i> <span>Nests</span></a>
</li>
```

Add **below** it:

```php
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.subdomainmanager') ?: 'active' }}">
    <a href="{{ route('admin.subdomainmanager') }}"><i class="fa fa-globe"></i> <span>Subdomain Manager</span></a>
</li>
```

{% endstep %}

{% step %}

### Edit `app/Models/Permission.php`

Find the `docker-image` permission block. After it (down around 3 curly braces), add:

```php
'subdomains' => [
    'description' => 'Permissions that control a user\'s ability to manage subdomains for this server.',
    'keys' => [
        'read' => 'Allows a user to view the created subdomains.',
        'create' => 'Allows a user to create a new server subdomain.',
        'update' => 'Allows a user to update the server subdomains.',
        'delete' => 'Allows a user to delete an existing server subdomain.',
    ],
],
```

{% endstep %}

{% step %}

### Edit `app/Services/Servers/ServerDeletionService.php`

Find `public function handle(Server $server): void` and add this as the **very first line inside the method**:

```php
\Pterodactyl\Jobs\Server\DeleteSubdomainsJob::dispatch(\Illuminate\Support\Facades\DB::table('server_subdomains')->where('server_id', $server->id)->pluck('id')->toArray());
```

{% endstep %}

{% step %}

### Run post-install commands

```bash
cd /var/www/pterodactyl
php artisan migrate --force
php artisan route:clear
php artisan cache:clear
yarn build:production
```

{% endstep %}
{% endstepper %}

***

## Cloudflare Setup

1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → your domain → **Overview** → copy the **Zone ID** from the right sidebar.
2. Go to [API Tokens](https://dash.cloudflare.com/profile/api-tokens) → **Create Token** → use the **Edit zone DNS** template.
3. Scope the token to the specific zone you're managing.
4. Paste the token in Admin → **Subdomain Manager** → Configuration.

***

## Admin Usage

1. Go to **Admin → Subdomain Manager** in the panel sidebar.
2. Enter your Cloudflare API token, maximum subdomains limit, and optionally comma-separate blacklisted words (like `mail`, `admin`, `api` etc).
3. Click **Save Settings**, then **Test Connection** to verify your token.
4. Go to **Add New Domain**, enter a domain (e.g. `mc.example.com`), the Cloudflare Zone ID, select which Eggs can use this domain, and save.
5. Users assigned those egg types will see a **Subdomains** tab in their server panel!

***

## Uninstallation

{% stepper %}
{% step %}

### Reverse route additions

Reverse all route additions in `api-client.php`, `api-application.php`, `admin.php`.
{% endstep %}

{% step %}

### Remove the sidebar link

Remove the sidebar link from `admin.blade.php`.
{% endstep %}

{% step %}

### Remove permissions and deletion hook

Remove the `subdomains` permission block from `Permission.php`.

Remove the `DeleteSubdomainsJob::dispatch` line from `ServerDeletionService.php`.
{% endstep %}

{% step %}

### Remove frontend routing

Remove the import and route entry from `routes.ts`, then rebuild frontend.
{% endstep %}

{% step %}

### Delete copied files and clean up database

Delete all copied files from `app/`, `database/`, `resources/`.

Rollback DB migration (if necessary), or drop tables: `subdomains`, `server_subdomains`, `subdomain_manager_configs`.
{% endstep %}

{% step %}

### Clear caches

Run:

```bash
php artisan route:clear
php artisan cache:clear
```

{% endstep %}
{% endstepper %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://juber.gitbook.io/bbb-guides/setup.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
