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
Requires
- php: ^8.2
- illuminate/cache: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/filesystem: ^11.0|^12.0
- illuminate/pipeline: ^11.0|^12.0
- illuminate/queue: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.1
- pestphp/pest-plugin-laravel: ^4.0
README
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
andOrganizationSwitched
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 defaultdatabase/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:
- Clear the current organization from session
- Log the user out
- 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
(fromconfig/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.