# Subdomain-Fundament (Registry + Routing + Session) — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Macht alle 10 Subdomains (`webtools.kotsch.tech`, `ios.kotsch.tech`, …) live, indem sie über eine zentrale Registry auf die bestehenden Pfad-Inhalte derselben Laravel-App geroutet werden — bei vollem Parallelbetrieb (alte Pfade bleiben gültig) und subdomain-übergreifender Session.

**Architecture:** Eine `config/subdomains.php`-Registry ist die einzige Wahrheitsquelle. Pro Subdomain registriert `routes/web.php` eine `Route::domain()`-Catch-all-Route, die einen `SubdomainDispatcher` aufruft. Der Dispatcher stellt dem Pfad das Basis-Prefix voran (`/farb-konverter` → `/webtools/farb-konverter`), tauscht den Host auf den Apex und re-dispatcht per Sub-Request (bewährtes Muster aus `LocalizedPageController`). Findet die prefixed Route nichts, fällt er auf den Original-Pfad zurück (globale Seiten wie `/impressum` funktionieren auch auf Subdomains).

**Tech Stack:** Laravel 12, PHP 8.2, PHPUnit, SQLite (Sessions in DB), Cloudflare Tunnel (remote-managed), Apache/XAMPP (Port 85).

**Dieser Plan ist Teil 1 von 4.** Folgepläne: (2) SEO/Canonical/Sitemaps/ads.txt, (3) Design-System/Tokens, (4) Hub-Startseite/Navigation.

---

## Datei-Struktur

| Datei | Verantwortung | Aktion |
|---|---|---|
| `config/subdomains.php` | Registry: alle Subdomains + Hub mit Host/Base/Design-Feldern | **Neu** |
| `app/Support/SubdomainRegistry.php` | Lookups über die Registry (forHost, subdomains, find) | **Neu** |
| `app/Support/SubdomainDispatcher.php` | Pfad-Rewrite + Host-Swap + Sub-Request-Dispatch mit Fallback | **Neu** |
| `routes/web.php` | Domain-Catch-all-Routen aus der Registry (am Dateianfang) | **Ändern** |
| `bootstrap/app.php` | `trustHosts` für `kotsch.tech`+Subdomains | **Ändern** |
| `.env` / `.env.example` | `SESSION_DOMAIN=.kotsch.tech` | **Ändern** |
| `tests/Unit/SubdomainRegistryTest.php` | Unit-Tests Registry | **Neu** |
| `tests/Feature/SubdomainRoutingTest.php` | Feature-Tests Routing + Fallback + Session-Cookie | **Neu** |

---

### Task 1: Git-Sicherheitsnetz

**Files:**
- Create: `.gitignore` (falls nicht vorhanden)

Bei einer Migration auf einer Live-Site ist Versionierung Pflicht (Rollback-Fähigkeit). Das Projekt ist aktuell kein Git-Repo.

- [ ] **Step 1: Repo initialisieren**

Run:
```powershell
git init
```
Expected: `Initialized empty Git repository in C:/xampp/htdocs/kotsch.tech/kotsch-tech/.git/`

- [ ] **Step 2: `.gitignore` sicherstellen** (Laravel-Standard; nur anlegen, falls fehlt)

Falls `.gitignore` fehlt, anlegen mit:
```
/vendor
/node_modules
/public/build
/public/hot
/storage/*.key
/.env
/.env.backup
/auth.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
```

- [ ] **Step 3: Basis-Commit (Ist-Zustand sichern)**

Run:
```powershell
git add -A
git commit -m "chore: baseline before subdomain architecture"
```
Expected: Commit erstellt; `git log --oneline` zeigt den Baseline-Commit.

---

### Task 2: Die Registry `config/subdomains.php`

**Files:**
- Create: `config/subdomains.php`

- [ ] **Step 1: Registry anlegen**

```php
<?php

// Zentrale Subdomain-Registry — einzige Wahrheitsquelle für Routing, Canonical,
// Navigation, Hub, Sitemaps und Design-Tokens. Neue Subdomain = 1 Eintrag.
// Felder:
//   host         FQDN der Subdomain
//   base         Pfad-Prefix der bestehenden Inhalte (Parallelbetrieb)
//   label        Anzeigename
//   tagline      Kurzbeschreibung (Hub/Meta)
//   accent       Tier-1-Design-Token (Akzentfarbe)
//   accent_2     optionale zweite Akzentfarbe
//   font_display Display-Schrift (Akzent-Layer)
//   motif        3D-Signatur-Schlüssel
//   nav_group    Gruppierung in der globalen Navigation
//   index        true = erscheint auf der Hub-Startseite
//   noindex      true = von Suchmaschinen ausschließen

return [

    // Apex/Hub — KEINE Domain-Catch-all-Route (dient direkt den Pfad-Routen).
    'hub' => [
        'host' => 'kotsch.tech',
        'base' => '/',
        'label' => 'Kotsch.Tech',
        'tagline' => 'Apps, Tools & IT von Arthur Kotsch',
        'accent' => '#FF5D2E',
        'accent_2' => null,
        'font_display' => 'Clash Display',
        'motif' => 'constellation',
        'nav_group' => null,
        'index' => false,
        'noindex' => false,
        'hub' => true,
    ],

    'ios' => [
        'host' => 'ios.kotsch.tech', 'base' => '/apple',
        'label' => 'iOS Apps', 'tagline' => 'iOS- & Apple-Apps',
        'accent' => '#0A84FF', 'accent_2' => null,
        'font_display' => 'Cabinet Grotesque', 'motif' => 'glass-tiles',
        'nav_group' => 'apps', 'index' => true, 'noindex' => false,
    ],
    'windows' => [
        'host' => 'windows.kotsch.tech', 'base' => '/windows',
        'label' => 'Windows', 'tagline' => 'Windows-Programme',
        'accent' => '#1B6CF2', 'accent_2' => null,
        'font_display' => 'Space Grotesk', 'motif' => 'window-panes',
        'nav_group' => 'apps', 'index' => true, 'noindex' => false,
    ],
    'android' => [
        'host' => 'android.kotsch.tech', 'base' => '/android',
        'label' => 'Android', 'tagline' => 'Android-Apps',
        'accent' => '#19C37D', 'accent_2' => null,
        'font_display' => 'Clash Display', 'motif' => 'robot-blocks',
        'nav_group' => 'apps', 'index' => true, 'noindex' => false,
    ],
    'webtools' => [
        'host' => 'webtools.kotsch.tech', 'base' => '/webtools',
        'label' => 'Web-Tools', 'tagline' => '220+ kostenlose Online-Tools',
        'accent' => '#0FB5C9', 'accent_2' => null,
        'font_display' => 'JetBrains Mono', 'motif' => 'gears',
        'nav_group' => 'tools', 'index' => true, 'noindex' => false,
    ],
    'shop' => [
        'host' => 'shop.kotsch.tech', 'base' => '/shop',
        'label' => 'Shop', 'tagline' => 'Hardware & digitale Produkte',
        'accent' => '#0F8A4F', 'accent_2' => '#C8962B',
        'font_display' => 'Fraunces', 'motif' => 'product-podium',
        'nav_group' => 'commerce', 'index' => true, 'noindex' => false,
    ],
    'hosting' => [
        'host' => 'hosting.kotsch.tech', 'base' => '/hosting',
        'label' => 'Hosting', 'tagline' => 'Webhosting & vServer',
        'accent' => '#14B8A6', 'accent_2' => null,
        'font_display' => 'Space Grotesk', 'motif' => 'server-rack',
        'nav_group' => 'services', 'index' => true, 'noindex' => false,
    ],
    'news' => [
        'host' => 'news.kotsch.tech', 'base' => '/news',
        'label' => 'News', 'tagline' => 'Artikel & Ratgeber',
        'accent' => '#F4B41A', 'accent_2' => null,
        'font_display' => 'Fraunces', 'motif' => 'folding-paper',
        'nav_group' => 'content', 'index' => true, 'noindex' => false,
    ],
    'it-hilfe' => [
        'host' => 'it-hilfe.kotsch.tech', 'base' => '/it-hilfe',
        'label' => 'IT-Hilfe', 'tagline' => 'IT-Support & Hilfe',
        'accent' => '#FF7A45', 'accent_2' => null,
        'font_display' => 'Cabinet Grotesque', 'motif' => 'helper-shield',
        'nav_group' => 'services', 'index' => true, 'noindex' => false,
    ],
    'webdesign' => [
        'host' => 'webdesign.kotsch.tech', 'base' => '/webdesign',
        'label' => 'Webdesign', 'tagline' => 'Webdesign-Studio & Vorlagen',
        'accent' => '#E6259A', 'accent_2' => '#0FB5C9',
        'font_display' => 'Bricolage Grotesque', 'motif' => 'swatches',
        'nav_group' => 'services', 'index' => true, 'noindex' => false,
    ],
    'admin' => [
        'host' => 'admin.kotsch.tech', 'base' => '/admin',
        'label' => 'Admin', 'tagline' => 'Verwaltung',
        'accent' => '#64748B', 'accent_2' => null,
        'font_display' => 'IBM Plex Sans', 'motif' => null,
        'nav_group' => null, 'index' => false, 'noindex' => true,
    ],
];
```

- [ ] **Step 2: Config-Cache leeren & Syntax prüfen**

Run:
```powershell
php artisan config:clear; php -l config/subdomains.php
```
Expected: `No syntax errors detected in config/subdomains.php`

- [ ] **Step 3: Commit**

```powershell
git add config/subdomains.php
git commit -m "feat: add subdomain registry config"
```

---

### Task 3: `SubdomainRegistry`-Helfer (TDD)

**Files:**
- Create: `app/Support/SubdomainRegistry.php`
- Test: `tests/Unit/SubdomainRegistryTest.php`

- [ ] **Step 1: Failing Test schreiben**

```php
<?php

namespace Tests\Unit;

use App\Support\SubdomainRegistry;
use Tests\TestCase;

class SubdomainRegistryTest extends TestCase
{
    private function registry(): SubdomainRegistry
    {
        return new SubdomainRegistry();
    }

    public function test_for_host_finds_subdomain_entry(): void
    {
        $entry = $this->registry()->forHost('webtools.kotsch.tech');
        $this->assertNotNull($entry);
        $this->assertSame('/webtools', $entry['base']);
        $this->assertSame('webtools', $entry['key']);
    }

    public function test_for_host_returns_null_for_unknown_host(): void
    {
        $this->assertNull($this->registry()->forHost('nope.example.com'));
    }

    public function test_hub_is_excluded_from_routable_subdomains(): void
    {
        $keys = array_keys($this->registry()->subdomains());
        $this->assertContains('webtools', $keys);
        $this->assertNotContains('hub', $keys);
    }

    public function test_subdomains_have_required_fields(): void
    {
        foreach ($this->registry()->subdomains() as $key => $entry) {
            $this->assertArrayHasKey('host', $entry, "$key fehlt host");
            $this->assertArrayHasKey('base', $entry, "$key fehlt base");
            $this->assertStringStartsWith('/', $entry['base']);
        }
    }
}
```

- [ ] **Step 2: Test laufen lassen — muss fehlschlagen**

Run:
```powershell
php artisan test --filter=SubdomainRegistryTest
```
Expected: FAIL — `Class "App\Support\SubdomainRegistry" not found`.

- [ ] **Step 3: Minimale Implementierung**

```php
<?php

namespace App\Support;

class SubdomainRegistry
{
    /** @return array<string,array> Alle Einträge inkl. Hub, jeweils mit 'key'. */
    public function all(): array
    {
        $out = [];
        foreach (config('subdomains', []) as $key => $entry) {
            $entry['key'] = $key;
            $out[$key] = $entry;
        }
        return $out;
    }

    /** @return array<string,array> Routebare Subdomains (ohne Hub). */
    public function subdomains(): array
    {
        return array_filter($this->all(), fn ($e) => empty($e['hub']));
    }

    /** Eintrag per Schlüssel. */
    public function find(string $key): ?array
    {
        return $this->all()[$key] ?? null;
    }

    /** Eintrag per Host (case-insensitive). */
    public function forHost(string $host): ?array
    {
        $host = strtolower($host);
        foreach ($this->all() as $entry) {
            if (strtolower($entry['host'] ?? '') === $host) {
                return $entry;
            }
        }
        return null;
    }
}
```

- [ ] **Step 4: Test laufen lassen — muss bestehen**

Run:
```powershell
php artisan test --filter=SubdomainRegistryTest
```
Expected: PASS (4 Tests grün).

- [ ] **Step 5: Commit**

```powershell
git add app/Support/SubdomainRegistry.php tests/Unit/SubdomainRegistryTest.php
git commit -m "feat: add SubdomainRegistry helper with lookups"
```

---

### Task 4: `SubdomainDispatcher` + Domain-Routen (TDD)

**Files:**
- Create: `app/Support/SubdomainDispatcher.php`
- Modify: `routes/web.php` (Block ganz am Dateianfang, nach den `use`-Imports)
- Test: `tests/Feature/SubdomainRoutingTest.php`

- [ ] **Step 1: Failing Feature-Test schreiben**

```php
<?php

namespace Tests\Feature;

use Tests\TestCase;

class SubdomainRoutingTest extends TestCase
{
    public function test_subdomain_serves_prefixed_content(): void
    {
        // Bare /farb-konverter existiert nur unter /webtools — auf der Subdomain muss es 200 liefern.
        $this->get('http://webtools.kotsch.tech/farb-konverter')
            ->assertOk();
    }

    public function test_subdomain_root_serves_section_index(): void
    {
        $this->get('http://webtools.kotsch.tech/')->assertOk();
    }

    public function test_global_page_falls_back_on_subdomain(): void
    {
        // /impressum lebt auf dem Apex; /webtools/impressum existiert nicht → Fallback greift.
        $this->get('http://webtools.kotsch.tech/impressum')->assertOk();
    }

    public function test_apex_paths_keep_working_parallel(): void
    {
        $this->get('http://kotsch.tech/webtools/farb-konverter')->assertOk();
    }

    public function test_unknown_path_on_subdomain_is_404(): void
    {
        $this->get('http://webtools.kotsch.tech/diese-seite-gibt-es-nicht-xyz')
            ->assertNotFound();
    }
}
```

- [ ] **Step 2: Test laufen lassen — muss fehlschlagen**

Run:
```powershell
php artisan test --filter=SubdomainRoutingTest
```
Expected: FAIL — `webtools.kotsch.tech/farb-konverter` liefert 404 (noch keine Domain-Route).

- [ ] **Step 3: Dispatcher implementieren**

```php
<?php

namespace App\Support;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class SubdomainDispatcher
{
    private string $apexHost;

    public function __construct()
    {
        $this->apexHost = parse_url((string) config('app.url'), PHP_URL_HOST) ?: 'kotsch.tech';
    }

    /**
     * Dient einen Subdomain-Request, indem der Basis-Pfad vorangestellt und
     * auf dem Apex-Host re-dispatcht wird. Fällt auf den Original-Pfad zurück.
     */
    public function dispatch(Request $request, string $base, ?string $path): Response
    {
        $path = '/' . ltrim((string) $path, '/');

        // Optionales /de oder /en muss vorne bleiben.
        $locale = '';
        if (preg_match('#^/(de|en)(/.*|)$#', $path, $m)) {
            $locale = '/' . $m[1];
            $path = ($m[2] === '') ? '/' : $m[2];
        }

        $base = '/' . trim($base, '/');

        $prefixed = ($locale . $base . ($path === '/' ? '' : $path)) ?: '/';

        // Subdomain-Inhalt zuerst versuchen.
        $response = $this->tryDispatch($request, $prefixed);
        if ($response !== null) {
            return $response;
        }

        // Globale Apex-Seiten (z.B. /impressum, /csrf-token) leben NICHT unter dem
        // Basis-Prefix. Nur bei idempotentem GET auf den Original-Pfad zurückfallen.
        if ($request->isMethod('GET')) {
            $original = ($locale . ($path === '/' ? '' : $path)) ?: '/';
            if ($original !== $prefixed) {
                $fallback = $this->tryDispatch($request, $original);
                if ($fallback !== null) {
                    return $fallback;
                }
            }
        }

        abort(404, 'Seite nicht gefunden.');
    }

    // Dispatcht eine URI auf dem Apex-Host. Gibt null zurück, wenn nichts gefunden
    // wurde — Route-Miss ODER Controller-seitiges abort(404) — damit der Fallback
    // auch greift, wenn die prefixed Route zwar matcht, aber 404 liefert.
    private function tryDispatch(Request $request, string $uri): ?Response
    {
        $sub = $this->makeApexRequest($request, $uri);

        try {
            $response = Route::dispatch($sub);
        } catch (NotFoundHttpException|MethodNotAllowedHttpException) {
            return null;
        }

        return $response->getStatusCode() === 404 ? null : $response;
    }

    private function makeApexRequest(Request $request, string $uri): Request
    {
        $server = $request->server->all();
        $server['HTTP_HOST'] = $this->apexHost;
        $server['SERVER_NAME'] = $this->apexHost;
        $server['REQUEST_URI'] = $uri;
        $server['PATH_INFO'] = parse_url($uri, PHP_URL_PATH) ?: '/';

        $sub = Request::create(
            $uri,
            $request->method(),
            $request->all(),
            $request->cookies->all(),
            $request->files->all(),
            $server,
            $request->getContent(),
        );

        $sub->headers->replace($request->headers->all());
        $sub->headers->set('Host', $this->apexHost);

        if ($request->hasSession()) {
            $sub->setLaravelSession($request->session());
        }

        return $sub;
    }
}
```

- [ ] **Step 4: Domain-Routen am Anfang von `routes/web.php` registrieren**

Direkt **nach den `use`-Imports** und **vor** `Route::view('/', 'welcome')` einfügen:

```php
// ── Subdomain-Routing (Registry-getrieben) ──────────────────────────
// Pro Subdomain eine Catch-all-Route, die den Basis-Pfad voranstellt und
// auf dem Apex re-dispatcht. MUSS vor den Pfad-Routen stehen, damit sie auf
// Subdomain-Hosts zuerst greift. Hub (apex) bekommt KEINE solche Route.
foreach ((new \App\Support\SubdomainRegistry())->subdomains() as $sub) {
    Route::domain($sub['host'])->group(function () use ($sub) {
        Route::any('{path?}', function (\Illuminate\Http\Request $request, ?string $path = null) use ($sub) {
            return app(\App\Support\SubdomainDispatcher::class)->dispatch($request, $sub['base'], $path);
        })->where('path', '.*');
    });
}
```

- [ ] **Step 5: Test laufen lassen — muss bestehen**

Run:
```powershell
php artisan config:clear; php artisan route:clear; php artisan test --filter=SubdomainRoutingTest
```
Expected: PASS (5 Tests grün). Falls die verschachtelte Locale-Logik oder CSRF bei einzelnen Tests hakt: das ist der erwartete TDD-Punkt — Dispatcher anpassen, bis grün (dies ist das heikelste Bauteil des Plans).

- [ ] **Step 6: Commit**

```powershell
git add app/Support/SubdomainDispatcher.php routes/web.php tests/Feature/SubdomainRoutingTest.php
git commit -m "feat: route subdomains to prefixed content via SubdomainDispatcher"
```

---

### Task 5: Session über Subdomains + TrustHosts (TDD)

**Files:**
- Modify: `bootstrap/app.php` (im `withMiddleware`-Closure)
- Modify: `.env`, `.env.example`
- Test: `tests/Feature/SubdomainRoutingTest.php` (Test ergänzen)

- [ ] **Step 1: Failing Test für Session-Cookie-Domain ergänzen**

In `tests/Feature/SubdomainRoutingTest.php` ergänzen:

```php
    public function test_session_cookie_is_scoped_to_root_domain(): void
    {
        config()->set('session.domain', '.kotsch.tech');

        $response = $this->get('http://webtools.kotsch.tech/');

        $cookie = collect($response->headers->getCookies())
            ->first(fn ($c) => str_contains($c->getName(), 'session'));

        $this->assertNotNull($cookie, 'Session-Cookie fehlt');
        $this->assertSame('.kotsch.tech', $cookie->getDomain());
    }
```

- [ ] **Step 2: Test laufen lassen — muss fehlschlagen**

Run:
```powershell
php artisan test --filter=test_session_cookie_is_scoped_to_root_domain
```
Expected: FAIL — Cookie-Domain ist `null`, nicht `.kotsch.tech` (Test setzt config, aber wir verankern es als Nächstes dauerhaft in `.env`).

> Hinweis: Der Test setzt `session.domain` selbst, um das Verhalten zu prüfen. Falls er schon grün ist, weil das Cookie korrekt gesetzt wird, fahre mit Step 3 fort, um den Wert dauerhaft in `.env` zu verankern.

- [ ] **Step 3: `SESSION_DOMAIN` in `.env` und `.env.example` setzen**

In `.env` die Zeile `SESSION_DOMAIN=null` ersetzen durch:
```
SESSION_DOMAIN=.kotsch.tech
```
In `.env.example` ebenso `SESSION_DOMAIN=null` → `SESSION_DOMAIN=.kotsch.tech`.

- [ ] **Step 4: `trustHosts` in `bootstrap/app.php` ergänzen**

Im `->withMiddleware(function (Middleware $middleware): void { … })`-Closure, direkt nach dem `trustProxies(...)`-Aufruf, ergänzen:

```php
        // Wir akzeptieren jetzt viele Hosts (Wildcard-Tunnel) → Host-Header gegen
        // Spoofing absichern: nur kotsch.tech und seine Subdomains sind gültig.
        $middleware->trustHosts(at: ['kotsch.tech'], subdomains: true);
```

- [ ] **Step 5: Tests + Config-Cache**

Run:
```powershell
php artisan config:clear; php artisan test --filter=SubdomainRoutingTest
```
Expected: PASS (alle 6 Tests grün).

- [ ] **Step 6: Commit**

```powershell
git add bootstrap/app.php .env.example tests/Feature/SubdomainRoutingTest.php
git commit -m "feat: scope session to .kotsch.tech and trust subdomains"
```
> `.env` wird durch `.gitignore` nicht committet — das ist korrekt. Den Wert manuell auf dem Server setzen (Task 6, Step 3).

---

### Task 6: Go-Live — Live-Config + Cloudflare Tunnel

> ⚠️ **Live-wirksam:** Diese Schritte ändern das Live-Verhalten. Code-Edits (A, B) mache ich, die Cloudflare-Schritte (1–4) macht Arthur im **Zero-Trust-Dashboard** (Tunnel ist remote-managed via Token). Nach jedem Block sofort verifizieren.

- [ ] **Step A: `trustHosts` aktivieren** (`bootstrap/app.php`, im `withMiddleware`-Closure nach `trustProxies(...)`):

```php
        // Wildcard akzeptiert viele Hosts → Host-Header gegen Spoofing absichern.
        // localhost/127.0.0.1 bleiben erlaubt für direkten Origin-/Health-Zugriff.
        $middleware->trustHosts(at: ['kotsch.tech', 'localhost', '127.0.0.1'], subdomains: true);
```

Sofort verifizieren (legitime Hosts 200, Fremd-Host 400):
```powershell
curl.exe -s -o NUL -w "%{http_code}`n" -H "Host: kotsch.tech" http://localhost:85/up
curl.exe -s -o NUL -w "%{http_code}`n" -H "Host: localhost" http://localhost:85/up
curl.exe -s -o NUL -w "%{http_code}`n" -H "Host: evil.example.com" http://localhost:85/up
```
Erwartet: `200`, `200`, `400`. Falls legitimer Host 400 liefert → Host zur Liste hinzufügen.

- [ ] **Step B: Live-`.env` setzen**

```
SESSION_DOMAIN=.kotsch.tech
APP_URL=https://kotsch.tech
```
Dann `php artisan config:clear`. (Admin wird ggf. einmalig ausgeloggt — unkritisch.)

- [ ] **Step 1: Wildcard-DNS prüfen/anlegen**

Cloudflare → DNS → Record `*.kotsch.tech` (CNAME, proxied/orange) auf den Tunnel zeigend. Apex `kotsch.tech` und `www` ebenfalls auf den Tunnel.

- [ ] **Step 2: Tunnel-Public-Hostnames konfigurieren**

Cloudflare → Zero Trust → Networks → Tunnels → (dein Tunnel) → Public Hostnames:
- **Wildcard:** `*.kotsch.tech` → Service `HTTP://localhost:85`
- **Carve-outs (spezifischer, daher Vorrang) beibehalten/prüfen:** `files.kotsch.tech`→:8081, `invoice.kotsch.tech`→:8083, `aitools.kotsch.tech`→:8084, `mail.kotsch.tech`→:443
- **Shop umbiegen:** `shop.kotsch.tech` → `HTTP://localhost:85` (Laravel-Shop)
- **Alt-Shop retten:** `ai-shop.kotsch.tech` → `HTTP://localhost:8082` (bisheriger AI Creator Shop)

- [ ] **Step 3: `.env` auf dem Server setzen**

In der echten `.env` (nicht `.env.example`):
```
SESSION_DOMAIN=.kotsch.tech
APP_URL=https://kotsch.tech
```
Dann:
```powershell
php artisan config:clear
```

- [ ] **Step 4: www → Apex Redirect (Cloudflare Redirect Rule)**

Cloudflare → Rules → Redirect Rules: `www.kotsch.tech/*` → `https://kotsch.tech/$1` (301).

- [ ] **Step 5: Live-Verifikation**

Im Browser prüfen:
- `https://webtools.kotsch.tech/` → Webtools-Übersicht lädt
- `https://webtools.kotsch.tech/farb-konverter` → Tool lädt
- `https://kotsch.tech/webtools/farb-konverter` → funktioniert weiterhin (Parallelbetrieb)
- `https://shop.kotsch.tech/` → Laravel-Shop (nicht der alte AI-Shop)
- `https://ai-shop.kotsch.tech/` → alter AI Creator Shop erreichbar
- `https://www.kotsch.tech/` → 301 auf `https://kotsch.tech/`

- [ ] **Step 6: Verifikation dokumentieren**

Ergebnisse (welche Hosts grün/rot) kurz festhalten; bei rot zurück zum jeweiligen Step.

---

## Self-Review (Plan ↔ Spec)

**Spec-Abdeckung (Abschnitte des Konzepts, die dieser Plan abdeckt):**
- §5.1 Registry → Task 2 ✅
- §5.2 ResolveSubdomain/Dispatch → Task 4 ✅ (als Domain-Catch-all + Dispatcher umgesetzt; robuster als reine Middleware, da Session verfügbar)
- §5.3 Session + TrustHosts → Task 5 ✅
- §6 Infrastruktur (Wildcard-Tunnel, Carve-outs, shop→ai-shop, www→301) → Task 6 ✅
- §11 Testing → Unit- + Feature-Tests in Task 3/4/5 ✅
- §12 Rollback → Git (Task 1) + Middleware/Route entfernbar ✅
- **Bewusst NICHT in diesem Plan** (Folgepläne): §7 Design, §8 SEO/Canonical/Sitemaps/ads.txt, §9 Hub/Navigation. → Plan 2-4.

**Placeholder-Scan:** Keine TBD/TODO; jeder Code-Step enthält vollständigen Code. ✅

**Typ-Konsistenz:** `SubdomainRegistry::subdomains()`/`forHost()`/`find()` konsistent zwischen Task 3, 4, 5. `SubdomainDispatcher::dispatch(Request, string, ?string)` konsistent zwischen Definition (Task 4 Step 3) und Aufruf (Task 4 Step 4). ✅

**Offener Risiko-Punkt (in Task 4 Step 5 markiert):** Die Verschachtelung Locale × Sub-Request × CSRF ist das heikelste Stück; die Feature-Tests sind so gewählt, dass sie genau das absichern.
