Appearance
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
| Feature | Where |
|---|---|
| Organizations, with a unique slug | organizations table |
| Membership with three roles | organization_user table |
| Invitations by email, with signed single-use links | organization_invitations table |
| Session-backed active organization | TenantResolver |
| Membership revalidated on every request | middleware on the web group |
| Onboarding, picker, settings and members screens | your 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-configdotenv
LARASPRING_ORGANIZATIONS_MODE=optional # or required
LARASPRING_ORGANIZATIONS_INVITATION_EXPIRES=10080 # minutes an invitation stays validThe 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-routesPublishing 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.
| Name | Method | Path |
|---|---|---|
organizations.select | GET | /organizations |
organizations.create | GET | /organizations/create |
organizations.store | POST | /organizations |
organizations.switch | PUT | /organizations/{organization}/switch |
organizations.settings | GET | /settings/organization |
organizations.update | PATCH | /settings/organization |
organizations.destroy | DELETE | /settings/organization |
organizations.members | GET | /settings/organization/members |
organizations.members.update | PATCH | /settings/organization/members/{member} |
organizations.members.destroy | DELETE | /settings/organization/members/{member} |
organizations.ownership.transfer | PUT | /settings/organization/members/{member}/ownership |
organizations.leave | DELETE | /settings/organization/membership |
organizations.invitations.store | POST | /settings/organization/invitations |
organizations.invitations.resend | POST | /settings/organization/invitations/{invitation}/resend |
organizations.invitations.destroy | DELETE | /settings/organization/invitations/{invitation} |
organizations.invitations.show | GET | /invitations/{token} |
organizations.invitations.accept | POST | /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 constant | Reference edition page | Drawn |
|---|---|---|
Screens::CREATE | organizations/create | before the dashboard |
Screens::SELECT | organizations/select | before the dashboard |
Screens::INVITATION | organizations/invitation | before the dashboard |
Screens::SETTINGS | settings/organization | inside the application |
Screens::MEMBERS | settings/organization-members | inside 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:
| Event | Fired when |
|---|---|
OrganizationCreated | a new organization exists, owner already attached |
OrganizationUpdated | its name or slug changed |
OrganizationDeleted | before the delete, while members are still readable |
MemberInvited | an invitation was issued or reissued, with the plain token |
InvitationRevoked | a pending invitation was withdrawn |
MemberJoined | somebody is now a member, however they got in |
MemberRemoved | a membership ended, expelled or voluntary |
MemberRoleChanged | a role changed within the organization |
OwnershipTransferred | the 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.