a2insights/octo-core
Core Laravel package for Octo, providing foundational modules, settings, authentication, and shared utilities for building modular admin panels and content-driven applications.
a2insights/octo-core is a starter/boilerplate codebase for a Filament-powered SaaS: teams (multi-tenancy), user management, subscriptions, and the surrounding auth/panel scaffolding. Treat it as a template you fork or vendor, not a typical drop-in utility package — the fastest path is to clone/fork the repo and copy the parts you need, or install it as a dependency and publish its assets.
Minimal steps:
composer require a2insights/octo-core
# Publish migrations, config, and Filament resources/views
php artisan vendor:publish --provider="A2Insights\OctoCore\OctoCoreServiceProvider"
php artisan migrate
php artisan db:seed # seeds roles/permissions and a default admin if provided
Then wire it into a Filament panel:
// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->tenant(Team::class) // if tenancy is enabled
->tenantMiddleware([
\A2Insights\OctoCore\Http\Middleware\SetCurrentTeam::class,
])
->plugins([
\A2Insights\OctoCore\OctoCorePlugin::make(),
]);
}
Where to look first:
config/octo-core.php (after publishing) — this is the contract for the whole package: model class overrides, tenancy on/off, billing driver, feature flags.src/Models/ — User, Team, and any Membership/Invitation models. These are what you'll extend.src/Filament/ — Resources, Pages, and Clusters. Copy these into app/Filament/ rather than fight the vendor versions.database/migrations/ — check table names before you design your own schema on top.First use case: stand up a multi-tenant admin panel where users register, create/join a team, and manage team-scoped resources (projects, members, invitations) — with subscription state gating features. That's what the template is shaped for; get one team-scoped resource rendering end-to-end before touching billing.
1. Extend, don't edit vendor code. Point the config at your subclasses:
// config/octo-core.php
return [
'models' => [
'user' => App\Models\User::class,
'team' => App\Models\Team::class,
],
'tenancy' => [
'enabled' => true,
'slug_attribute' => 'slug',
],
];
class User extends \A2Insights\OctoCore\Models\User
{
public function favoriteColor(): Attribute { /* ... */ }
}
This is the single most important pattern — the package's value proposition is that its own classes resolve through config, so your app owns the concrete models.
2. Tenant scoping on resources. Resources in a tenant panel should scope to the current team. The idiomatic Filament way:
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->whereBelongsTo(Filament::getTenant());
}
If the package ships a BelongsToTeam trait, apply it to your domain models so the package's global scoping middleware covers them too. Check whether ->tenant() on the panel is enough or whether the package registers its own global scope — they can conflict.
3. Team lifecycle hooks. Registration → team creation → invitation acceptance is usually the fragile part. Register listeners rather than overriding controllers:
Event::listen(TeamCreated::class, function (TeamCreated $event) {
$event->team->subscriptions()->create([...]);
});
If the package exposes events, prefer them; if not, override the CreateNewUser / CreateTeam action classes in config.
4. Billing integration. Typical shape: a Billable trait on Team, a CheckoutController or Filament page for plan selection, and a webhook route registered by the package. Keep your webhook handler after the package's in the route list, or you'll get double-processed subscription updates. Verify the signature middleware is applied:
Route::post('/stripe/webhook', [StripeWebhookController::class, 'handle'])
->middleware('octo-core.verify-webhook');
5. Roles and permissions as a seam. Use the package's role definitions as a baseline and add your own in a separate seeder that runs after the package seeder. Don't edit the vendor seeder — it will be overwritten on update.
6. Panel-per-context. A common split: AdminPanelProvider (super-admin, no tenancy) and AppPanelProvider (tenant-scoped, user-facing). Register the plugin or resources in both but gate visibility with policies. Trying to make one panel do both usually ends in canAccessPanel() hacks.
7. Customization of views. Publish views and override at resources/views/vendor/octo-core/. Blade view overrides survive package updates; controller overrides don't.
"a2insights/octo-core": "1.0.*") and consider vendoring the code into app/ for anything you depend on heavily. Forking is a legitimate strategy here.users and teams that clash with your existing app or with Breeze/Jetstream. Read the migration filenames and check for if (! Schema::hasTable(...)) guards before running php artisan migrate on an existing database. Test on a scratch DB first.composer.json for the constraint before upgrading Filament — a v3→v4 jump will break resource signatures (getEloquentQuery, form/table schemas, ->tenant() behavior). Run composer why-not filament/filament if resolution fails.tenancy.enabled after seeding, existing users teams/relationships may be inconsistent. Decide before you seed. The tenant middleware must also be registered in the panel and the panel must call ->tenant() — doing one without the other yields silent null tenants and unscoped queries.php artisan config:cache in local dev will make you think the config file isn't being read. Run config:clear after every edit, or leave caching off locally.Filament::getTenant() returning null inside a resource is almost always a middleware ordering issue. Dump app('octo-core.team') (or whatever the container binding is) in a middleware to confirm the team is bound before the panel renders. Put your SetCurrentTeam middleware after StartSession and Authenticate.return true for team members. Audit every policy before deploying — particularly anything touching billing, API tokens, or member removal.firstOrCreate in your own seeders and never rely on the vendor seeder being idempotent.Billable trait, package events (team created, subscription updated, invitation accepted), published Blade views, and the Filament plugin class if one exists. If a behavior you need isn't reachable through one of those four, overriding a vendor class is the last resort — and it's the first thing to break on update.How can I help you explore Laravel packages today?