Skip to content

Organizations

Organizations ship as laraspring/organizations, a Composer package your application installs. It owns membership, roles, invitations and the notion of an active organization; your edition owns every screen those flows put in front of a user.

It is also the package that turns the application multi-tenant. Nothing else has to know: every package asks TenantResolver which tenant a request belongs to, and laraspring/core answers "none" until this package is installed.

What you get out of the box

FeatureWhere
Organizations, with a unique slugorganizations table
Membership with three rolesorganization_user table
Invitations by email, with signed single-use linksorganization_invitations table
Session-backed active organizationTenantResolver
Membership revalidated on every requestmiddleware on the web group
Onboarding, picker, settings and members screensyour edition renders them

Optional or required

One decision shapes the whole experience, and it is a config key rather than a code change:

php
// config/laraspring-organizations.php
'mode' => env('LARASPRING_ORGANIZATIONS_MODE', 'optional'),

optional (the default) lets a user into the application with no organization at all. Organizations are a feature they may opt into, which is what a product with solo users wants.

required means a signed-in user with no organization cannot reach the dashboard: every page load sends them to onboarding until they have created one or accepted an invitation. Redirecting them after sign-in would not be enough, because a bookmark or a typed URL skips it, so the gate lives in the middleware.

The gate deliberately does not block everything. laraspring-organizations.required_bypass lists the route names that stay reachable, and its defaults matter: without them somebody who cannot create an organization yet could not sign out, verify their address, or accept the invitation that would have got them in.

php
'required_bypass' => [
    'organizations.*',
    'logout',
    'verification.*',
    'password.confirm',
    'two-factor.*',
],

There is no personal organization

Some kits create an organization for every user at sign-up. This one does not. An organization created behind a user's back is a row every later feature has to special-case, and undoing it once billing is attached is expensive. Pick a mode instead.

Configuration

Publish the config to change anything:

bash
php artisan vendor:publish --tag=laraspring-organizations-config
dotenv
LARASPRING_ORGANIZATIONS_MODE=optional              # or required
LARASPRING_ORGANIZATIONS_INVITATION_EXPIRES=10080   # minutes an invitation stays valid

The migrations and routes can be taken over the same way as any Laraspring package:

bash
php artisan vendor:publish --tag=laraspring-organizations-migrations
php artisan vendor:publish --tag=laraspring-organizations-routes

Publishing the migrations restamps their timestamps so they sort after your own; set laraspring-organizations.migrations to false afterwards so the tables are not created twice.

Routes

Route names are public API. Your frontend resolves them through Ziggy, so a rename is a breaking change.

NameMethodPath
organizations.selectGET/organizations
organizations.createGET/organizations/create
organizations.storePOST/organizations
organizations.switchPUT/organizations/{organization}/switch
organizations.settingsGET/settings/organization
organizations.updatePATCH/settings/organization
organizations.destroyDELETE/settings/organization
organizations.membersGET/settings/organization/members
organizations.members.updatePATCH/settings/organization/members/{member}
organizations.members.destroyDELETE/settings/organization/members/{member}
organizations.ownership.transferPUT/settings/organization/members/{member}/ownership
organizations.leaveDELETE/settings/organization/membership
organizations.invitations.storePOST/settings/organization/invitations
organizations.invitations.resendPOST/settings/organization/invitations/{invitation}/resend
organizations.invitations.destroyDELETE/settings/organization/invitations/{invitation}
organizations.invitations.showGET/invitations/{token}
organizations.invitations.acceptPOST/invitations/{token}

Notice what almost none of them take: an organization in the URL. Every screen past the picker acts on the active organization, which is what keeps organizations.switch the single place the tenant changes. A settings route that accepted an id would be a second one, and a quieter one.

Screens

The package never calls Inertia::render(). It names a screen and your edition decides what that name draws, through Laraspring\Organizations\Contracts\OrganizationScreenRenderer:

Screen constantReference edition pageDrawn
Screens::CREATEorganizations/createbefore the dashboard
Screens::SELECTorganizations/selectbefore the dashboard
Screens::INVITATIONorganizations/invitationbefore the dashboard
Screens::SETTINGSsettings/organizationinside the application
Screens::MEMBERSsettings/organization-membersinside the application

App\Organizations\InertiaOrganizationScreenRenderer holds that map, and is where you point a flow at a page of your own without touching the package.

Events

Everything worth reacting to announces itself, so laraspring/billing and laraspring/mail can hang off organizations without either package knowing about the other:

EventFired when
OrganizationCreateda new organization exists, owner already attached
OrganizationUpdatedits name or slug changed
OrganizationDeletedbefore the delete, while members are still readable
MemberInvitedan invitation was issued or reissued, with the plain token
InvitationRevokeda pending invitation was withdrawn
MemberJoinedsomebody is now a member, however they got in
MemberRemoveda membership ended, expelled or voluntary
MemberRoleChangeda role changed within the organization
OwnershipTransferredthe organization has a new owner

OwnershipTransferred is deliberately separate from MemberRoleChanged: it is the one a billing package has to act on, because whoever the invoices are addressed to just changed.

Laraspring is a commercial starter kit. Buying it gets you the source.