Skip to main content

better_auth_api/plugins/organization/
hooks.rs

1//! Typed application hooks. Mutable inputs represent upstream `data` overrides.
2
3use super::types::OrganizationResponse;
4
5use async_trait::async_trait;
6use better_auth_core::{
7    AuthConfig, AuthContext, AuthRequest, AuthResult, AuthSchema, CreateInvitation, CreateMember,
8    CreateOrganization, CreateTeam, Invitation, Member, Team, TeamMember, UpdateOrganization,
9    UpdateTeam,
10    store::{InvitationStore, MemberStore, OrganizationStore, TeamStore},
11    wire::{SessionView, UserView},
12};
13
14/// Request and persistence capabilities supplied to endpoint-aware callbacks.
15pub struct OrganizationEndpoint<'a> {
16    /// HTTP request, absent for a trusted server call without a request.
17    pub request: Option<&'a AuthRequest>,
18    /// Global authentication configuration.
19    pub auth_config: &'a AuthConfig,
20    /// Organization persistence used by the running auth instance.
21    pub organizations: &'a dyn OrganizationStore,
22    /// Team persistence, including atomic member capacity checks.
23    pub teams: &'a dyn TeamStore,
24    /// Organization membership persistence.
25    pub members: &'a dyn MemberStore,
26    /// Invitation persistence.
27    pub invitations: &'a dyn InvitationStore,
28}
29
30impl<'a> OrganizationEndpoint<'a> {
31    pub(crate) fn new<S: AuthSchema>(
32        ctx: &'a AuthContext<S>,
33        request: Option<&'a AuthRequest>,
34    ) -> Self {
35        Self {
36            request,
37            auth_config: &ctx.config,
38            organizations: ctx.database.as_ref(),
39            teams: ctx.database.as_ref(),
40            members: ctx.database.as_ref(),
41            invitations: ctx.database.as_ref(),
42        }
43    }
44}
45
46/// An organization and the user acting on it.
47#[derive(Clone, Copy)]
48pub struct OrganizationUser<'a> {
49    /// Persisted organization associated with the operation.
50    pub organization: &'a OrganizationResponse,
51    /// Acting user, or the invited/added user where upstream supplies that user.
52    pub user: &'a UserView,
53}
54
55/// Member and user supplied to organization update hooks.
56#[derive(Clone, Copy)]
57pub struct OrganizationActor<'a> {
58    /// Acting user's organization membership.
59    pub member: &'a Member,
60    /// Acting user.
61    pub user: &'a UserView,
62}
63
64/// A membership operation and its upstream user subject.
65#[derive(Clone, Copy)]
66pub struct OrganizationMemberEvent<'a> {
67    /// Membership being added, removed, or changed.
68    pub member: &'a Member,
69    /// User supplied by the upstream hook at this call site.
70    pub user: &'a UserView,
71    /// Organization containing the membership.
72    pub organization: &'a OrganizationResponse,
73}
74
75/// Invitation state and the user acting on it.
76#[derive(Clone, Copy)]
77pub struct OrganizationInvitationEvent<'a> {
78    /// Invitation snapshot at this hook's position in the operation.
79    pub invitation: &'a Invitation,
80    /// Inviter, recipient, or cancelling user, according to the hook.
81    pub user: &'a UserView,
82    /// Organization receiving the invited user.
83    pub organization: &'a OrganizationResponse,
84}
85
86/// Team state and its optional authenticated actor.
87#[derive(Clone, Copy)]
88pub struct OrganizationTeamEvent<'a> {
89    /// Persisted team snapshot.
90    pub team: &'a Team,
91    /// Acting user; absent for trusted team creation/deletion without a session.
92    pub user: Option<&'a UserView>,
93    /// Organization containing the team.
94    pub organization: &'a OrganizationResponse,
95}
96
97/// The target of a team membership change.
98#[derive(Clone, Copy)]
99pub struct OrganizationTeamMemberTarget<'a> {
100    /// Team receiving or losing the member.
101    pub team: &'a Team,
102    /// User being added or removed, rather than the administrator.
103    pub user: &'a UserView,
104    /// Organization containing the team.
105    pub organization: &'a OrganizationResponse,
106}
107
108/// Membership input before the adapter generates its creation timestamp.
109#[derive(Debug, Clone)]
110pub struct OrganizationMemberDraft {
111    pub additional_fields: better_auth_core::FieldMap,
112
113    pub organization_id: better_auth_core::SchemaValue<String>,
114    pub user_id: better_auth_core::SchemaValue<String>,
115    pub role: better_auth_core::SchemaValue<String>,
116    /// Server-side addMember input; team assignment is handled separately from this record.
117    pub team_id: Option<String>,
118    /// The hook starts without a timestamp; the adapter always generates the stored value.
119    pub created_at: Option<better_auth_core::FieldDate>,
120}
121impl OrganizationMemberDraft {
122    pub(crate) fn into_create(mut self) -> CreateMember {
123        if let Some(team_id) = self.team_id {
124            let _ = self
125                .additional_fields
126                .insert("teamId".into(), team_id.into());
127        }
128        CreateMember {
129            organization_id: self.organization_id,
130            user_id: self.user_id,
131            role: self.role,
132            additional_fields: self.additional_fields,
133        }
134    }
135}
136
137/// Team fields visible before adapter defaults are applied.
138#[derive(Debug, Clone)]
139pub struct OrganizationTeamDraft {
140    pub additional_fields: better_auth_core::FieldMap,
141
142    pub id: Option<String>,
143    pub name: better_auth_core::SchemaValue<String>,
144    pub organization_id: better_auth_core::SchemaValue<String>,
145    pub created_at: Option<better_auth_core::FieldDate>,
146    /// None leaves the route default; Some(None) explicitly clears the timestamp.
147    pub updated_at: Option<Option<better_auth_core::FieldDate>>,
148}
149impl OrganizationTeamDraft {
150    pub(crate) fn into_create(
151        mut self,
152        created_at: chrono::DateTime<chrono::Utc>,
153        updated_at: Option<chrono::DateTime<chrono::Utc>>,
154    ) -> CreateTeam {
155        if self.created_at.is_some() {
156            let _ = self.additional_fields.remove("createdAt");
157        }
158        if self.updated_at.is_some() {
159            let _ = self.additional_fields.remove("updatedAt");
160        }
161        CreateTeam {
162            additional_fields: self.additional_fields,
163            id: self.id,
164            name: self.name,
165            organization_id: self.organization_id,
166            created_at: Some(self.created_at.unwrap_or_else(|| created_at.into())),
167            updated_at: self
168                .updated_at
169                .unwrap_or_else(|| updated_at.map(Into::into)),
170        }
171    }
172}
173
174/// Invitation fields visible before adapter defaults are applied.
175#[derive(Debug, Clone)]
176pub struct OrganizationInvitationDraft {
177    pub additional_fields: better_auth_core::FieldMap,
178
179    pub id: Option<String>,
180    pub organization_id: String,
181    pub email: String,
182    pub role: String,
183    pub inviter_id: String,
184    pub created_at: better_auth_core::SchemaValue<better_auth_core::FieldDate>,
185    pub status: better_auth_core::SchemaValue<better_auth_core::InvitationStatus>,
186    pub expires_at: better_auth_core::SchemaValue<better_auth_core::FieldDate>,
187    /// Informational first-team alias. Upstream ignores returned changes to this field.
188    pub team_id: better_auth_core::SchemaValue<String>,
189    /// Destination teams used by the adapter after the hook.
190    pub team_ids: better_auth_core::SchemaValue<Vec<String>>,
191}
192impl OrganizationInvitationDraft {
193    pub(crate) fn into_create(
194        mut self,
195        expires_at: chrono::DateTime<chrono::Utc>,
196        inviter_id: &str,
197    ) -> AuthResult<CreateInvitation> {
198        for (name, value) in [
199            ("createdAt", self.created_at.field_value()),
200            ("status", self.status.field_value()),
201            ("expiresAt", self.expires_at.field_value()),
202        ] {
203            if !value.is_undefined() {
204                let _ = self.additional_fields.insert(name.into(), value);
205            }
206        }
207        if self.inviter_id != inviter_id {
208            let _ = self.additional_fields.remove("inviterId");
209        }
210        Ok(CreateInvitation {
211            additional_fields: self.additional_fields,
212            id: self.id,
213            created_at: None,
214            status: None,
215            organization_id: self.organization_id,
216            email: self.email,
217            role: self.role,
218            inviter_id: self.inviter_id,
219            expires_at: expires_at.into(),
220            team_id: super::input::join_invitation_teams(&self.team_ids)?,
221        })
222    }
223}
224
225/// Hooks run in upstream order. Returning an error stops the current operation.
226///
227/// Before hooks modify typed inputs in place. After-hook failures do not undo
228/// persistence that upstream has already committed.
229#[async_trait]
230pub trait OrganizationHooks: Send + Sync {
231    /// Override organization creation fields after the initial slug check.
232    async fn before_create_organization(
233        &self,
234        _data: &mut CreateOrganization,
235        _user: &UserView,
236    ) -> AuthResult<()> {
237        Ok(())
238    }
239    /// Run after owner/default-team creation and before selecting the active organization.
240    async fn after_create_organization(
241        &self,
242        _event: OrganizationMemberEvent<'_>,
243    ) -> AuthResult<()> {
244        Ok(())
245    }
246    /// Override the update after permission and initial slug checks.
247    async fn before_update_organization(
248        &self,
249        _data: &mut UpdateOrganization,
250        _actor: OrganizationActor<'_>,
251    ) -> AuthResult<()> {
252        Ok(())
253    }
254    /// Observe the persisted organization update.
255    async fn after_update_organization(
256        &self,
257        _organization: &OrganizationResponse,
258        _actor: OrganizationActor<'_>,
259    ) -> AuthResult<()> {
260        Ok(())
261    }
262    /// Run before deleting organization data.
263    async fn before_delete_organization(
264        &self,
265        _event: OrganizationUser<'_>,
266        _ctx: OrganizationEndpoint<'_>,
267    ) -> AuthResult<()> {
268        Ok(())
269    }
270    /// Run after deleting organization data.
271    async fn after_delete_organization(
272        &self,
273        _event: OrganizationUser<'_>,
274        _ctx: OrganizationEndpoint<'_>,
275    ) -> AuthResult<()> {
276        Ok(())
277    }
278    /// Override membership creation fields for an owner or server-side member addition.
279    async fn before_add_member(
280        &self,
281        _data: &mut OrganizationMemberDraft,
282        _event: OrganizationUser<'_>,
283    ) -> AuthResult<()> {
284        Ok(())
285    }
286    /// Observe membership creation after any requested team membership succeeds.
287    async fn after_add_member(&self, _event: OrganizationMemberEvent<'_>) -> AuthResult<()> {
288        Ok(())
289    }
290    /// Run before removing a member. Leaving an organization does not invoke this hook.
291    async fn before_remove_member(&self, _event: OrganizationMemberEvent<'_>) -> AuthResult<()> {
292        Ok(())
293    }
294    /// Run after removing the member and clearing the applicable active organization.
295    async fn after_remove_member(&self, _event: OrganizationMemberEvent<'_>) -> AuthResult<()> {
296        Ok(())
297    }
298    /// Override the new role after validating the requested role and owner constraints.
299    async fn before_update_member_role(
300        &self,
301        _role: &mut String,
302        _event: OrganizationMemberEvent<'_>,
303    ) -> AuthResult<()> {
304        Ok(())
305    }
306    /// Observe the updated membership and the role it replaced.
307    async fn after_update_member_role(
308        &self,
309        _previous_role: &str,
310        _event: OrganizationMemberEvent<'_>,
311    ) -> AuthResult<()> {
312        Ok(())
313    }
314    /// Override invitation creation fields after permission and capacity checks.
315    async fn before_create_invitation(
316        &self,
317        _data: &mut OrganizationInvitationDraft,
318        _event: OrganizationUser<'_>,
319    ) -> AuthResult<()> {
320        Ok(())
321    }
322    /// Run after persisting a new invitation and delivering its email. Resends skip this hook.
323    async fn after_create_invitation(
324        &self,
325        _event: OrganizationInvitationEvent<'_>,
326    ) -> AuthResult<()> {
327        Ok(())
328    }
329    /// Run after checking recipient membership capacity and before accepting the invitation.
330    async fn before_accept_invitation(
331        &self,
332        _event: OrganizationInvitationEvent<'_>,
333    ) -> AuthResult<()> {
334        Ok(())
335    }
336    /// Observe the accepted invitation and its new organization membership.
337    async fn after_accept_invitation(
338        &self,
339        _member: &Member,
340        _event: OrganizationInvitationEvent<'_>,
341    ) -> AuthResult<()> {
342        Ok(())
343    }
344    /// Run before changing a recipient's invitation to rejected.
345    async fn before_reject_invitation(
346        &self,
347        _event: OrganizationInvitationEvent<'_>,
348    ) -> AuthResult<()> {
349        Ok(())
350    }
351    /// Observe the rejected invitation.
352    async fn after_reject_invitation(
353        &self,
354        _event: OrganizationInvitationEvent<'_>,
355    ) -> AuthResult<()> {
356        Ok(())
357    }
358    /// Run before cancellation; the event user is the cancelling user.
359    async fn before_cancel_invitation(
360        &self,
361        _event: OrganizationInvitationEvent<'_>,
362    ) -> AuthResult<()> {
363        Ok(())
364    }
365    /// Observe the cancelled invitation.
366    async fn after_cancel_invitation(
367        &self,
368        _event: OrganizationInvitationEvent<'_>,
369    ) -> AuthResult<()> {
370        Ok(())
371    }
372    /// Override new team fields, including the default team during organization creation.
373    async fn before_create_team(
374        &self,
375        _data: &mut OrganizationTeamDraft,
376        _organization: &OrganizationResponse,
377        _user: Option<&UserView>,
378    ) -> AuthResult<()> {
379        Ok(())
380    }
381    /// Observe a persisted team after default-team membership, when applicable.
382    async fn after_create_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
383        Ok(())
384    }
385    /// Modify or replace the update after permission checks. Team IDs remain immutable.
386    async fn before_update_team(
387        &self,
388        _updates: &mut UpdateTeam,
389        _event: OrganizationTeamEvent<'_>,
390    ) -> AuthResult<()> {
391        Ok(())
392    }
393    /// Observe the persisted team update.
394    async fn after_update_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
395        Ok(())
396    }
397    /// Run before deleting team membership and invitation references.
398    async fn before_delete_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
399        Ok(())
400    }
401    /// Run after team deletion commits.
402    async fn after_delete_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
403        Ok(())
404    }
405    /// Run before adding a team member. Upstream ignores this hook's data overrides.
406    async fn before_add_team_member(
407        &self,
408        _target: OrganizationTeamMemberTarget<'_>,
409    ) -> AuthResult<()> {
410        Ok(())
411    }
412    /// Observe the existing or newly created team membership.
413    async fn after_add_team_member(
414        &self,
415        _member: &TeamMember,
416        _target: OrganizationTeamMemberTarget<'_>,
417    ) -> AuthResult<()> {
418        Ok(())
419    }
420    /// Run before removing the target team membership.
421    async fn before_remove_team_member(
422        &self,
423        _member: &TeamMember,
424        _target: OrganizationTeamMemberTarget<'_>,
425    ) -> AuthResult<()> {
426        Ok(())
427    }
428    /// Run after removing the target team membership.
429    async fn after_remove_team_member(
430        &self,
431        _member: &TeamMember,
432        _target: OrganizationTeamMemberTarget<'_>,
433    ) -> AuthResult<()> {
434        Ok(())
435    }
436}
437
438/// Authenticated session supplied to dynamic team limits.
439#[derive(Clone, Copy)]
440pub struct OrganizationSession<'a> {
441    /// Authenticated user, which may differ from the member being added.
442    pub user: &'a UserView,
443    /// Current authenticated session.
444    pub session: &'a SessionView,
445}
446
447/// Context for the maximum-team callback.
448#[derive(Clone, Copy)]
449pub struct OrganizationTeamLimit<'a> {
450    /// Organization whose teams will be counted.
451    pub organization_id: &'a str,
452    /// Current session, absent for trusted server-side team creation.
453    pub session: Option<OrganizationSession<'a>>,
454}
455
456/// Context for atomic team membership capacity checks.
457#[derive(Clone, Copy)]
458pub struct OrganizationTeamMemberLimit<'a> {
459    /// Organization containing the destination team.
460    pub organization_id: &'a str,
461    /// Destination team identifier.
462    pub team_id: &'a str,
463    /// Authenticated actor's session.
464    pub session: OrganizationSession<'a>,
465}
466
467/// Asynchronous configuration callbacks. `None` keeps the configured static value.
468#[async_trait]
469pub trait OrganizationPolicy: Send + Sync {
470    /// Decide whether this user may create an organization.
471    async fn allow_user_to_create_organization(
472        &self,
473        _user: &UserView,
474    ) -> AuthResult<Option<bool>> {
475        Ok(None)
476    }
477    /// Return true when the user's organization limit has been reached.
478    async fn organization_limit_reached(&self, _user: &UserView) -> AuthResult<Option<bool>> {
479        Ok(None)
480    }
481    /// Resolve pending invitation capacity for the authenticated inviter.
482    async fn invitation_limit(
483        &self,
484        _event: OrganizationMemberEvent<'_>,
485        _ctx: OrganizationEndpoint<'_>,
486    ) -> AuthResult<Option<usize>> {
487        Ok(None)
488    }
489    /// Resolve the maximum number of teams before team creation.
490    async fn maximum_teams(
491        &self,
492        _data: OrganizationTeamLimit<'_>,
493        _ctx: OrganizationEndpoint<'_>,
494    ) -> AuthResult<Option<usize>> {
495        Ok(None)
496    }
497    /// Resolve the maximum number of stored dynamic roles.
498    async fn maximum_roles_per_organization(
499        &self,
500        _organization_id: &str,
501    ) -> AuthResult<Option<usize>> {
502        Ok(None)
503    }
504    /// Create and persist a default team, or return `None` for normal creation.
505    async fn create_default_team(
506        &self,
507        _organization: &OrganizationResponse,
508        _ctx: OrganizationEndpoint<'_>,
509    ) -> AuthResult<Option<Team>> {
510        Ok(None)
511    }
512}
513
514/// Dynamic team capacity requires an authenticated actor session.
515#[async_trait]
516pub trait TeamMemberLimitPolicy: Send + Sync {
517    async fn maximum_members_per_team(
518        &self,
519        data: OrganizationTeamMemberLimit<'_>,
520    ) -> AuthResult<usize>;
521}
522
523/// Dynamic organization capacity. Listing endpoints still use the upstream default page size.
524#[async_trait]
525pub trait MembershipLimitPolicy: Send + Sync {
526    async fn membership_limit(&self, data: OrganizationUser<'_>) -> AuthResult<usize>;
527}