# Nelara SaaS — Phase 1, Increment 1: Central Layer & Stancl Wiring

This package is a **drop-in addition** to your existing Laravel project at
`/home/nelara/public_html/go.nelarahealth.com`. It contains the central
(`nelara_master`) database layer and the Stancl tenancy wiring. It does **not**
yet touch routes, the login flow, or seed any tenant — those are the next
increments (see §7).

Everything here was built against **stancl/tenancy v3.10** and your actual
codebase (Laravel 11, 108-table HMIS schema). Where a class name or config key
is version-sensitive, it is flagged inline; compare against the freshly
published Stancl files after install (§2).

---

## 1. What's in this package

```
config/tenancy.php                                  # replaces the published Stancl config
database/migrations/central/                        # the 6 platform tables (run vs nelara_master)
  2026_06_07_000010_create_subscription_plans_table.php
  2026_06_07_000020_create_organizations_table.php   # = Stancl tenant table, slug PK
  2026_06_07_000030_create_database_connections_table.php
  2026_06_07_000040_create_organization_users_table.php
  2026_06_07_000050_create_organization_modules_table.php
  2026_06_07_000060_create_platform_audit_logs_table.php
app/Models/Organization.php                         # the Stancl tenant
app/Models/DatabaseConnection.php                   # encrypted per-tenant DB creds
app/Models/SubscriptionPlan.php
app/Models/OrganizationUser.php
app/Models/OrganizationModule.php
app/Models/PlatformAuditLog.php
app/Http/Middleware/SetTenantUrlDefaults.php        # path-based route() generation (v3)
app/Http/Middleware/EnsureSessionTenantMatches.php  # the cross-org access guard
scripts/split-migrations.sh                         # quarantine real-data, move HMIS schema to tenant/
docs/PHASE1-FOUNDATION.md                            # this file
```

Copy these into the project root, preserving paths.

---

## 2. Install Stancl & publish its files

On the server, from the project root, using the EA-PHP 8.3 binary:

```bash
PHP=/opt/cpanel/ea-php83/root/usr/bin/php
$PHP /opt/cpanel/composer/bin/composer require stancl/tenancy:^3.10
$PHP artisan vendor:publish --provider="Stancl\Tenancy\TenancyServiceProvider" --tag=config
$PHP artisan vendor:publish --provider="Stancl\Tenancy\TenancyServiceProvider" --tag=migrations  # central tenants/domains helpers (we don't use them; safe to delete)
```

Then **overwrite** the published `config/tenancy.php` with the one in this
package (ours is tailored for path identification + cPanel pre-created DBs).

Publishing also creates `app/Providers/TenancyServiceProvider.php`. Make one
cPanel-specific edit there (§4).

---

## 3. Edits to existing files

### 3a. `config/database.php` — add two connections, default to central

Add inside the `'connections' => [ ... ]` array:

```php
'central' => [
    'driver'    => 'mysql',
    'host'      => env('CENTRAL_DB_HOST', '127.0.0.1'),
    'port'      => env('CENTRAL_DB_PORT', '3306'),
    'database'  => env('CENTRAL_DB_DATABASE', 'nelara_master'),
    'username'  => env('CENTRAL_DB_USERNAME'),
    'password'  => env('CENTRAL_DB_PASSWORD'),
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
    'strict'    => true,
    'engine'    => null,
],

// Runtime template — Stancl clones this and fills DB name/user/pass per tenant.
'tenant' => [
    'driver'    => 'mysql',
    'host'      => env('TENANT_DB_HOST', '127.0.0.1'),
    'port'      => env('TENANT_DB_PORT', '3306'),
    'database'  => null,
    'username'  => null,
    'password'  => null,
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
    'strict'    => true,
    'engine'    => null,
],
```

And change the default line to:

```php
'default' => env('DB_CONNECTION', 'central'),
```

### 3b. `.env` — central + tenant template

```
DB_CONNECTION=central

CENTRAL_DB_HOST=localhost
CENTRAL_DB_PORT=3306
CENTRAL_DB_DATABASE=nelara_master
CENTRAL_DB_USERNAME=nelara_master_usr
CENTRAL_DB_PASSWORD=__set_in_cpanel__

TENANT_DB_HOST=localhost
TENANT_DB_PORT=3306
```

(`APP_KEY` must be set — the `encrypted` cast on `db_password` depends on it.
Never rotate `APP_KEY` after credentials are stored, or they become unreadable.)

### 3c. `bootstrap/app.php` — register the two middleware aliases

Inside `->withMiddleware(function (Middleware $middleware) { ... })`, add to the
existing `$middleware->alias([...])`:

```php
'tenant.url'   => \App\Http\Middleware\SetTenantUrlDefaults::class,
'tenant.match' => \App\Http\Middleware\EnsureSessionTenantMatches::class,
```

(They are wired into the tenant route group in the next increment, not yet.)

---

## 4. cPanel-specific edit to `app/Providers/TenancyServiceProvider.php`

cPanel's account MySQL user cannot `CREATE DATABASE`, so Stancl's automatic
database creation will fail. In the published provider, find the
`Events\TenantCreated::class` mapping and **comment out the CreateDatabase job**.
We provision tenant DBs by hand in cPanel and migrate them explicitly:

```php
Events\TenantCreated::class => [
    JobPipeline::make([
        // Jobs\CreateDatabase::class,   // DISABLED: DB pre-created in cPanel
        // Jobs\MigrateDatabase::class,  // DISABLED: we run `tenants:migrate` manually
    ])->send(fn (Events\TenantCreated $event) => $event->tenant)
      ->shouldBeQueued(false),
],
```

---

## 5. Stand up `nelara_master` and verify

1. In **WHM/cPanel**, create database `nelara_master` and a user with ALL
   privileges on it; put the credentials in `.env` (§3b).
2. Run the central migrations:

```bash
$PHP artisan migrate --path=database/migrations/central --database=central
```

3. Verify the 6 tables exist and the app still boots:

```bash
$PHP artisan tinker --execute="echo App\Models\Organization::count();"   # expect 0
$PHP artisan about | grep -i database
```

If `Organization::count()` returns `0` without error, the central layer and the
Stancl tenant model are wired correctly. **Do not create any tenant yet** — the
tenant migration split and the demo seeder come next.

---

## 6. Intentional deviations from the brief (so they're on the record)

- **Slug as the tenant primary key.** `organizations.id` holds the slug
  (`crh`, `demo`) instead of a numeric id, because path identification resolves
  the `{tenant}` URL segment straight to the key. A `slug` column is kept too
  for the admin UI. If you ever need a numeric surrogate for external
  references, we add an `org_number` column — no rework to tenancy.
- **`organization_users` is not the HMIS login store.** Per our auth decision,
  ordinary HMIS users authenticate per-tenant against each org's own `users`
  table. `organization_users` holds platform/org-admin accounts only.

---

## 7. What comes next — and the one decision I need

**Increment 2 — routing & login binding:** partition `routes/web.php` (+
`corporate.php`, `anc-consults.php`) into a central group and a `/{tenant}`
tenant group with `InitializeTenancyByPath` + `PreventAccessFromCentralDomains`
+ `tenant.url` + `tenant.match`; stamp `session(['tenant_id' => ...])` at login;
add the reserved-slug guard.

**Increment 3 — the demo tenant + seeder.** This is where the data-loading mess
I flagged has to be resolved, and it needs your call:

> A tenant built from migrations alone is **incomplete** — departments and
> insurance come from migrations, but the **full role/permission set and the
> service catalogue do not** (they only existed in the old `DatabaseSeeder`,
> which also collides with several migrations). So "migrate + a transactions-only
> seeder" would produce broken RBAC.

Two ways forward for building tenant databases:

- **(A) Migrate, then run one authoritative `DemoEnvironmentSeeder`** that
  supplies everything the migrations don't (full roles/permissions, a synthetic
  service catalogue including the controller-critical codes
  `CON011/CON012/DEN035/CON013`, demo users, then 150+ synthetic patients and
  encounters). It resolves all IDs by lookup, never hardcoded. More seeder code,
  but stays within Laravel's normal `tenants:migrate` + seed flow.

- **(B) Golden-schema clone.** Build one reference tenant by running the full
  migration chain once (PII import quarantined), `mysqldump --no-data` it to a
  canonical `tenant_schema.sql`, and stand up each new tenant by importing that
  dump + running the synthetic seeder. More deterministic on cPanel, but adds a
  "regenerate the dump when migrations change" step.

I lean **(A)** — it keeps one source of truth (migrations + seeder) and avoids a
binary artifact to maintain. **Run `scripts/split-migrations.sh` whenever you're
ready** (it's safe and reversible via git), but tell me **A or B** before I write
Increment 3's seeder.

**Increment 4 —** platform admin + org admin screens (org CRUD, suspend/activate,
modules, subscriptions, audit view).

**Increment 5 —** the full command-by-command VPS deployment, as originally
planned.
