Handles admin-provisioned membership — adding members by email + role, password activation via signed links, an assignable-role allow-list, and the members directory UI. An alternative to self-serve team invitations. Activates when provisioning members, wiring activation, gating member endpoints, or rendering the membership UI.
php artisan boost:add-skill happones/kinetix
Save this content to: .claude/skills/kinetix-membership/SKILL.md
# Kinetix Membership Development
## When to Apply
Activate this skill when:
- Enabling the admin-provisioned onboarding model instead of self-serve team invitations.
- Provisioning members (email + role) and wiring the password-activation flow.
- Gating the `members.*` endpoints/UI, or constraining which roles a provisioner may assign.
- Rendering the membership UI (`<KinetixMemberList>`, `<KinetixMemberProvisioner>`, `<KinetixMemberActivation>`) or using the `useKinetixMembers` composable.
- Attaching activated users to the host app's own team pivot via `attach_member` / `detach_member`.
## Provisioning modes (members with no email address) — REQUIRED reading
Two axes, BOTH defaulting to today's behavior:
```php
'provisioning' => 'activation', // | 'direct'
'delivery' => 'mail', // | 'manual'
'identifier' => 'email', // | 'username' | 'phone'
```
- **`activation`** (default) — no `User` until the person sets their own
password. The invariant: no password-less accounts pile up.
- **`direct`** — the `User` is created immediately with a temporary password.
GIVES UP that invariant in exchange for working with NO delivery channel,
which is the point when staff have no email address. Provision goes straight
to `active`.
- **`manual`** delivery sends nothing and returns the credential ONCE.
- **`sms`** delivery texts the link to the member's phone over
`membership.sms_channel`.
### Rules
- **The credential is shown exactly once.** It is hashed into the user record;
there is no reading it back. `POST {prefix}/members/{provision}/credential`
always REGENERATES (the old one stops working). Never add a "reveal" that
retrieves.
- **`members.credentials` is a separate ability** from `members.provision` —
handing over a credential means being able to become that person.
- **`direct` requires `credentials.enabled`**, or the forced first-login change
never happens and the admin-chosen password works forever. Boot warning +
`kinetix:doctor`.
- **`membership.identifier` must be in `credentials.identity.fields`** or
Kinetix falls back to `email` — provisioning someone under an identifier
nobody can sign in with is a directory entry, not an account.
- **Identifiers are normalized** through the same resolver the login uses
(`KinetixIdentity::normalize`), so one phone number cannot become two
provisions.
- **`store()` stays `updateOrCreate`** whatever the mode: a repeat on a known
identifier must look identical, or provisioning becomes a membership oracle.
- **Use `identifier` for display**, not `email` — it is whichever field carries
it. `email`/`username`/`phone` travel alongside.
### SMS delivery
NEVER hardcode an SMS provider. Vonage/Twilio/local gateways each define their
own channel AND their own message object; the choice is a business decision
about coverage and price. Kinetix routes over `membership.sms_channel` and ships
the TEXT (`smsContent()`); the host subclasses the notification and points
`membership.activation_notification` at it:
```php
class SmsActivation extends MemberActivationNotification
{
public function toVonage(object $notifiable): VonageMessage
{
return (new VonageMessage)->content($this->smsContent());
}
}
```
A missing `to{Channel}()` would throw inside a QUEUED job at send time — nobody
sees it and the member never gets their link. Kinetix checks the method exists
first and falls back to handing the link over; `kinetix:doctor` errors on it.
Same fallback when the member has no phone on file. Keep `smsContent()` short:
a signed URL is long and every extra character risks a second billable segment.
## Documentation
For full details, reference `docs/membership.md` (published at https://happones.github.io/kinetix/membership).
## Common integration mistakes
> **Diagnose first.** `php artisan kinetix:doctor` reports every silent
> misconfiguration in one shot (prefix, team scoping, teamless roles,
> `attach_member`, config closures, published-file drift, duplicated i18n
> bundles) and exits non-zero on errors.
Check these **first** — they are what actually breaks integrations, and each one
fails silently:
1. **Writing your own controller under the wrong prefix.** The module's endpoints
live at `{current_team}/{kinetix.route_prefix}/members/…` (e.g.
`/acme/_kinetix/members`) and `<KinetixMemberList>` calls them itself. A
controller of yours at `{current_team}/members` is never invoked — the app
registers only the *Inertia page* route. Verify with
`php artisan kinetix:routes members` before writing any endpoint.
2. **Leaving `attach_member` at `null` in a teams app.** Kinetix assigns the role
and never touches the host's team pivot, so activation succeeds and the member
belongs to **no team** — in a team-routed app they can't reach any page. Kinetix
logs a boot warning for exactly this combination.
3. **Redefining a `kinetix_*` Inertia prop.** `HandleInertiaRequests::share()` is
merged **over** the package's shared props, so your own `kinetix_permissions`
key silently replaces Kinetix's and the `members.*` gating in the UI collapses.
Share your data under your own key.
4. **Seeding the assignable roles without team context.** Under team scoping a
role created with no team id is global (visible in every team, super-admin
editable only), so it won't behave like the team role you expected.
`kinetix:permissions:sync` lists teamless roles.
5. **Writing a config callback as a closure.** `assignable_roles`,
`attach_member`, `detach_member` and `assign_role` as `fn (...) => ...` make
`php artisan config:cache` abort ("value at … is non-serializable"), i.e. the
app can't deploy. Always use the serializable forms:
`[SyncProvisionedMember::class, 'attach']` or an invokable class-string.
6. **A fixed `assignable_roles` list in a published config.** Configs published
before v0.262 carry `['editor', 'viewer']`, which does NOT follow the Roles
screen: roles created there never reach the member picker. Set it to `null`
(the default) to link them; `kinetix:doctor` reports which one is in use.
7. **Trying to make a member super-admin.** The protected roles
(`permissions.protected_roles`, `super-admin` by default) belong to the team
owner and are stripped from the allow-list whatever it says — provisioning or
changing a member to them is a 422. Don't work around it.
## Configuration
Enable the module in `config/kinetix.php` (opt-in, default off) and publish + run the migration:
```bash
php artisan vendor:publish --tag=kinetix-membership-migrations
php artisan migrate
```
```php
'membership' => [
'enabled' => env('KINETIX_MEMBERSHIP_ENABLED', false),
'teams' => env('KINETIX_MEMBERSHIP_TEAMS', false),
'user_model' => env('KINETIX_MEMBERSHIP_USER_MODEL', 'App\\Models\\User'),
// The ONLY roles a provisioner may assign — the guard that keeps added
// members from ever becoming admin. null (default) = the Roles screen's
// roles for the team (team + global), else a fixed array or an invokable
// class-string (config:cache-safe). Entries may be names or
// ['name' => …, 'label' => …, 'description' => …]. Protected roles
// (super-admin) are always removed.
'assignable_roles' => null,
'activation_expiry' => env('KINETIX_MEMBERSHIP_ACTIVATION_HOURS', 72),
'activation_view' => env('KINETIX_MEMBERSHIP_ACTIVATION_VIEW', 'Kinetix/MemberActivation'),
'redirect_after' => env('KINETIX_MEMBERSHIP_REDIRECT', '/'),
// Optional callables: fn ($user, MemberProvision $provision) => void
'attach_member' => null,
'detach_member' => null,
// Roles from the app's own logic, not spatie: [Class, 'method'] receiving
// ($user, ?string $to, ?string $from, MemberProvision $provision).
// null $to removes, null $from grants. null (default) = spatie roles.
'assign_role' => null,
],
```
### Roles that don't come from Roles & Permissions
The member picker does NOT require the Roles screen. When the app already
decides what a member is (a pivot `role` column, a flag, another package),
point `assignable_roles` at an invokable returning
`[['name' => 'cashier', 'label' => __('Cashier'), 'description' => __('…')], …]`
and `assign_role` at the method that writes it. Kinetix then never calls
spatie for members (activation, direct provisioning, role change, revoke all
go through the callback), still enforces the allow-list server-side, and
still strips the protected roles.
---
## Invitation vs Provisioning
This module is an **alternative** to the starter-kit's self-serve team invitations,
not an extension of them. Use provisioning when admins should add people who never
own anything and never create a personal team — they only activate by setting a
password. Registration stays closed; the only way in is provisioning.
---
## Backend Usage
The module registers a `members` feature with the permission registry, so its
abilities appear in the permission matrix and `kinetix:permissions:sync`:
`members.viewAny`, `members.provision`, `members.update`, `members.revoke`.
Management endpoints, gated through Laravel's Gate like `roles.manage`. `{prefix}`
is `{current_team}/_kinetix` with teams on, `_kinetix` without — **never a path of
your own** (`php artisan kinetix:routes members` prints the resolved URIs):
| Method | Endpoint | Ability |
|---|---|---|
| `GET` | `{prefix}/members` | `members.viewAny` |
| `POST` | `{prefix}/members` | `members.provision` |
| `POST` | `{prefix}/members/{provision}/resend` | `members.provision` |
| `PUT` | `{prefix}/members/{provision}` | `members.update` |
| `DELETE` | `{prefix}/members/{provision}` | `members.revoke` |
Outside the members screen (admin panel, console, job), revoke with
`KinetixMembership::revoke($provision)`: role removed in the provision's team,
`detach_member` runs, provision kept as `revoked`. It checks no ability.
Activation endpoints are public but protected by a temporary signed URL (GET shows
the page, POST completes it — both share the same path so one signature covers both):
| Method | Endpoint |
|---|---|
| `GET` | `/members/activate/{provision}` (signed) |
| `POST` | `/members/activate/{provision}` (signed) |
**The allow-list is the security boundary.** A user who can `members.provision`
still cannot assign a role outside `membership.assignable_roles` — it is enforced
at provision **and** activation time (at activation against the *provision's*
team, since the signed URL carries no `{current_team}` segment). Kinetix assigns
the dynamic Kinetix role; it never touches the host's team pivot, so wire
`attach_member` — **required** once team scoping is on:
Teams that create their own roles in the Roles UI also need a **dynamic**
allow-list — a callback receiving the provision's team key instead of a static
array. **Write both as callable arrays, never closures**: a closure in a config
file makes `php artisan config:cache` fail, so a closure-based config is not
deployable.
```php
// config/kinetix.php → membership
'attach_member' => [\App\Kinetix\SyncProvisionedMember::class, 'attach'],
'detach_member' => [\App\Kinetix\SyncProvisionedMember::class, 'detach'],
'assignable_roles' => [\App\Kinetix\AssignableRoles::class, 'forTeam'],
```
```php
class SyncProvisionedMember
{
public function attach(Model $user, MemberProvision $provision): void
{
if ($provision->team_id !== null) {
Team::find($provision->team_id)?->users()->attach($user->getKey());
}
}
}
// Prefer the built-in resolver — it matches the Roles UI's visibility rule
// (the team's roles PLUS global team-NULL ones, minus protected roles):
// 'assignable_roles' => \Happones\Kinetix\Permissions\AssignableRoles::class,
// To withhold extra names, wrap it:
class AssignableRoles
{
public function __invoke(int|string|null $teamId): array
{
return \Happones\Kinetix\Permissions\AssignableRoles::names($teamId, except: ['admin']);
}
}
```
NEVER hand-write `where('team_id', $teamId)` here: it drops global (team-NULL)
roles — exactly where `KinetixRolesSeeder`'s presets live — leaving the invite
picker empty. The correct scope is `whereNull('team_id')->orWhere('team_id', $teamId)`
(what `AssignableRoles::query()` returns). An array is read as a callback only
when it is a `[class-string, method]` pair, so `['editor', 'viewer']` still
means a list of role names.
State machine: `update`/`resend` on a REVOKED provision and `resend` on an
ACTIVE one are 422s — re-provision deliberately. The activation mail is
`ShouldQueue`: without a running worker (or `QUEUE_CONNECTION=sync`) no invite
email is ever sent while the UI reports success.
---
## Frontend Usage
Components publish with `--tag=kinetix-components` and read the reactive
`kinetix_permissions` prop, so gate them with the same `{feature}.{ability}` keys.
### 1. Members directory (drop-in)
`<KinetixMemberList>` embeds the provisioning form (identifier field from
`membership.identifier`, optional name, role picker showing what the role
grants and that super-admin is the owner's) and lists members with joined /
invited / expiry dates, status filters with counts, role change (a role that
is no longer assignable stays listed, locked), resend, new link / new password
(the credential shown once, to copy) and revoke. Every action shows only when
the viewer holds its `members.*` ability:
```vue
<script setup lang="ts">
import { useKinetixCan } from '@/composables/useKinetixCan'
import KinetixMemberList from '@/components/kinetix/KinetixMemberList.vue'
const { can } = useKinetixCan()
</script>
<template>
<KinetixMemberList v-if="can('members.provision')" />
</template>
```
For a custom layout, compose the presentational
`<KinetixMemberProvisioner :assignable-roles :roles :reserved-roles :identifier-type @submit>`
(`submit(identifier, role, name?)`) with the `useKinetixMembers()` composable
(`load`, `provision(identifier, role, name?)` → the provision with its one-time
`credential`, `issueCredential`, `resend`, `updateRole`, `revoke`, plus reactive
`provisions`, `assignableRoles`, `roles`, `reservedRoles`, `identifierField`,
`can`).
### 2. Activation page
Render `<KinetixMemberActivation :email="email" :action="action">` from the page
named by `membership.activation_view` (default `Kinetix/MemberActivation`),
forwarding the `email` and `action` props the controller passes. Email is fixed
(from the provision); the member only sets a name + password, posting back to the
signed `action` URL.
---
## Composing with Permissions
Membership and [Permissions](https://happones.github.io/kinetix/permissions) are
independent but complementary: **teams = where**, **roles/permissions = what**,
**provisioning = the intersection** (adds a user to a team AND assigns a role).
With Permissions enabled, grant the `members.*` abilities to a role; without it,
define those gates in your app.
## UUID / ULID Host Models
This feature's migration builds `user_id`, `team_id` and `invited_by` with
`Happones\Kinetix\Support\HostKeys`, which types each column after YOUR model
at migrate time from its primary key (`HasUlids` -> ulid, `HasUuids` -> uuid, string `$keyType` ->
string, else bigint). Pin `kinetix.key_types.user|team` when detection cannot
see the setup; morph ids follow `kinetix.key_types.morph` (default bigint) —
set it when the referenced models use UUIDs/ULIDs. Apps migrated on an older
Kinetix have bigint columns on disk and need their own ALTER migration. Full
recipe: the `kinetix-boost` skill, section "UUID / ULID Host Models".
Activate this skill when:
members.* endpoints/UI, or constraining which roles a provisioner may assign.<KinetixMemberList>, <KinetixMemberProvisioner>, <KinetixMemberActivation>) or using the useKinetixMembers composable.attach_member / detach_member.Two axes, BOTH defaulting to today's behavior:
'provisioning' => 'activation', // | 'direct'
'delivery' => 'mail', // | 'manual'
'identifier' => 'email', // | 'username' | 'phone'
activation (default) — no User until the person sets their own
password. The invariant: no password-less accounts pile up.direct — the User is created immediately with a temporary password.
GIVES UP that invariant in exchange for working with NO delivery channel,
which is the point when staff have no email address. Provision goes straight
to active.manual delivery sends nothing and returns the credential ONCE.sms delivery texts the link to the member's phone over
membership.sms_channel.POST {prefix}/members/{provision}/credential
always REGENERATES (the old one stops working). Never add a "reveal" that
retrieves.members.credentials is a separate ability from members.provision —
handing over a credential means being able to become that person.direct requires credentials.enabled, or the forced first-login change
never happens and the admin-chosen password works forever. Boot warning +
kinetix:doctor.membership.identifier must be in credentials.identity.fields or
Kinetix falls back to email — provisioning someone under an identifier
nobody can sign in with is a directory entry, not an account.KinetixIdentity::normalize), so one phone number cannot become two
provisions.store() stays updateOrCreate whatever the mode: a repeat on a known
identifier must look identical, or provisioning becomes a membership oracle.identifier for display, not email — it is whichever field carries
it. email/username/phone travel alongside.NEVER hardcode an SMS provider. Vonage/Twilio/local gateways each define their
own channel AND their own message object; the choice is a business decision
about coverage and price. Kinetix routes over membership.sms_channel and ships
the TEXT (smsContent()); the host subclasses the notification and points
membership.activation_notification at it:
class SmsActivation extends MemberActivationNotification
{
public function toVonage(object $notifiable): VonageMessage
{
return (new VonageMessage)->content($this->smsContent());
}
}
A missing to{Channel}() would throw inside a QUEUED job at send time — nobody
sees it and the member never gets their link. Kinetix checks the method exists
first and falls back to handing the link over; kinetix:doctor errors on it.
Same fallback when the member has no phone on file. Keep smsContent() short:
a signed URL is long and every extra character risks a second billable segment.
For full details, reference docs/membership.md (published at https://happones.github.io/kinetix/membership).
Diagnose first.
php artisan kinetix:doctorreports every silent misconfiguration in one shot (prefix, team scoping, teamless roles,attach_member, config closures, published-file drift, duplicated i18n bundles) and exits non-zero on errors.
Check these first — they are what actually breaks integrations, and each one fails silently:
Writing your own controller under the wrong prefix. The module's endpoints
live at {current_team}/{kinetix.route_prefix}/members/… (e.g.
/acme/_kinetix/members) and <KinetixMemberList> calls them itself. A
controller of yours at {current_team}/members is never invoked — the app
registers only the Inertia page route. Verify with
php artisan kinetix:routes members before writing any endpoint.
Leaving attach_member at null in a teams app. Kinetix assigns the role
and never touches the host's team pivot, so activation succeeds and the member
belongs to no team — in a team-routed app they can't reach any page. Kinetix
logs a boot warning for exactly this combination.
Redefining a kinetix_* Inertia prop. HandleInertiaRequests::share() is
merged over the package's shared props, so your own kinetix_permissions
key silently replaces Kinetix's and the members.* gating in the UI collapses.
Share your data under your own key.
Seeding the assignable roles without team context. Under team scoping a
role created with no team id is global (visible in every team, super-admin
editable only), so it won't behave like the team role you expected.
kinetix:permissions:sync lists teamless roles.
Writing a config callback as a closure. assignable_roles,
attach_member, detach_member and assign_role as fn (...) => ... make
php artisan config:cache abort ("value at … is non-serializable"), i.e. the
app can't deploy. Always use the serializable forms:
[SyncProvisionedMember::class, 'attach'] or an invokable class-string.
A fixed assignable_roles list in a published config. Configs published
before v0.262 carry ['editor', 'viewer'], which does NOT follow the Roles
screen: roles created there never reach the member picker. Set it to null
(the default) to link them; kinetix:doctor reports which one is in use.
Trying to make a member super-admin. The protected roles
(permissions.protected_roles, super-admin by default) belong to the team
owner and are stripped from the allow-list whatever it says — provisioning or
changing a member to them is a 422. Don't work around it.
Enable the module in config/kinetix.php (opt-in, default off) and publish + run the migration:
php artisan vendor:publish --tag=kinetix-membership-migrations
php artisan migrate
'membership' => [
'enabled' => env('KINETIX_MEMBERSHIP_ENABLED', false),
'teams' => env('KINETIX_MEMBERSHIP_TEAMS', false),
'user_model' => env('KINETIX_MEMBERSHIP_USER_MODEL', 'App\\Models\\User'),
// The ONLY roles a provisioner may assign — the guard that keeps added
// members from ever becoming admin. null (default) = the Roles screen's
// roles for the team (team + global), else a fixed array or an invokable
// class-string (config:cache-safe). Entries may be names or
// ['name' => …, 'label' => …, 'description' => …]. Protected roles
// (super-admin) are always removed.
'assignable_roles' => null,
'activation_expiry' => env('KINETIX_MEMBERSHIP_ACTIVATION_HOURS', 72),
'activation_view' => env('KINETIX_MEMBERSHIP_ACTIVATION_VIEW', 'Kinetix/MemberActivation'),
'redirect_after' => env('KINETIX_MEMBERSHIP_REDIRECT', '/'),
// Optional callables: fn ($user, MemberProvision $provision) => void
'attach_member' => null,
'detach_member' => null,
// Roles from the app's own logic, not spatie: [Class, 'method'] receiving
// ($user, ?string $to, ?string $from, MemberProvision $provision).
// null $to removes, null $from grants. null (default) = spatie roles.
'assign_role' => null,
],
The member picker does NOT require the Roles screen. When the app already
decides what a member is (a pivot role column, a flag, another package),
point assignable_roles at an invokable returning
[['name' => 'cashier', 'label' => __('Cashier'), 'description' => __('…')], …]
and assign_role at the method that writes it. Kinetix then never calls
spatie for members (activation, direct provisioning, role change, revoke all
go through the callback), still enforces the allow-list server-side, and
still strips the protected roles.
This module is an alternative to the starter-kit's self-serve team invitations, not an extension of them. Use provisioning when admins should add people who never own anything and never create a personal team — they only activate by setting a password. Registration stays closed; the only way in is provisioning.
The module registers a members feature with the permission registry, so its
abilities appear in the permission matrix and kinetix:permissions:sync:
members.viewAny, members.provision, members.update, members.revoke.
Management endpoints, gated through Laravel's Gate like roles.manage. {prefix}
is {current_team}/_kinetix with teams on, _kinetix without — never a path of
your own (php artisan kinetix:routes members prints the resolved URIs):
| Method | Endpoint | Ability |
|---|---|---|
GET |
{prefix}/members |
members.viewAny |
POST |
{prefix}/members |
members.provision |
POST |
{prefix}/members/{provision}/resend |
members.provision |
PUT |
{prefix}/members/{provision} |
members.update |
DELETE |
{prefix}/members/{provision} |
members.revoke |
Outside the members screen (admin panel, console, job), revoke with
KinetixMembership::revoke($provision): role removed in the provision's team,
detach_member runs, provision kept as revoked. It checks no ability.
Activation endpoints are public but protected by a temporary signed URL (GET shows the page, POST completes it — both share the same path so one signature covers both):
| Method | Endpoint |
|---|---|
GET |
/members/activate/{provision} (signed) |
POST |
/members/activate/{provision} (signed) |
The allow-list is the security boundary. A user who can members.provision
still cannot assign a role outside membership.assignable_roles — it is enforced
at provision and activation time (at activation against the provision's
team, since the signed URL carries no {current_team} segment). Kinetix assigns
the dynamic Kinetix role; it never touches the host's team pivot, so wire
attach_member — required once team scoping is on:
Teams that create their own roles in the Roles UI also need a dynamic
allow-list — a callback receiving the provision's team key instead of a static
array. Write both as callable arrays, never closures: a closure in a config
file makes php artisan config:cache fail, so a closure-based config is not
deployable.
// config/kinetix.php → membership
'attach_member' => [\App\Kinetix\SyncProvisionedMember::class, 'attach'],
'detach_member' => [\App\Kinetix\SyncProvisionedMember::class, 'detach'],
'assignable_roles' => [\App\Kinetix\AssignableRoles::class, 'forTeam'],
class SyncProvisionedMember
{
public function attach(Model $user, MemberProvision $provision): void
{
if ($provision->team_id !== null) {
Team::find($provision->team_id)?->users()->attach($user->getKey());
}
}
}
// Prefer the built-in resolver — it matches the Roles UI's visibility rule
// (the team's roles PLUS global team-NULL ones, minus protected roles):
// 'assignable_roles' => \Happones\Kinetix\Permissions\AssignableRoles::class,
// To withhold extra names, wrap it:
class AssignableRoles
{
public function __invoke(int|string|null $teamId): array
{
return \Happones\Kinetix\Permissions\AssignableRoles::names($teamId, except: ['admin']);
}
}
NEVER hand-write where('team_id', $teamId) here: it drops global (team-NULL)
roles — exactly where KinetixRolesSeeder's presets live — leaving the invite
picker empty. The correct scope is whereNull('team_id')->orWhere('team_id', $teamId)
(what AssignableRoles::query() returns). An array is read as a callback only
when it is a [class-string, method] pair, so ['editor', 'viewer'] still
means a list of role names.
State machine: update/resend on a REVOKED provision and resend on an
ACTIVE one are 422s — re-provision deliberately. The activation mail is
ShouldQueue: without a running worker (or QUEUE_CONNECTION=sync) no invite
email is ever sent while the UI reports success.
Components publish with --tag=kinetix-components and read the reactive
kinetix_permissions prop, so gate them with the same {feature}.{ability} keys.
<KinetixMemberList> embeds the provisioning form (identifier field from
membership.identifier, optional name, role picker showing what the role
grants and that super-admin is the owner's) and lists members with joined /
invited / expiry dates, status filters with counts, role change (a role that
is no longer assignable stays listed, locked), resend, new link / new password
(the credential shown once, to copy) and revoke. Every action shows only when
the viewer holds its members.* ability:
<script setup lang="ts">
import { useKinetixCan } from '@/composables/useKinetixCan'
import KinetixMemberList from '@/components/kinetix/KinetixMemberList.vue'
const { can } = useKinetixCan()
</script>
<template>
<KinetixMemberList v-if="can('members.provision')" />
</template>
For a custom layout, compose the presentational
<KinetixMemberProvisioner :assignable-roles :roles :reserved-roles :identifier-type @submit>
(submit(identifier, role, name?)) with the useKinetixMembers() composable
(load, provision(identifier, role, name?) → the provision with its one-time
credential, issueCredential, resend, updateRole, revoke, plus reactive
provisions, assignableRoles, roles, reservedRoles, identifierField,
can).
Render <KinetixMemberActivation :email="email" :action="action"> from the page
named by membership.activation_view (default Kinetix/MemberActivation),
forwarding the email and action props the controller passes. Email is fixed
(from the provision); the member only sets a name + password, posting back to the
signed action URL.
Membership and Permissions are
independent but complementary: teams = where, roles/permissions = what,
provisioning = the intersection (adds a user to a team AND assigns a role).
With Permissions enabled, grant the members.* abilities to a role; without it,
define those gates in your app.
This feature's migration builds user_id, team_id and invited_by with
Happones\Kinetix\Support\HostKeys, which types each column after YOUR model
at migrate time from its primary key (HasUlids -> ulid, HasUuids -> uuid, string $keyType ->
string, else bigint). Pin kinetix.key_types.user|team when detection cannot
see the setup; morph ids follow kinetix.key_types.morph (default bigint) —
set it when the referenced models use UUIDs/ULIDs. Apps migrated on an older
Kinetix have bigint columns on disk and need their own ALTER migration. Full
recipe: the kinetix-boost skill, section "UUID / ULID Host Models".
How can I help you explore Laravel packages today?