# modules/orgs

```go
import "apistock.dev/modules/orgs"
```

Package orgs holds the building blocks for organisations in multi-tenant apps (ADR-0023, ADR-0048): organisation IDs, the membership check every organisation operation starts with, and invitation emails. The generated app owns the flows, tables and SQL in internal/modules/orgs.

Stability: pre-1.0 (ADR-0015).

## Constants

### const RoleOwner, …

```go
const (
	RoleOwner  = "owner"
	RoleAdmin  = "admin"
	RoleMember = "member"
)
```

The organisation roles every app declares in its org catalog. Apps may add their own. Role names are public API.

## Variables

### var ErrOrgNotFound, …

```go
var (
	// ErrOrgNotFound reports an organisation that doesn't exist, is deleted,
	// or that the caller isn't a member of. The three look the same, so
	// organisation IDs can't be probed.
	ErrOrgNotFound = errors.New("orgs: organisation not found")
	// ErrNotMember is returned by [Memberships] when the user isn't a
	// member of a live organisation. [RequireMember] turns it into
	// [ErrOrgNotFound].
	ErrNotMember = errors.New("orgs: not a member")
	// ErrInvalidID reports a string that isn't an organisation ID.
	ErrInvalidID = errors.New("orgs: invalid organisation ID")
)
```

Errors. Check them with [errors.Is](https://pkg.go.dev/errors#Is).

## Types

### type Emails

```go
type Emails interface {
	// SendInvitation invites to to join an organisation.
	SendInvitation(ctx context.Context, to string, inv Invitation) error
}
```

Emails sends the emails organisations need. Apps use [NewMailEmails](#NewMailEmails) or their own templates. Implementations should queue rather than deliver inline (jobs.AsyncSender), so requests don't wait for the provider.

#### func NewMailEmails

```go
func NewMailEmails(sender mail.Sender, appName string) Emails
```

NewMailEmails returns plain, readable emails sent through sender, which sets the From address (mail.WithDefaults). appName appears in subjects and bodies.

### type ID

```go
type ID string
```

ID identifies an organisation, such as org\_2x7…. It is a distinct type so a user ID can't be passed where an organisation ID is expected.

#### func NewID

```go
func NewID() ID
```

NewID returns a random organisation ID with 128 bits of randomness.

#### func ParseID

```go
func ParseID(s string) (ID, error)
```

ParseID returns s as an ID, or [ErrInvalidID](#ErrInvalidID) when s isn't shaped like one. It doesn't check that the organisation exists.

#### func (ID) String

```go
func (id ID) String() string
```

String returns the ID as stored.

### type Invitation

```go
type Invitation struct {
	// OrgName is the organisation's name and InvitedBy who sent the
	// invitation (a name or email address).
	OrgName   string
	InvitedBy string
	// Role is the role the invited person gets.
	Role string
	// URL opens the invitation in the app's frontend; it carries the token.
	URL string
	// ExpiresIn is how long the invitation stays valid.
	ExpiresIn time.Duration
}
```

Invitation is what an invitation email says.

### type Member

```go
type Member struct {
	OrgID  ID
	UserID string
	Role   string
}
```

Member is a user's membership in an organisation.

#### func RequireMember

```go
func RequireMember(ctx context.Context, m Memberships, catalog *auth.Catalog, orgID ID, permission string) (context.Context, Member, error)
```

RequireMember checks that the signed-in user is a member of orgID whose role grants permission, and returns a context whose actor acts in that organisation: OrgID is set and Permissions are the role's permissions in catalog, replacing platform permissions for the operation. Every organisation operation calls it first (ADR-0048).

It returns [actor.ErrUnauthenticated](/reference/actor/#ErrUnauthenticated) without a signed-in user, [ErrOrgNotFound](#ErrOrgNotFound) when orgID isn't one of the user's live organisations, [actor.ErrStepUpRequired](/reference/actor/#ErrStepUpRequired) when the role grants permission only to sessions verified with a second factor, and [actor.ErrForbidden](/reference/actor/#ErrForbidden) when it doesn't grant it.

The membership is read on every call, so removing a member takes effect on their next request.

### type Memberships

```go
type Memberships interface {
	// MemberRole returns userID's role in orgID, or [ErrNotMember] when the
	// user isn't a member or the organisation is deleted.
	MemberRole(ctx context.Context, orgID ID, userID string) (string, error)
}
```

Memberships reads memberships. The app's orgs repository implements it.

