# Supabase BYOD — 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:** Replace the platform-owned Supabase backend (global Cloud + per-user self-hosted Docker) with BYOD: a per-user connection library that projects attach at creation, and remove every trace of the old model (code, Go builder, autosetup, docs, i18n).

**Architecture:** New `supabase_connections` table (per-user) + `projects.supabase_connection_id`. `SupabaseService::resolveForProject` sources only from the project's connection. Schema/`defineTable`/capability code is reused, re-sourced from the connection. The provisioner abstraction, admin Database tab, builder Docker stack, autosetup Docker, and all self-hosted/mode docs+strings are deleted. `enable_database` plan gate kept.

**Tech Stack:** Laravel 13 / Inertia+React 19 / Go builder / bash (autosetup) / JSON i18n.

Reference spec: `specs/2026-06-04-supabase-byod-design.md`. Run after each phase: `php -d memory_limit=512M ./vendor/bin/phpunit --filter Supabase` and `php artisan about`.

---

## Phase A — Data model

### Task A1: Migrations

**Files:**
- Create: `database/migrations/2026_06_04_000001_create_supabase_connections_table.php`
- Create: `database/migrations/2026_06_04_000002_add_supabase_connection_id_to_projects.php`
- Create: `database/migrations/2026_06_04_000003_drop_supabase_lifecycle_from_users.php`

- [ ] **Step 1: Create `supabase_connections` table migration**

```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('supabase_connections', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('label');
            $table->string('url');
            $table->string('publishable_key')->nullable();
            $table->text('secret_key')->nullable();        // encrypted
            $table->text('db_connection')->nullable();      // encrypted
            $table->timestamp('last_tested_at')->nullable();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('supabase_connections');
    }
};
```

- [ ] **Step 2: Create `projects.supabase_connection_id` migration**

```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::table('projects', function (Blueprint $table) {
            $table->foreignId('supabase_connection_id')->nullable()
                ->after('supabase_schema')
                ->constrained('supabase_connections')->nullOnDelete();
        });
    }

    public function down(): void
    {
        Schema::table('projects', function (Blueprint $table) {
            $table->dropConstrainedForeignId('supabase_connection_id');
        });
    }
};
```

- [ ] **Step 3: Create drop-user-lifecycle migration**

```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn([
                'supabase_config', 'supabase_provisioned_at', 'supabase_status',
                'supabase_suspended_at', 'supabase_reclaim_warned_at',
            ]);
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->text('supabase_config')->nullable();
            $table->timestamp('supabase_provisioned_at')->nullable();
            $table->string('supabase_status')->nullable();
            $table->timestamp('supabase_suspended_at')->nullable();
            $table->timestamp('supabase_reclaim_warned_at')->nullable();
        });
    }
};
```

- [ ] **Step 4: Run migrations**

Run: `php artisan migrate` (the dev DB is sqlite; for sqlite `dropColumn` of multiple columns works on Laravel 13). Expected: 3 migrations run OK.

- [ ] **Step 5: Commit**

```bash
git add database/migrations/2026_06_04_00000*
git commit -m "feat(byod): supabase_connections table + project FK; drop user lifecycle cols"
```

### Task A2: Models

**Files:**
- Create: `app/Models/SupabaseConnection.php`
- Modify: `app/Models/Project.php`, `app/Models/User.php`

- [ ] **Step 1: Create the model**

```php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class SupabaseConnection extends Model
{
    protected $fillable = ['label', 'url', 'publishable_key', 'secret_key', 'db_connection', 'last_tested_at'];

    protected $hidden = ['secret_key', 'db_connection'];

    protected function casts(): array
    {
        return [
            'secret_key' => 'encrypted',
            'db_connection' => 'encrypted',
            'last_tested_at' => 'datetime',
        ];
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function projects(): HasMany
    {
        return $this->hasMany(Project::class);
    }
}
```

- [ ] **Step 2: Add relation to `Project.php`** — add a `belongsTo` after the existing relations:

```php
    public function supabaseConnection(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(SupabaseConnection::class);
    }
```
Add `'supabase_connection_id'` to `Project::$fillable`.

- [ ] **Step 3: Clean `User.php`** — remove the 5 supabase lifecycle entries from `$fillable` (lines ~44-48) and the 4 casts (lines ~77-80: `supabase_config`, `supabase_provisioned_at`, `supabase_suspended_at`, `supabase_reclaim_warned_at`). Add a relation:

```php
    public function supabaseConnections(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(SupabaseConnection::class);
    }
```

- [ ] **Step 4: Boot check** — `php artisan about` boots clean.

- [ ] **Step 5: Commit**

```bash
git add app/Models/
git commit -m "feat(byod): SupabaseConnection model + relations; drop user supabase casts"
```

---

## Phase B — Service rewrite (per-project resolution)

### Task B1: Rewrite `SupabaseService` resolution

**Files:**
- Modify: `app/Services/SupabaseService.php`

- [ ] **Step 1: Replace `resolveForProject`** (lines 67-90) with connection-sourced resolution:

```php
    public function resolveForProject(Project $project): array
    {
        $schema = $this->getProjectSchema($project);
        $conn = $project->supabaseConnection;

        if (! $conn) {
            return ['url' => '', 'publishable_key' => '', 'secret_key' => '', 'schema' => $schema, 'db_connection' => ''];
        }

        return [
            'url' => (string) $conn->url,
            'publishable_key' => (string) $conn->publishable_key,
            'secret_key' => (string) $conn->secret_key,
            'schema' => $schema,
            'db_connection' => (string) $conn->db_connection,
        ];
    }
```

- [ ] **Step 2: Replace `getMode()` + `isConfigured()`** (lines 38-57) with a per-project check:

```php
    /** True when the project has a usable BYOD connection. */
    public function hasConnection(Project $project): bool
    {
        $conn = $project->supabaseConnection;

        return $conn !== null && $conn->url !== '' && (string) $conn->db_connection !== '';
    }
```
Delete the `MODE_CLOUD`/`MODE_SELF_HOSTED` consts (lines 24-26).

- [ ] **Step 3: Delete the cloud getters** (`cloudUrl`, `cloudPublishableKey`, `cloudSecretKey`, `cloudDbConnection`, lines 443-461).

- [ ] **Step 4: Fix `testConnection`** — ensure it takes an explicit config and no longer falls back to `cloudDbConnection()`. Change any `?: $this->cloudDbConnection()` fallback to require the passed connection string; return `{ok:false,message}` when empty.

- [ ] **Step 5: Grep for leftovers**

Run: `grep -nE "getMode|MODE_CLOUD|MODE_SELF_HOSTED|cloudUrl|cloudPublishableKey|cloudSecretKey|cloudDbConnection|isConfigured" app/Services/SupabaseService.php`
Expected: no matches (all replaced/removed).

- [ ] **Step 6: Commit**

```bash
git add app/Services/SupabaseService.php
git commit -m "feat(byod): resolveForProject sources from project connection; drop modes/cloud getters"
```

### Task B2: Capability + table-create gating

**Files:**
- Modify: `app/Services/BuilderService.php` (line ~589), `app/Http/Controllers/Api/BuilderSupabaseController.php` (line ~44)

- [ ] **Step 1: `BuilderService::buildSupabaseCapability`** — replace the `isConfigured()` check:

```php
    private function buildSupabaseCapability(Project $project, ?Plan $plan): array
    {
        $supabase = app(SupabaseService::class);

        if (! ($plan?->databaseEnabled() ?? false) || ! $supabase->hasConnection($project)) {
            return ['enabled' => false];
        }

        return array_merge(['enabled' => true], $supabase->resolveForProject($project));
    }
```

- [ ] **Step 2: `BuilderSupabaseController::defineTable`** — change the gate from `$supabase->isConfigured()` to `$supabase->hasConnection($project)` (find the line that gates on plan + configured; replace the configured check).

- [ ] **Step 3: Verify**

Run: `grep -rn "isConfigured" app/ | grep -i supabase`
Expected: no matches.

- [ ] **Step 4: Commit**

```bash
git add app/Services/BuilderService.php app/Http/Controllers/Api/BuilderSupabaseController.php
git commit -m "feat(byod): gate supabase capability + defineTable on project connection"
```

### Task B3: ProjectObserver — drop provisioner, no auto-drop

**Files:**
- Modify: `app/Observers/ProjectObserver.php`

- [ ] **Step 1: Rewrite `created()` supabase block** — replace the `try { ... ensureUserBackend ... ensureProjectSchema ... }` block with (no provisioner):

```php
        // BYOD: if the owner's plan allows databases and the project has a
        // linked connection, create its schema in the user's own DB.
        try {
            $plan = $project->user?->getCurrentPlan();
            if ($plan?->databaseEnabled() && $project->supabase_connection_id) {
                app(SupabaseService::class)->ensureProjectSchema($project);
            }
        } catch (Throwable $e) {
            Log::warning('ProjectObserver: ensureProjectSchema failed', [
                'project_id' => $project->id, 'error' => $e->getMessage(),
            ]);
        }
```

- [ ] **Step 2: Rewrite `deleting()`** — remove the force-delete `dropProjectSchema` block (never auto-drop a user-owned DB). Keep only the publishing-field cleanup. New top of method:

```php
    public function deleting(Project $project): void
    {
        // Note: we never drop the project's schema in BYOD — it lives in the
        // user's own database and is theirs to remove.
        $fields = [];
        if ($project->subdomain !== null) {
            $fields['subdomain'] = null;
            $fields['published_at'] = null;
        }
        if ($project->custom_domain !== null) {
            $fields['custom_domain'] = null;
            $fields['custom_domain_verified'] = false;
            $fields['custom_domain_ssl_status'] = null;
            $fields['custom_domain_verified_at'] = null;
        }
        if (! empty($fields)) {
            $project->forceFill($fields)->saveQuietly();
        }
    }
```

- [ ] **Step 3: Remove the now-unused import** `use App\Contracts\SupabaseProvisioner;` from the top of the file.

- [ ] **Step 4: Commit**

```bash
git add app/Observers/ProjectObserver.php
git commit -m "feat(byod): observer creates schema only when connection linked; no auto-drop"
```

---

## Phase C — Connection library (backend) + create selector

### Task C1: Model policy/ownership + controller

**Files:**
- Create: `app/Http/Controllers/UserSupabaseConnectionController.php`
- Modify: `routes/web.php`

- [ ] **Step 1: Create the controller**

```php
<?php
namespace App\Http\Controllers;

use App\Models\SupabaseConnection;
use App\Services\SupabaseService;
use Illuminate\Http\Request;

class UserSupabaseConnectionController extends Controller
{
    public function index(Request $request)
    {
        return response()->json(
            $request->user()->supabaseConnections()->latest()->get()
                ->map(fn (SupabaseConnection $c) => $this->present($c))
        );
    }

    public function store(Request $request)
    {
        $this->authorizeDatabase($request);
        $data = $this->validateConn($request, true);
        $conn = $request->user()->supabaseConnections()->create($data);

        return response()->json($this->present($conn), 201);
    }

    public function update(Request $request, SupabaseConnection $connection)
    {
        abort_unless($connection->user_id === $request->user()->id, 403);
        $data = $this->validateConn($request, false);
        foreach (['secret_key', 'db_connection'] as $secret) {
            if (empty($data[$secret])) {
                unset($data[$secret]); // keep existing
            }
        }
        $connection->update($data);

        return response()->json($this->present($connection));
    }

    public function destroy(Request $request, SupabaseConnection $connection)
    {
        abort_unless($connection->user_id === $request->user()->id, 403);
        $connection->delete();

        return response()->json(['ok' => true]);
    }

    public function test(Request $request, SupabaseConnection $connection, SupabaseService $supabase)
    {
        abort_unless($connection->user_id === $request->user()->id, 403);
        $result = $supabase->testConnection(['db_connection' => $connection->db_connection]);
        if ($result['ok'] ?? false) {
            $connection->update(['last_tested_at' => now()]);
        }

        return response()->json($result);
    }

    private function authorizeDatabase(Request $request): void
    {
        abort_unless($request->user()->getCurrentPlan()?->databaseEnabled(), 403);
    }

    private function validateConn(Request $request, bool $secretsRequired): array
    {
        return $request->validate([
            'label' => 'required|string|max:100',
            'url' => 'required|url|max:255',
            'publishable_key' => 'nullable|string|max:1000',
            'secret_key' => ($secretsRequired ? 'required' : 'nullable').'|string|max:2000',
            'db_connection' => ($secretsRequired ? 'required' : 'nullable').'|string|max:2000',
        ]);
    }

    /** Masked, browser-safe representation. */
    private function present(SupabaseConnection $c): array
    {
        return [
            'id' => $c->id,
            'label' => $c->label,
            'url' => $c->url,
            'publishable_key' => $c->publishable_key,
            'has_secret_key' => filled($c->secret_key),
            'has_db_connection' => filled($c->db_connection),
            'last_tested_at' => $c->last_tested_at,
        ];
    }
}
```

- [ ] **Step 2: Routes** — in `routes/web.php`, inside the authenticated group, add:

```php
    Route::get('/supabase-connections', [UserSupabaseConnectionController::class, 'index'])->name('supabase-connections.index');
    Route::post('/supabase-connections', [UserSupabaseConnectionController::class, 'store'])->name('supabase-connections.store');
    Route::put('/supabase-connections/{connection}', [UserSupabaseConnectionController::class, 'update'])->name('supabase-connections.update');
    Route::delete('/supabase-connections/{connection}', [UserSupabaseConnectionController::class, 'destroy'])->name('supabase-connections.destroy');
    Route::post('/supabase-connections/{connection}/test', [UserSupabaseConnectionController::class, 'test'])->name('supabase-connections.test');
```
Add `use App\Http\Controllers\UserSupabaseConnectionController;` at top.

- [ ] **Step 3: Verify routes** — `php artisan route:list | grep supabase-connections` shows 5 routes.

- [ ] **Step 4: Commit**

```bash
git add app/Http/Controllers/UserSupabaseConnectionController.php routes/web.php
git commit -m "feat(byod): user supabase connection library controller + routes"
```

### Task C2: Create payload wiring

**Files:**
- Modify: `app/Http/Controllers/ProjectController.php` (store, ~line 127), `app/Http/Controllers/CreateController.php`

- [ ] **Step 1: Validate + persist `supabase_connection_id`** in `ProjectController@store`:
  - Add to the `validate` array: `'supabase_connection_id' => 'nullable|integer|exists:supabase_connections,id',`
  - After validation, enforce ownership: `if (!empty($validated['supabase_connection_id'])) { abort_unless($request->user()->supabaseConnections()->whereKey($validated['supabase_connection_id'])->exists() && $request->user()->getCurrentPlan()?->databaseEnabled(), 403); }`
  - Add `'supabase_connection_id' => $validated['supabase_connection_id'] ?? null,` to the `Project::create([...])` array.

- [ ] **Step 2: Pass connections to the Create page** in `CreateController` (the method that renders Create) — add to the Inertia props: `'supabaseConnections' => $user->getCurrentPlan()?->databaseEnabled() ? $user->supabaseConnections()->get(['id','label']) : [],`

- [ ] **Step 3: Commit**

```bash
git add app/Http/Controllers/ProjectController.php app/Http/Controllers/CreateController.php
git commit -m "feat(byod): accept supabase_connection_id on create; pass connections to Create page"
```

### Task C3: Frontend — Connections page + Create selector

**Files:**
- Create: `resources/js/Pages/Profile/SupabaseConnections.tsx` (model on the existing AI-settings/profile sub-page pattern)
- Modify: `resources/js/components/Dashboard/PromptInput.tsx` (add the Database `<Select>`), `resources/js/Pages/Create.tsx` (thread `supabaseConnections` prop + `supabase_connection_id` into the `router.post('/projects', {...})` payload), and the Profile/Settings nav to link the new page (gated on the plan flag prop).

- [ ] **Step 1: Build the Connections page** — a table of the user's connections (label, url, "secret saved" badge, last tested) with Add/Edit (dialog: label, url, publishable_key, secret_key [password], db_connection [password, "leave blank to keep"]), Delete, and a Test button per row hitting `route('supabase-connections.test', id)`. Reuse the masking pattern (`has_secret_key`). Use the shadcn `Dialog`, `Input`, `Button`, `Table` already in the project. Gate the whole page behind the plan flag shared via Inertia.

- [ ] **Step 2: Create-page selector** — in `PromptInput.tsx`, after the design-system `<Select>` block, add (only when `supabaseConnections.length` is passed and plan allows):

```tsx
{supabaseConnections.length > 0 && (
  <Select value={selectedConnectionId?.toString() ?? 'none'} onValueChange={handleSelectConnection}>
    <SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
    <SelectContent>
      <SelectItem value="none"><div className="flex items-center gap-2"><Database className="h-4 w-4" /><span>{t('No database')}</span></div></SelectItem>
      {supabaseConnections.map((c) => (
        <SelectItem key={c.id} value={c.id.toString()}>
          <div className="flex items-center gap-2"><Database className="h-4 w-4" /><span>{c.label}</span></div>
        </SelectItem>
      ))}
    </SelectContent>
  </Select>
)}
```
Thread `selectedConnectionId` up to `Create.tsx`'s submit (add `supabase_connection_id: connectionId` to the `router.post('/projects', {...})` body). Import `Database` from `lucide-react`.

- [ ] **Step 3: Build + verify**

Run: `npm run build` (per the project rule: full build after touching .tsx). Expected: `tsc + vite build` clean.

- [ ] **Step 4: Commit**

```bash
git add resources/js/ && git commit -m "feat(byod): connections library page + create-page database selector"
```

---

## Phase D — Remove the old Laravel backend

### Task D1: Delete provisioners + binding

**Files:**
- Delete: `app/Services/Supabase/CloudProvisioner.php`, `app/Services/Supabase/SelfHostedProvisioner.php`, `app/Contracts/SupabaseProvisioner.php`
- Modify: `app/Providers/AppServiceProvider.php`

- [ ] **Step 1:** `git rm app/Services/Supabase/CloudProvisioner.php app/Services/Supabase/SelfHostedProvisioner.php app/Contracts/SupabaseProvisioner.php`
- [ ] **Step 2:** In `AppServiceProvider::register()` remove the `$this->app->bind(SupabaseProvisioner::class, ...)` block and the related `use` imports (`SupabaseProvisioner`, `CloudProvisioner`, `SelfHostedProvisioner`).
- [ ] **Step 3:** `grep -rn "SupabaseProvisioner\|CloudProvisioner\|SelfHostedProvisioner" app/` → no matches.
- [ ] **Step 4:** `php artisan about` boots. Commit:
```bash
git add -A app/ && git commit -m "refactor(byod): remove Supabase provisioner abstraction"
```

### Task D2: Remove admin Database settings

**Files:**
- Delete: `resources/js/Pages/Admin/Settings/DatabaseSettingsTab.tsx`
- Modify: `resources/js/Pages/Admin/Settings.tsx`, `app/Http/Controllers/Admin/SettingsController.php`, `routes/web.php`

- [ ] **Step 1:** `git rm resources/js/Pages/Admin/Settings/DatabaseSettingsTab.tsx`
- [ ] **Step 2:** In `Settings.tsx` remove the `DatabaseSettingsTab` import (~18), its entry in the settings array (~46), and its render branch (~172).
- [ ] **Step 3:** In `SettingsController.php` delete methods `getDatabaseSettings()` (426-441), `updateDatabase()` (446-472), `testSupabase()` (477-490), and any reference to them in the page-prop assembly.
- [ ] **Step 4:** In `routes/web.php` remove `admin.settings.database.update` and `admin.settings.database.test` (457-458).
- [ ] **Step 5:** `grep -rn "settings.database\|getDatabaseSettings\|updateDatabase\|testSupabase\|DatabaseSettingsTab" app/ routes/ resources/js/` → no matches.
- [ ] **Step 6:** `npm run build` clean. Commit:
```bash
git add -A && git commit -m "refactor(byod): remove admin Database settings tab + controller + routes"
```

---

## Phase E — Remove the Go builder Docker stack

**Files (webby-builder):**
- Delete: `internal/supabase/manager.go`, `internal/supabase/config.go`, `internal/api/supabase.go`
- Modify: `internal/api/router.go`, `internal/config/config.go`, and any wiring in `app.go`/main that constructs the Manager or mounts `/api/supabase/{provision,suspend,resume,teardown}`.

- [ ] **Step 1:** `cd /Users/noriellecruz/Web/webby-builder && git rm internal/supabase/manager.go internal/supabase/config.go internal/api/supabase.go` (keep `internal/client/laravel/define_table.go` and the defineTable tool).
- [ ] **Step 2:** In `internal/api/router.go` remove the route registrations for `/api/supabase/provision|suspend|resume|teardown` and the handler/Manager wiring. In `internal/config/config.go` remove the `Supabase` config struct fields + `WEBBY_SUPABASE_*` bindings. Remove any Manager construction in `app.go`/server bootstrap.
- [ ] **Step 3:** `grep -rn "supabase.Manager\|/api/supabase/provision\|WEBBY_SUPABASE\|internal/supabase" internal/ app.go` → only `define-table`/defineTable-tool references remain (those are kept).
- [ ] **Step 4:** `gofmt -l . && go vet ./... && go build ./... && go test ./...` — all clean/pass.
- [ ] **Step 5:** Commit (webby-builder):
```bash
git add -A && git commit -m "refactor(byod): remove per-user Supabase Docker stack + provisioning API"
```

---

## Phase F — autosetup.sh

**Files:**
- Modify: `autosetup.sh`

- [ ] **Step 1:** Delete functions `prompt_supabase()` (~1401-1421), `install_docker()` (~1424-1460), `setup_supabase()` (~1463-1561), and their call sites in the main flow.
- [ ] **Step 2:** Remove the `ENABLE_SUPABASE` / `SUPABASE_*` variable declarations (~56-68), the saved-state line (~216), the builder-config supabase block, the supabase SystemSetting writes, and any `pm2 restart` tied to it.
- [ ] **Step 3:** Verify: `grep -niE "supabase|docker" autosetup.sh` → no matches. `bash -n autosetup.sh` (syntax check) → OK.
- [ ] **Step 4:** Commit:
```bash
git add autosetup.sh && git commit -m "refactor(byod): remove Supabase/Docker setup from autosetup.sh"
```

---

## Phase G — Docs

**Files (webby):**
- Rewrite: `docs/src/pages/Supabase.tsx`
- Modify: `docs/src/lib/searchIndex.ts`, `docs/src/pages/VpsAutoSetup.tsx`, `docs/src/pages/Plugins.tsx`, `docs/src/pages/UserGuide.tsx`, `docs/src/pages/AdminGuide.tsx`

- [ ] **Step 1: Rewrite `Supabase.tsx`** — remove the two-mode model, "Admin → Settings → Database", the Cloud/Self-hosted capability matrix, the `#modes` and `#self-hosted` sections, autosetup/per-user-stack content, the admin-database `Screenshot`. New content (keep the page's component conventions + heading `id`s): "Bring your own database" intro; adding a connection in Profile → Database Connections (URL, publishable key, secret key, DB connection string, Test); plan gating (`enable_database`); attaching a connection on the Create page; that you own the DB (schemas live in your Supabase, not auto-deleted); `defineTable`/RLS behavior; `id`/`created_at`/`user_id` managed columns.
- [ ] **Step 2: `searchIndex.ts`** — delete the `/supabase#modes` and `/supabase#self-hosted` entries (~306-314); repoint/rewrite the remaining `/supabase` entries to BYOD terms (connection library, bring your own database). Ensure every `/supabase#anchor` matches a heading id in the rewritten page.
- [ ] **Step 3: `VpsAutoSetup.tsx`** — remove the "Optional: Self-hosted Supabase" `Callout` (~189-193).
- [ ] **Step 4: `Plugins.tsx:230`** — replace "configured in Admin → Settings → Database" with: the Supabase database capability is gated by the per-plan `enable_database` flag and each user attaches their own Supabase connection (Profile → Database Connections).
- [ ] **Step 5: `UserGuide.tsx`** (Database Management, ~253) — note that database-backed apps require attaching one of your own Supabase connections (set on the Create page); keep the `/supabase` link.
- [ ] **Step 6: `AdminGuide.tsx:138`** — reword the plan-feature row to "Database — lets users on this plan attach their own Supabase database to projects."
- [ ] **Step 7: Verify + build** — `grep -rniE "self.?hosted|supabase.?mode|admin.*settings.*database" docs/src | grep -i supabase` → none. `cd docs && npm run build` → clean.
- [ ] **Step 8: Commit:**
```bash
git add docs/ && git commit -m "docs(byod): rewrite Supabase guide for BYOD; purge self-hosted/admin-mode traces"
```

---

## Phase H — i18n

**Files:**
- Modify: `lang/{en,ar,de,fr,id,it,ja,pt,ru,zh}/admin.json` and `lang/{...}/profile.json`

- [ ] **Step 1: Identify the dead admin keys** (present in `en/admin.json`): `"Supabase Database"`, `"Configure the Supabase backend used for database-backed apps"`, `"Mode"`, `"Select mode"`, `"Supabase Cloud"`, `"Self-hosted"`, `"One Supabase project is shared by all users (accepted limitation)."`, `"Self-hosted Supabase is configured automatically by autosetup.sh on a fresh VPS. Self-hosted setup is not covered by customer support."`, `"These values are auto-filled by autosetup.sh. Edit them only as a fallback."`, `"Leave blank to keep the saved value."`, `"•••••••• (saved — leave blank to keep)"`, `"Project URL"`, `"Publishable Key"`, `"Enter publishable key"`, `"DB Connection String"`, `"Copy from Supabase Dashboard → Project Settings → API Keys and Database → Connection string"`. **Do NOT remove** `"Database (Supabase)"`, `"Reverb (Self-hosted)"`, `"Self-hosted Laravel Reverb WebSocket server"`, or generic keys still used elsewhere (`"Test Connection"`, `"Leave blank to keep existing"` — verify with a `resources/js` grep before deleting any).

- [ ] **Step 2: Remove the dead keys from all 10 locales' `admin.json`** via a script (so parity is preserved exactly). Write `/tmp/byod_i18n.py`:

```python
import json, glob, os
DEAD = [
  "Supabase Database",
  "Configure the Supabase backend used for database-backed apps",
  "Mode","Select mode","Supabase Cloud","Self-hosted",
  "One Supabase project is shared by all users (accepted limitation).",
  "Self-hosted Supabase is configured automatically by autosetup.sh on a fresh VPS. Self-hosted setup is not covered by customer support.",
  "These values are auto-filled by autosetup.sh. Edit them only as a fallback.",
  "Leave blank to keep the saved value.",
  "•••••••• (saved — leave blank to keep)",
  "Project URL","Publishable Key","Enter publishable key","DB Connection String",
  "Copy from Supabase Dashboard → Project Settings → API Keys and Database → Connection string",
]
base = "/Users/noriellecruz/Web/webby/lang"
for f in glob.glob(f"{base}/*/admin.json"):
    d = json.load(open(f, encoding="utf-8"))
    removed = [k for k in DEAD if k in d]
    for k in DEAD: d.pop(k, None)
    json.dump(d, open(f,"w",encoding="utf-8"), ensure_ascii=False, indent=4)
    open(f,"a").write("\n")
    print(os.path.basename(os.path.dirname(f)), "removed", len(removed))
```
Run: `python3 /tmp/byod_i18n.py` — every locale reports the same removed count. **Before running, confirm `"Mode"`/`"Select mode"` aren't used by a non-Supabase admin control** (grep `resources/js/Pages/Admin` for `t('Mode')`); if used, drop them from `DEAD`.

- [ ] **Step 3: Add the new BYOD keys** to `lang/en/profile.json` (English values), then translate into the other 9 locales. Keys: `"Database Connections"`, `"Add Connection"`, `"Connection name"`, `"Supabase URL"`, `"Publishable Key"`, `"Secret Key"`, `"DB Connection String"`, `"Test Connection"`, `"No database"`, `"Select a database"`, `"Leave blank to keep existing"`, `"Delete connection"`, `"Connection saved"`, plus any help text used by the page in Task C3. Use the **localization-translator** workflow for the 9 non-English locales (this is a normal in-app feature; installer/docs/demo stay English-only and are untouched).

- [ ] **Step 4: Parity + orphan check**

Run:
```bash
cd /Users/noriellecruz/Web/webby
for g in admin profile; do
  base=$(python3 -c "import json;print(len(json.load(open('lang/en/$g.json'))))")
  for l in ar de fr id it ja pt ru zh; do
    n=$(python3 -c "import json;print(len(json.load(open('lang/$l/$g.json'))))")
    [ "$n" = "$base" ] && echo "$g/$l OK ($n)" || echo "$g/$l PARITY MISMATCH ($n vs $base)"
  done
done
grep -rn "Supabase Cloud\|Self-hosted Supabase is configured" resources/js && echo "ORPHAN REF" || echo "no orphan refs"
```
Expected: all `OK`, `no orphan refs`.

- [ ] **Step 5: Commit + cleanup:**
```bash
rm -f /tmp/byod_i18n.py
git add lang/ && git commit -m "i18n(byod): remove dead admin Supabase strings; add connection-library strings (all locales)"
```

---

## Phase I — Final verification

**Files:** none (verification only).

- [ ] **Step 1: Laravel** — `php artisan about` boots; `php -d memory_limit=512M ./vendor/bin/phpunit --filter "Supabase|Project|Plan|Builder"` passes; `./vendor/bin/pint --test app/ database/` clean.
- [ ] **Step 2: Removal grep (webby)** —
```bash
grep -rnE "MODE_SELF_HOSTED|MODE_CLOUD|supabase_mode|CloudProvisioner|SelfHostedProvisioner|SupabaseProvisioner|getDatabaseSettings|updateDatabase|supabase_config|->isConfigured\(\)" app/ routes/ resources/js/ database/ | grep -v "drop_supabase_lifecycle"
```
Expected: no matches.
- [ ] **Step 2b: docs + autosetup grep** — `grep -rniE "self.?hosted|supabase.?mode" docs/src | grep -i supabase` → none; `grep -niE "supabase|docker" autosetup.sh` → none.
- [ ] **Step 3: Go builder** — `cd /Users/noriellecruz/Web/webby-builder && go build ./... && go vet ./... && go test ./...` pass; `grep -rn "internal/supabase\|/api/supabase/provision" internal/` → none.
- [ ] **Step 4: Frontend builds** — `npm run build` (webby root) and `cd docs && npm run build` both clean.
- [ ] **Step 5: Smoke (manual, optional)** — with the stack running: add a connection in Profile → Database Connections (Test passes), create a project with it selected, confirm the build gets `supabase.enabled=true` and `defineTable` writes to the user's DB; create a project with `No database` → capability disabled.

---

## Self-Review (completed by plan author)

- **Spec coverage:** data model (A1/A2), connection library (C1/C3), create selector + resolution (B1/C2/C3), capability+defineTable gating (B2), schema lifecycle/no-auto-drop (B3), removal — Laravel (D1/D2), Go (E), autosetup (F), docs (G), i18n (H), gating kept (`enable_database` referenced throughout), security (encrypted casts A2 + masking C1), testing (I). All spec sections mapped.
- **Placeholder scan:** new code is complete for all backend units; React pages (C3) reference existing component patterns rather than reproducing full files (acceptable — large UI, executor follows the cited existing page); removals cite exact files + verified line ranges; every verification has an exact command + expected output.
- **Type/name consistency:** `supabase_connections` / `supabase_connection_id` / `SupabaseConnection` / `supabaseConnection()` / `supabaseConnections()` / `hasConnection()` used consistently; `resolveForProject` bundle keys (`url, publishable_key, secret_key, schema, db_connection`) unchanged so downstream consumers are untouched; gate is `plan.databaseEnabled() && hasConnection($project)` in both BuilderService and BuilderSupabaseController.
