Skip to content

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.

Terminal window
npx @backcap/cli add organizations

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);
}
FieldTypeDescription
idstringUnique identifier (UUID)
namestringOrganization name (required, trimmed)
slugOrgSlugValidated URL-safe slug
planstringSubscription plan (default: "free")
settingsRecord<string, unknown>Custom settings (default: {}), max 64KB serialized
createdAtDateSet on creation
updatedAtDateSet 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).

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.

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>

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.
Error ClassConditionMessage
OrgNotFoundOrganization lookup failedOrganization not found with id: "<id>"
OrgSlugTakenSlug already in useOrganization slug already taken: "<slug>"
MemberAlreadyExistsUser already a memberUser "<userId>" is already a member of organization "<orgId>"
CannotRemoveOwnerAttempt to remove ownerCannot remove the owner of organization "<orgId>"
EventEmitted ByPayload
OrganizationCreatedCreateOrganization use caseorganizationId, name, slug, ownerId, occurredAt
MemberInvitedInviteMember use caseorganizationId, invitedEmail, role, invitedBy, occurredAt
MemberJoinedAcceptInvitation use caseorganizationId, userId, role, occurredAt
MemberRemovedRemoveMember use caseorganizationId, userId, removedBy, occurredAt

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

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

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

Removes a member from an organization. Cannot remove the owner.

Possible failures: OrgNotFound, CannotRemoveOwner, user not a member

Returns all members of an organization.

Returns an organization by ID.

Updates an organization’s name and/or settings.

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>;
}
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>;
}
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>;
}
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>>
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