Organizations Domain
The organizations domain provides multi-tenant workspace isolation for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.
Install
Section titled “Install”npx @backcap/cli add organizationsDomain Model
Section titled “Domain Model”Organization Entity
Section titled “Organization Entity”The Organization entity is the aggregate root. It holds the org’s identity, name, slug, plan, and settings.
import { Organization } from "./domains/organizations/domain/entities/organization.entity";
const result = Organization.create({ id: crypto.randomUUID(), name: "My Team", slug: "my-team", plan: "pro", settings: { maxMembers: 50 },});
if (result.isOk()) { const org = result.unwrap(); console.log(org.name, org.slug.value, org.plan);}| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (UUID) |
name | string | Organization name (required, trimmed) |
slug | OrgSlug | Validated URL-safe slug |
plan | string | Subscription plan (default: "free") |
settings | Record<string, unknown> | Custom settings (default: {}), max 64KB serialized |
createdAt | Date | Set on creation |
updatedAt | Date | Set on creation, updated on mutation |
Organization.create() returns Result<Organization, Error>. Fails if name is empty or slug is invalid.
org.updateName(name) returns Result<Organization, Error>. org.updateSettings(settings) returns Result<Organization, Error> and rejects settings exceeding 64KB serialized size. Both return new instances (immutable).
Membership Entity
Section titled “Membership Entity”import { Membership } from "./domains/organizations/domain/entities/membership.entity";
const result = Membership.create({ id: crypto.randomUUID(), userId: "user-1", organizationId: "org-1", role: "admin",});// Result<Membership, Error>membership.changeRole(newRole) returns a new Membership instance with the updated role.
OrgSlug Value Object
Section titled “OrgSlug Value Object”Validates slugs: 3-63 characters, lowercase alphanumeric + hyphens, no leading/trailing hyphens.
import { OrgSlug } from "./domains/organizations/domain/value-objects/org-slug.vo";
const result = OrgSlug.create("my-team");// Result<OrgSlug, Error>MemberRole Value Object
Section titled “MemberRole Value Object”Valid roles: owner, admin, member, viewer.
role.isOwner()checks if the role is owner.role.isAtLeast(role)checks hierarchy: owner > admin > member > viewer.role.equals(other)compares values.
Domain Errors
Section titled “Domain Errors”| Error Class | Condition | Message |
|---|---|---|
OrgNotFound | Organization lookup failed | Organization not found with id: "<id>" |
OrgSlugTaken | Slug already in use | Organization slug already taken: "<slug>" |
MemberAlreadyExists | User already a member | User "<userId>" is already a member of organization "<orgId>" |
CannotRemoveOwner | Attempt to remove owner | Cannot remove the owner of organization "<orgId>" |
Domain Events
Section titled “Domain Events”| Event | Emitted By | Payload |
|---|---|---|
OrganizationCreated | CreateOrganization use case | organizationId, name, slug, ownerId, occurredAt |
MemberInvited | InviteMember use case | organizationId, invitedEmail, role, invitedBy, occurredAt |
MemberJoined | AcceptInvitation use case | organizationId, userId, role, occurredAt |
MemberRemoved | RemoveMember use case | organizationId, userId, removedBy, occurredAt |
Application Layer
Section titled “Application Layer”Use Cases
Section titled “Use Cases”CreateOrganization
Section titled “CreateOrganization”Creates a new organization and an owner membership for the creator.
import { CreateOrganization } from "./domains/organizations/application/use-cases/create-organization.use-case";
const createOrg = new CreateOrganization(organizationRepository, membershipRepository);
const result = await createOrg.execute({ name: "My Team", slug: "my-team", ownerId: "user-1", plan: "pro",});// Result<{ organizationId: string; event: OrganizationCreated }, Error>Possible failures: OrgSlugTaken, invalid name or slug
InviteMember
Section titled “InviteMember”Invites a user to join an organization by email.
const result = await inviteMember.execute({ organizationId: "org-1", email: "invite@example.com", role: "member", invitedBy: "user-1",});// Result<{ invitationId: string; event: MemberInvited }, Error>Possible failures: OrgNotFound, invalid role, owner role not allowed
AcceptInvitation
Section titled “AcceptInvitation”Accepts a pending invitation and creates the membership.
const result = await acceptInvitation.execute({ token: "invitation-token", userId: "user-2",});// Result<{ membershipId: string; event: MemberJoined }, Error>Possible failures: Invalid/expired token, MemberAlreadyExists
RemoveMember
Section titled “RemoveMember”Removes a member from an organization. Cannot remove the owner.
Possible failures: OrgNotFound, CannotRemoveOwner, user not a member
ListMembers
Section titled “ListMembers”Returns all members of an organization.
GetOrganization
Section titled “GetOrganization”Returns an organization by ID.
UpdateOrganization
Section titled “UpdateOrganization”Updates an organization’s name and/or settings.
Port Interfaces
Section titled “Port Interfaces”IOrganizationRepository
Section titled “IOrganizationRepository”export interface IOrganizationRepository { findById(id: string): Promise<Organization | null>; findBySlug(slug: string): Promise<Organization | null>; save(organization: Organization): Promise<void>; delete(id: string): Promise<void>;}IMembershipRepository
Section titled “IMembershipRepository”export interface IMembershipRepository { findById(id: string): Promise<Membership | null>; findByUserAndOrg(userId: string, organizationId: string): Promise<Membership | null>; findByOrganization(organizationId: string): Promise<Membership[]>; save(membership: Membership): Promise<void>; delete(id: string): Promise<void>;}IInvitationService
Section titled “IInvitationService”export interface IInvitationService { create(params: { organizationId: string; email: string; role: string; invitedBy: string }): Promise<Invitation>; findByToken(token: string): Promise<Invitation | null>; markAccepted(id: string): Promise<void>;}Public API (contracts/)
Section titled “Public API (contracts/)”import { createOrganizationService, IOrganizationService,} from "./domains/organizations/contracts";
const orgService: IOrganizationService = createOrganizationService({ organizationRepository, membershipRepository, invitationService,});
// IOrganizationService interface:// createOrganization(input): Promise<Result<{ organizationId: string }, Error>>// getOrganization(id): Promise<Result<OrgOutput, Error>>// updateOrganization(input): Promise<Result<OrgOutput, Error>>// inviteMember(input): Promise<Result<{ invitationId: string }, Error>>// acceptInvitation(input): Promise<Result<{ membershipId: string }, Error>>// removeMember(input): Promise<Result<void, Error>>// listMembers(orgId): Promise<Result<OrgMemberOutput[], Error>>File Map
Section titled “File Map”domains/organizations/ domain/ entities/organization.entity.ts entities/membership.entity.ts value-objects/org-slug.vo.ts value-objects/member-role.vo.ts errors/org-not-found.error.ts errors/org-slug-taken.error.ts errors/member-already-exists.error.ts errors/cannot-remove-owner.error.ts events/organization-created.event.ts events/member-invited.event.ts events/member-joined.event.ts events/member-removed.event.ts application/ use-cases/create-organization.use-case.ts use-cases/invite-member.use-case.ts use-cases/accept-invitation.use-case.ts use-cases/remove-member.use-case.ts use-cases/list-members.use-case.ts use-cases/get-organization.use-case.ts use-cases/update-organization.use-case.ts ports/organization-repository.port.ts ports/membership-repository.port.ts ports/invitation-service.port.ts dto/create-organization-input.dto.ts dto/invite-member-input.dto.ts dto/accept-invitation-input.dto.ts dto/remove-member-input.dto.ts dto/update-organization-input.dto.ts contracts/ organizations.contract.ts organizations.factory.ts index.ts shared/ result.ts