inmanturbo/homework-organizations

Database-per-organization isolation for Homework OAuth client applications

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/inmanturbo/homework-organizations

v0.0.1 2025-10-08 18:34 UTC

This package is auto-updated.

Last update: 2025-10-08 18:35:23 UTC


README

Latest Version on Packagist Tests Code Style Total Downloads

Database-per-organization support for Laravel applications using the Homework OAuth ecosystem. Provides database-per-organization isolation with zero code changes required in your application logic.

Features

  • Database-per-Organization - Automatic organization database switching based on authenticated user
  • Invisible Organization Isolation - No code changes needed - organization isolation happens automatically
  • Storage Isolation - Organization-specific storage folders while maintaining standard Laravel storage:link compatibility
  • Queue Organization Context - Jobs automatically execute in correct organization context using Laravel Context
  • Context Integration - Organization information automatically available in logs and across requests/jobs
  • Pipeline Architecture - Extensible actions for custom organization switching logic
  • Auto-Migration - Organization databases created and migrated automatically on first access
  • Event-Driven - OrganizationSwitching and OrganizationSwitched events for hooks
  • Octane-Safe - Scoped singleton bindings prevent organization bleeding

Requirements

  • PHP 8.2+
  • Laravel 11.x
  • SQLite extension (for central database)

Installation

Step 1: Install the Package

composer require inmanturbo/homework-organizations

Step 2: Run the Install Command

php artisan organizations:install

This command will:

  • Configure .env to use SQLite for sessions, cache, and queue
  • Comment out DB_DATABASE in .env (uses default database/database.sqlite)
  • Modify User model to use workos_id as primary key
  • Update users table migration to use workos_id as primary key

Step 3: Run Migrations

Run SQLite migrations for infrastructure (sessions, cache, jobs):

php artisan migrate --database=sqlite \
  --path="database/migrations/0001_01_01_000000_create_users_table.php" \
  --path="database/migrations/0001_01_01_000001_create_cache_table.php" \
  --path="database/migrations/0001_01_01_000002_create_jobs_table.php"

Run tenant.database migrations (uses default connection):

php artisan migrate

WorkOS Integration

When using this package with Laravel WorkOS authentication, use the OrganizationUserResolver to handle organization-aware user operations:

use Inmanturbo\HomeworkOrganizations\Support\OrganizationUserResolver;
use Laravel\WorkOS\Http\Requests\AuthKitAuthenticationRequest;

Route::get('authenticate', function (AuthKitAuthenticationRequest $request) {
    return tap(to_route('dashboard'), fn () => $request->authenticate(
        findUsing: OrganizationUserResolver::findUsing(...),
        createUsing: OrganizationUserResolver::createUsing(...),
        updateUsing: OrganizationUserResolver::updateUsing(...),
    ));
})->middleware(['guest']);

The OrganizationUserResolver automatically:

  • Switches to the correct organization database before finding/creating/updating users
  • Adds organization_id to Laravel Context for request tracking
  • Stores organization_id in session for subsequent requests
  • Ensures users are created in the correct organization database
  • Updates the organization_id field when user data changes

Switching Organizations

Users can switch to a different organization by visiting the /select-organization route. This will:

  1. Clear the current organization from session
  2. Log the user out
  3. Redirect to login, triggering organization selection on the OAuth server

Add a link in your navigation:

<a href="{{ route('organizations.select-organization') }}">Switch Organization</a>

This route is automatically registered by the package.

How It Works

Database Strategy

SQLite Database (database/database.sqlite): Stores infrastructure data shared across all organizations:

  • Sessions
  • Cache
  • Queue jobs
  • User records (with organization_id linking to organizations)

Organization Databases (MySQL/dynamic): Isolated per organization using convention-based naming:

  • Database name format: {default_database}_{organization_id}
  • Example: my_app_org_123
  • Contains all application data for that organization
  • Uses your default database connection (MySQL, PostgreSQL, etc.)

Automatic Organization Switching

The package automatically switches organization context based on:

Web Requests:

// Middleware checks authenticated user's organization_id
// Automatically switches to correct organization database

Queue Jobs:

// Laravel Context automatically propagates organization information
// Job executes in correct organization context
// No changes needed to job classes

Context Integration

Organization information is automatically added to Laravel's Context and available throughout your application:

use Illuminate\Support\Facades\Context;

// Available context data:
Context::get('tenant.organization_id'); // Current organization ID
Context::get('tenant.user_id');        // Current user ID
Context::get('tenant.database');       // Current organization database name
Context::get('tenant.storage.public'); // Public storage path
Context::get('tenant.storage.private'); // Private storage path

This context is:

  • Automatically propagated to queued jobs
  • Included in log entries for better debugging
  • Shared across HTTP requests and commands
  • Available in event listeners and middleware

Storage Isolation

Organization-specific storage paths are automatically configured:

Public Storage:

storage/app/public/organization_org_123/

Accessible via standard storage:link: https://example.com/storage/organization_org_123/file.jpg

Private Storage:

storage/app/private/organization_org_123/

Use Laravel's storage facade as normal - organization isolation is transparent:

// Automatically uses organization-specific path
Storage::disk('public')->put('avatars/user.jpg', $file);
Storage::disk('private')->put('documents/invoice.pdf', $pdf);

Configuration

Publish the configuration file (optional):

php artisan vendor:publish --tag=organizations-config

Available Options

config/organizations.php:

return [
    // Pipeline actions executed when switching organizations
    'actions' => [
        \Inmanturbo\HomeworkOrganizations\Actions\SwitchOrganizationDatabase::class,
        \Inmanturbo\HomeworkOrganizations\Actions\SwitchOrganizationStorage::class,
        // Add your custom actions here
    ],

    // How long to cache organization database names (seconds)
    'cache_ttl' => env('ORGANIZATIONS_CACHE_TTL', 3600),

    // Auto-migrate organization databases on first access
    'auto_migrate' => env('ORGANIZATIONS_AUTO_MIGRATE', true),
];

Advanced Usage

Manual Organization Switching

use Inmanturbo\HomeworkOrganizations\OrganizationManager;

$manager = app(OrganizationManager::class);
$context = $manager->switch('org_123');

// Now all database queries use organization_org_123
// Storage paths point to organization_org_123 folders

Custom Pipeline Actions

Create a custom action to run during organization switching:

namespace App\Organizations\Actions;

use Closure;
use Inmanturbo\HomeworkOrganizations\OrganizationContext;

class CustomOrganizationAction
{
    public function handle(OrganizationContext $context, Closure $next)
    {
        // Your custom logic here
        // Example: Set organization-specific config values
        config(['app.name' => "App - {$context->organizationId}"]);

        // Pass context data between actions
        $context->data['custom_key'] = 'value';

        return $next($context);
    }
}

Register it in config/organizations.php:

'actions' => [
    \Inmanturbo\HomeworkOrganizations\Actions\SwitchOrganizationDatabase::class,
    \Inmanturbo\HomeworkOrganizations\Actions\SwitchOrganizationStorage::class,
    \App\Organizations\Actions\CustomOrganizationAction::class,
],

Event Listeners

Listen to organization switching events:

use Illuminate\Support\Facades\Event;
use Inmanturbo\HomeworkOrganizations\Events\OrganizationSwitching;
use Inmanturbo\HomeworkOrganizations\Events\OrganizationSwitched;

Event::listen(OrganizationSwitching::class, function ($event) {
    // Before organization switch
    logger("Switching to organization: {$event->organizationId}");
});

Event::listen(OrganizationSwitched::class, function ($event) {
    // After organization switch
    logger("Switched to organization: {$event->organizationId}");
    // Access context data
    $database = $event->context->data['database'];
});

Disabling Auto-Migration

If you prefer manual control over organization migrations:

.env:

ORGANIZATIONS_AUTO_MIGRATE=false

Then migrate organization databases manually (will use default connection with organization database name):

php artisan migrate

Architecture

Pipeline Pattern

The package uses Laravel's Pipeline to execute a series of actions when switching organizations:

OrganizationManager::switch('org_123')
  → Fire OrganizationSwitching event
  → Create OrganizationContext
  → Run through Pipeline:
      → SwitchOrganizationDatabase
      → SwitchOrganizationStorage
      → (Your custom actions)
  → Fire OrganizationSwitched event
  → Return OrganizationContext

Database Naming Convention

Organization databases follow a predictable pattern:

  • Base database: my_app (from config/database.php)
  • Organization ID: org_123
  • Result: my_app_org_123

This convention allows:

  • No external database credential provider needed
  • Easy to understand and debug
  • Automatic database creation on first access

Primary Key Strategy

User records use workos_id as the primary key instead of auto-increment IDs:

  • Why: Prevents session collision across organizations (no duplicate ID 1, 2, 3...)
  • How: WorkOS ID is globally unique across all organizations
  • Compatibility: getIdAttribute() accessor maintains $user->id support

Roadmap

Future enhancements planned for this package:

  • External Database Provider - Fetch credentials from external APIs
  • Per-Organization Database Drivers - Organizations use different database types
  • Bring Your Own Database - Organizations provide their own database credentials
  • Advanced Storage - S3 buckets per organization, CDN integration
  • Organization Admin Access - Direct database access for org admins

Testing

composer test

License

MIT License. See LICENSE for details.