Help us shape the future of Go! Take the 2026 Go Developer Survey

organization

package
v1.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 22 Imported by: 0

README

organization

Multi-tenancy: organisations, members, roles, invitations and teams.

Without this plugin registered, none of that exists. The core engine knows about users and sessions and stops there. Add this and you get the whole B2B shape, where a person belongs to one or more organisations, holds a role in each, and can be invited into one by email.

Teams sit inside organisations for finer-grained grouping, and the plugin contributes its own dashboard pages and admin routes.

Use it when

  • You are selling to companies, so the account that pays you is not the person who logs in
  • Permissions need to be scoped per tenant, where the same user is an admin in one org and a viewer in another
  • You need invitation flows with accept and decline

Skip it when

  • Every user is an island. Consumer products rarely need this and it complicates every query you write
  • You have exactly one tenant, which is most internal tools
  • You want organisations later. Adding them after launch means backfilling ownership onto everything you already store

Wiring

authsome.WithPlugin(organization.New()),

Mount the routes somewhere other than the engine base path:

authsome.WithPlugin(organization.New(organization.Config{
    PathPrefix: "/v1/tenants",
})),

Config

Field Type Default What it does
PathPrefix string engine BasePath Where organisation routes are mounted
InvitationTTL time.Duration 7 days How long an invitation can be accepted. Accepting a lapsed one answers 410. Set it negative and invitations never expire

Endpoints

Method Path Purpose
GET /orgs List the caller's organisations
POST /orgs Create an organisation
GET /orgs/check-slug Check slug availability before creating
GET /orgs/:orgId Read an organisation
PATCH /orgs/:orgId Update an organisation
DELETE /orgs/:orgId Delete an organisation
GET /orgs/:orgId/members List members
POST /orgs/:orgId/members Add a member
PATCH /orgs/:orgId/members/:memberId Change a member's role
DELETE /orgs/:orgId/members/:memberId Remove a member
GET /orgs/:orgId/invitations List pending invitations
POST /orgs/:orgId/invitations Invite someone by email
POST /orgs/invitations/accept Accept an invitation
POST /orgs/invitations/decline Decline an invitation
GET /orgs/:orgId/teams List teams
POST /orgs/:orgId/teams Create a team
GET /orgs/:orgId/teams/:teamId Read a team
PATCH /orgs/:orgId/teams/:teamId Update a team
DELETE /orgs/:orgId/teams/:teamId Delete a team

An admin variant of the list endpoint is mounted under {PathPrefix}/admin.

Lifecycle hooks

None on the sign-in path. It registers a DataExportContributor, so a user's organisation memberships land in their GDPR export.

sso and scim scope their connections to an organisation. subscription can bill per organisation and auto-subscribe one on creation. consent covers the other half of the GDPR story.

Documentation

Overview

contract.go: Wires the organization plugin into the forge-dashboard contract surface via plugin.ContractContributor.

Package organization provides an opt-in organization management plugin for AuthSome.

When registered, this plugin contributes organization CRUD, member management, invitation workflows, team management, dashboard pages, and admin routes. Without this plugin, no organization functionality exists.

Index

Constants

View Source
const DefaultInvitationTTL = 7 * 24 * time.Hour

DefaultInvitationTTL is how long an invitation stays acceptable when Config.InvitationTTL is left at zero.

Variables

View Source
var ErrInvitationEmailMismatch = errors.New("organization: invitation was sent to a different email address")

ErrInvitationEmailMismatch is returned by AcceptInvitationAs when the signed-in user does not own the email the invitation was sent to. The HTTP layer answers it with 403 Forbidden.

View Source
var ErrInvitationExpired = errors.New("organization: invitation has expired")

ErrInvitationExpired is returned when an invitation is accepted after its ExpiresAt. The HTTP layer answers it with 410 Gone.

View Source
var ErrSlugTaken = errors.New("organization: slug already taken")

ErrSlugTaken is returned when an organization is created with a slug that another organization in the same app already holds. The HTTP layer answers it with 409 Conflict.

Functions

This section is empty.

Types

type AcceptInvitationRequest added in v0.0.7

type AcceptInvitationRequest struct {
	Token string `json:"token" description:"Invitation token"`
}

AcceptInvitationRequest binds the body for POST /orgs/invitations/accept.

type AddMemberRequest added in v1.9.0

type AddMemberRequest struct {
	OrgID  string `path:"orgId" description:"Organization identifier"`
	UserID string `json:"user_id" description:"User identifier to add"`
	Role   string `json:"role,omitempty" description:"Member role (owner, admin, member)"`
}

AddMemberRequest binds path + body for POST /orgs/:orgId/members.

type AdminListOrgsRequest added in v1.9.0

type AdminListOrgsRequest struct {
	AppID string `query:"app_id" description:"Application ID"`
}

AdminListOrgsRequest binds query params for GET /admin/orgs.

type CheckSlugRequest added in v1.9.0

type CheckSlugRequest struct {
	AppID string `query:"app_id" description:"Application ID"`
	Slug  string `query:"slug" description:"Slug to check"`
}

CheckSlugRequest binds query params for GET /orgs/check-slug.

type Config

type Config struct {
	// PathPrefix is the HTTP path prefix for organization routes.
	// Defaults to the engine's BasePath.
	PathPrefix string

	// InvitationTTL is how long an invitation can be accepted after it is
	// created. Zero means DefaultInvitationTTL (seven days). A negative value
	// turns expiry off, so invitations never lapse, which was the behaviour
	// before this option existed.
	InvitationTTL time.Duration
}

Config configures the organization plugin.

type CreateInvitationRequest added in v1.9.0

type CreateInvitationRequest struct {
	OrgID string `path:"orgId" description:"Organization identifier"`
	Email string `json:"email" description:"Email address to invite"`
	Role  string `json:"role,omitempty" description:"Member role (owner, admin, member)"`
}

CreateInvitationRequest binds path + body for POST /orgs/:orgId/invitations.

type CreateOrgRequest added in v1.9.0

type CreateOrgRequest struct {
	AppID string `json:"app_id,omitempty" description:"Application ID (optional, uses default)"`
	Name  string `json:"name" description:"Organization name"`
	Slug  string `json:"slug" description:"URL-safe slug"`
}

CreateOrgRequest binds the body for POST /orgs.

type CreateTeamRequest

type CreateTeamRequest struct {
	OrgID string `path:"orgId" description:"Organization identifier"`
	Name  string `json:"name" description:"Team name"`
	Slug  string `json:"slug" description:"URL-safe team slug"`
}

CreateTeamRequest binds path + body for POST /orgs/:orgId/teams.

type DeclineInvitationRequest added in v0.0.7

type DeclineInvitationRequest struct {
	Token string `json:"token" description:"Invitation token"`
}

DeclineInvitationRequest binds the body for POST /orgs/invitations/decline.

type DeleteOrgRequest added in v1.9.0

type DeleteOrgRequest struct {
	OrgID string `path:"orgId" description:"Organization identifier"`
}

DeleteOrgRequest binds the path for DELETE /orgs/:orgId.

type DeleteTeamRequest added in v0.0.7

type DeleteTeamRequest struct {
	OrgID  string `path:"orgId" description:"Organization identifier"`
	TeamID string `path:"teamId" description:"Team identifier"`
}

DeleteTeamRequest binds the path for DELETE /orgs/:orgId/teams/:teamId.

type GetOrgRequest added in v1.9.0

type GetOrgRequest struct {
	OrgID string `path:"orgId" description:"Organization identifier"`
}

GetOrgRequest binds the path for GET /orgs/:orgId.

type GetTeamRequest added in v1.9.0

type GetTeamRequest struct {
	OrgID  string `path:"orgId" description:"Organization identifier"`
	TeamID string `path:"teamId" description:"Team identifier"`
}

GetTeamRequest binds the path for GET /orgs/:orgId/teams/:teamId.

type InvitationListResponse added in v1.9.0

type InvitationListResponse struct {
	Invitations any `json:"invitations" description:"List of invitations"`
	// NextCursor is set when more remain; pass it as cursor to continue.
	NextCursor string `json:"next_cursor,omitempty" description:"Cursor for the next page, absent on the last"`
}

InvitationListResponse wraps a list of invitations.

type ListInvitationsRequest added in v1.9.0

type ListInvitationsRequest struct {
	OrgID  string `path:"orgId" description:"Organization identifier"`
	Limit  int    `query:"limit" description:"Page size (default 50, max 200)" optional:"true"`
	Cursor string `query:"cursor" description:"Cursor from the previous page's next_cursor" optional:"true"`
}

ListInvitationsRequest binds the path for GET /orgs/:orgId/invitations.

type ListMembersRequest added in v0.0.7

type ListMembersRequest struct {
	OrgID  string `path:"orgId" description:"Organization identifier"`
	Limit  int    `query:"limit" description:"Page size (default 50, max 200)" optional:"true"`
	Cursor string `query:"cursor" description:"Cursor from the previous page's next_cursor" optional:"true"`
}

ListMembersRequest binds the path for GET /orgs/:orgId/members.

type ListOrgsRequest added in v1.9.0

type ListOrgsRequest struct {
	Limit  int    `query:"limit" description:"Page size (default 50, max 200)" optional:"true"`
	Cursor string `query:"cursor" description:"Cursor from the previous page's next_cursor" optional:"true"`
}

ListOrgsRequest is an empty request for GET /orgs (user from context).

type ListTeamsRequest added in v0.0.7

type ListTeamsRequest struct {
	OrgID  string `path:"orgId" description:"Organization identifier"`
	Limit  int    `query:"limit" description:"Page size (default 50, max 200)" optional:"true"`
	Cursor string `query:"cursor" description:"Cursor from the previous page's next_cursor" optional:"true"`
}

ListTeamsRequest binds the path for GET /orgs/:orgId/teams.

type MemberListResponse added in v1.9.0

type MemberListResponse struct {
	Members any `json:"members" description:"List of members"`
	// NextCursor is set when more remain; pass it as cursor to continue.
	NextCursor string `json:"next_cursor,omitempty" description:"Cursor for the next page, absent on the last"`
}

MemberListResponse wraps a list of members.

type OrgListResponse added in v1.9.0

type OrgListResponse struct {
	Organizations any `json:"organizations" description:"List of organizations"`
	// NextCursor is set when more remain; pass it as cursor to continue.
	NextCursor string `json:"next_cursor,omitempty" description:"Cursor for the next page, absent on the last"`
}

OrgListResponse wraps a list of organizations.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the organization management plugin.

func New added in v1.9.0

func New(cfg ...Config) *Plugin

New creates a new organization plugin with optional configuration. The store is resolved automatically from the engine during OnInit.

func (*Plugin) AcceptInvitation added in v1.9.0

func (p *Plugin) AcceptInvitation(ctx context.Context, token string) (*organization.Member, error)

AcceptInvitation accepts a pending invitation by token and adds whichever account owns the invited email (primary or secondary) as a member. It does not check who is asking, so call it only from code you trust. The HTTP endpoint uses AcceptInvitationAs instead.

func (*Plugin) AcceptInvitationAs added in v1.9.0

func (p *Plugin) AcceptInvitationAs(ctx context.Context, token string, userID id.UserID) (*organization.Member, error)

AcceptInvitationAs accepts a pending invitation on behalf of userID, the signed-in caller. It succeeds only when userID owns the invited email, primary or secondary. Anyone else gets ErrInvitationEmailMismatch and the invitation stays pending for the person it was sent to.

func (*Plugin) AddMember added in v1.9.0

func (p *Plugin) AddMember(ctx context.Context, m *organization.Member) error

AddMember adds a member to an organization.

func (*Plugin) AdminListOrganizations added in v1.9.0

func (p *Plugin) AdminListOrganizations(ctx context.Context, appID id.AppID) ([]*organization.Organization, error)

AdminListOrganizations returns all organizations for the given app.

func (*Plugin) CreateInvitation added in v1.9.0

func (p *Plugin) CreateInvitation(ctx context.Context, inv *organization.Invitation) error

CreateInvitation creates an organization invitation. An invitation with no ExpiresAt gets one from Config.InvitationTTL (seven days by default).

func (*Plugin) CreateOrganization added in v1.9.0

func (p *Plugin) CreateOrganization(ctx context.Context, o *organization.Organization) error

CreateOrganization creates a new organization and adds the creator as owner.

func (*Plugin) CreateTeam added in v1.9.0

func (p *Plugin) CreateTeam(ctx context.Context, t *organization.Team) error

CreateTeam creates a new team within an organization.

func (*Plugin) DeclineInvitation added in v1.9.0

func (p *Plugin) DeclineInvitation(ctx context.Context, token string) error

DeclineInvitation declines a pending invitation by token.

func (*Plugin) DeleteOrganization added in v1.9.0

func (p *Plugin) DeleteOrganization(ctx context.Context, orgID id.OrgID) error

DeleteOrganization deletes an organization and all of its dependent records (members, teams, invitations) before emitting the AfterOrgDelete hook so other plugins (subscription, SCIM, …) can clean up their own org-scoped data.

The cascade is delegated to Store.DeleteOrganizationCascade, which each backend implements with its native transaction primitive (PgTx / SqliteTx / MongoTx) so a midway failure rolls back atomically. EmitAfterOrgDelete only fires once the cascade returns nil, so downstream plugins never see an event for an org that wasn't actually deleted.

Once the org is gone, every session its members hold that names it as the active org has that org cleared, the same as RemoveMember does for one member. A failure there is logged, not returned.

func (*Plugin) DeleteTeam added in v1.9.0

func (p *Plugin) DeleteTeam(ctx context.Context, teamID id.TeamID) error

DeleteTeam deletes a team.

func (*Plugin) ExportUserData added in v1.9.0

func (p *Plugin) ExportUserData(ctx context.Context, userID id.UserID) (label string, data any, err error)

ExportUserData returns the user's organization data for GDPR export.

func (*Plugin) GetOrganization added in v1.9.0

func (p *Plugin) GetOrganization(ctx context.Context, orgID id.OrgID) (*organization.Organization, error)

GetOrganization returns an organization by ID.

func (*Plugin) GetTeam added in v1.9.0

func (p *Plugin) GetTeam(ctx context.Context, teamID id.TeamID) (*organization.Team, error)

GetTeam returns a team by ID.

func (*Plugin) IsOrgSlugAvailable added in v1.9.0

func (p *Plugin) IsOrgSlugAvailable(ctx context.Context, appID id.AppID, slug string) (bool, error)

IsOrgSlugAvailable checks whether a slug is available for an app.

func (*Plugin) ListInvitations added in v1.9.0

func (p *Plugin) ListInvitations(ctx context.Context, orgID id.OrgID) ([]*organization.Invitation, error)

ListInvitations lists invitations for an organization.

func (*Plugin) ListInvitationsPage added in v1.9.0

func (p *Plugin) ListInvitationsPage(ctx context.Context, orgID id.OrgID, opts page.Opts) (page.Page[*organization.Invitation], error)

ListInvitationsPage is the bounded twin for the request path.

func (*Plugin) ListMembers added in v1.9.0

func (p *Plugin) ListMembers(ctx context.Context, orgID id.OrgID) ([]*organization.Member, error)

ListMembers returns all members of an organization.

func (*Plugin) ListMembersPage added in v1.9.0

func (p *Plugin) ListMembersPage(ctx context.Context, orgID id.OrgID, opts page.Opts) (page.Page[*organization.Member], error)

ListMembersPage is the bounded twin for the request path.

func (*Plugin) ListTeams added in v1.9.0

func (p *Plugin) ListTeams(ctx context.Context, orgID id.OrgID) ([]*organization.Team, error)

ListTeams returns all teams in an organization.

func (*Plugin) ListTeamsPage added in v1.9.0

func (p *Plugin) ListTeamsPage(ctx context.Context, orgID id.OrgID, opts page.Opts) (page.Page[*organization.Team], error)

ListTeamsPage is the bounded twin for the request path.

func (*Plugin) ListUserOrganizations added in v1.9.0

func (p *Plugin) ListUserOrganizations(ctx context.Context, userID id.UserID) ([]*organization.Organization, error)

ListUserOrganizations returns all organizations a user belongs to.

func (*Plugin) ListUserOrganizationsPage added in v1.9.0

func (p *Plugin) ListUserOrganizationsPage(ctx context.Context, userID id.UserID, opts page.Opts) (page.Page[*organization.Organization], error)

ListUserOrganizationsPage is the bounded twin for the request path.

func (*Plugin) Name added in v1.9.0

func (p *Plugin) Name() string

Name returns the plugin name.

func (*Plugin) OnInit added in v1.9.0

func (p *Plugin) OnInit(_ context.Context, engine plugin.Engine) error

OnInit captures engine capabilities for use by the plugin's service layer.

func (*Plugin) RegisterContract added in v1.9.0

func (p *Plugin) RegisterContract(
	d *dispatcher.Dispatcher,
	reg contract.Registry,
	wreg contract.WardenRegistry,
	engine plugin.Engine,
) error

func (*Plugin) RegisterRoutes

func (p *Plugin) RegisterRoutes(router forge.Router) error

RegisterRoutes registers organization management routes on a forge.Router.

func (*Plugin) RemoveMember added in v1.9.0

func (p *Plugin) RemoveMember(ctx context.Context, memberID id.MemberID) error

RemoveMember removes a member from an organization.

func (*Plugin) SetStore added in v1.9.0

func (p *Plugin) SetStore(s store.Store)

SetStore allows direct store injection for testing.

func (*Plugin) UpdateMemberRole added in v1.9.0

func (p *Plugin) UpdateMemberRole(ctx context.Context, memberID id.MemberID, role organization.MemberRole) (*organization.Member, error)

UpdateMemberRole updates a member's role within an organization.

func (*Plugin) UpdateOrganization added in v1.9.0

func (p *Plugin) UpdateOrganization(ctx context.Context, o *organization.Organization) error

UpdateOrganization updates an existing organization.

func (*Plugin) UpdateTeam added in v1.9.0

func (p *Plugin) UpdateTeam(ctx context.Context, t *organization.Team) error

UpdateTeam updates an existing team.

type RemoveMemberRequest added in v0.0.7

type RemoveMemberRequest struct {
	OrgID    string `path:"orgId" description:"Organization identifier"`
	MemberID string `path:"memberId" description:"Member identifier"`
}

RemoveMemberRequest binds the path for DELETE /orgs/:orgId/members/:memberId.

type SlugAvailableResponse added in v1.9.0

type SlugAvailableResponse struct {
	Available bool `json:"available" description:"Whether the slug is available"`
}

SlugAvailableResponse reports whether a slug is available.

type StatusResponse

type StatusResponse struct {
	Status string `json:"status" description:"Operation status"`
}

StatusResponse is a generic status response.

type TeamListResponse added in v1.9.0

type TeamListResponse struct {
	Teams any `json:"teams" description:"List of teams"`
	// NextCursor is set when more remain; pass it as cursor to continue.
	NextCursor string `json:"next_cursor,omitempty" description:"Cursor for the next page, absent on the last"`
}

TeamListResponse wraps a list of teams.

type UpdateMemberRequest

type UpdateMemberRequest struct {
	OrgID    string `path:"orgId" description:"Organization identifier"`
	MemberID string `path:"memberId" description:"Member identifier"`
	Role     string `json:"role" description:"New member role (owner, admin, member)"`
}

UpdateMemberRequest binds path + body for PATCH /orgs/:orgId/members/:memberId.

type UpdateOrgRequest added in v1.9.0

type UpdateOrgRequest struct {
	OrgID string  `path:"orgId" description:"Organization identifier"`
	Name  *string `json:"name,omitempty" description:"Organization name"`
}

UpdateOrgRequest binds path + body for PATCH /orgs/:orgId.

type UpdateTeamRequest

type UpdateTeamRequest struct {
	OrgID  string  `path:"orgId" description:"Organization identifier"`
	TeamID string  `path:"teamId" description:"Team identifier"`
	Name   *string `json:"name,omitempty" description:"Team name"`
	Slug   *string `json:"slug,omitempty" description:"URL-safe team slug"`
}

UpdateTeamRequest binds path + body for PATCH /orgs/:orgId/teams/:teamId.

Directories

Path Synopsis
Package contract wires the organization plugin's intent surface into the forge-dashboard contract registry.
Package contract wires the organization plugin's intent surface into the forge-dashboard contract registry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL