Skip to content

Company

Purpose

The tenant unit of the platform. Every domain row in every other context (catalog, passes, customers, billing, payments, notifications) is scoped to a Company. A Company comes into existence the moment a founder finishes the onboarding flow; deletion cascades through all Postgres-level relations in the companies schema and requires application-level cleanup for cross-schema references.

Identity & key fields

  • Primary key: id (uuid, default gen_random_uuid()).
  • name, email, specialization (text, NOT NULL).
  • ownerId (nullable uuid) — references companyMembers.id logically; nullable to allow the bootstrap flow Create Company → Create Member → Update Owner (per schema comment). No DB-level FK declared.
  • logoUrl (nullable text, default NULL).
  • type (enum company_type: SELF_EMPLOYED, COMPANY, default COMPANY).

type distinguishes a self-employed individual (SELF_EMPLOYED) from a multi-person company (COMPANY) — drives onboarding copy and possibly billing tiers (verify in product). ownerId logically points at a Company member but is nullable in the schema purely to allow the transactional creation flow.

Invariants

  • name, email, specialization NOT NULL (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/companies.schema.ts).
  • ownerId is nullable and has no DB-level FK — application is responsible for the logical owner relationship (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/companies.schema.ts).

Business invariants:

  • One row per tenant. No merge / split operation exists.
  • ownerId is nullable in the schema only to resolve the create-time chicken-and-egg between Company and the founding Company member. In steady state it always points at one member row — CompaniesService.create sets it within the same transaction (libs/features/companies/src/lib/services/companies.service.ts).
  • Creating a Company is transactional with three other inserts: one companyMember (role OWNER) plus one companySubscription (plan=free, status=trialing). All four succeed together or roll back together.
  • Deletion cascades within the companies Postgres schema (companyMember, companyCustomer, companyInvitation, companyWhitelabelApp, companySubscription, subscriptionPayment). Cross-schema references (activities, locations, passes, etc.) do not cascade — application is responsible for orphan cleanup, see ADR cross-schema-references-without-fk.

Lifecycle

No status column on this row — operational state lives on companySubscription.

  • Create: transactional 4-step flow in CompaniesService.create — insert Company (with ownerId=NULL), insert OWNER member, update Company.ownerId, insert companySubscription (free/trialing).
  • Update: name, email, logoUrl, specialization, type — via PATCH /api/business/companies/{id} from the settings UI.
  • Delete: hard delete. Cascades within the companies Postgres schema; cross-schema dependants (activities, passes, etc.) require app-level cleanup.

Relationships

  • Company member (ENT-019) — referenced by companyMembers.companyId (NOT NULL, on-delete cascade). 1:N. Owner is one of these members (via the nullable ownerId back-reference).
  • Company customer (ENT-017) — referenced by companyCustomers.companyId (NOT NULL, on-delete cascade). 1:N.
  • Company invitation (ENT-018) — referenced by companyInvitations.companyId (NOT NULL, on-delete cascade). 1:N.
  • Company whitelabel app (ENT-020) — 1:N, on-delete cascade.
  • Company subscription (ENT-001) — 1:1 via companySubscriptions.companyId (UNIQUE, on-delete cascade).

API surfaces

SurfaceExposedNotes
clientyes — /companies/{id}, /companies/{companyId}/me (CompanyResponseDto, CompanyBriefDto, CompanyIdParam)Swagger UI
businessyes — managed via libs/features/companies/src/lib/companies-admin.module.tsSwagger UI
super-adminno

Known gotchas / open questions

  • ownerId is nullable by design to break the create-time chicken-and-egg between Company and CompanyMember. No DB FK exists on this column.
  • Client-surface reads use a narrowed projection (findOneForClient in companies.service.ts) that hides email and ownerId — per ADR web-mobile-logic-port Decision 1 (Field-visibility rule for GET /companies/{id}).
  • The OpenAPI schemas for “Company” on the business surface are not visible in the audit-time business.openapi.yaml extract — confirm with libs/features/companies/src/lib/companies-admin.module.ts.
  • Company deletion today is hard and destructive. There is no soft-delete or isActive flag at this level.

Recommendations

Forward-looking improvements suggested while filling this doc — not currently in place.

  • DB trigger or service-level guard that prevents ownerId IS NULL post-creation (today the schema allows it indefinitely; only the initial transaction is permitted to leave it NULL temporarily).
  • Soft-delete / archive workflow for companies. Hard delete is destructive across many cascading tables and has no recovery path.
  • Document CompanyTypeEnum values (SELF_EMPLOYED, COMPANY) — what differs in product / billing / UI between the two types.
  • Slug / canonical URL identifier for companies. Today routes use uuid — a kebab-slug would be friendlier for shareable links and SEO.
  • Cron / monitoring for data drift — detect companies with ownerId IS NULL or with no OWNER companyMember (the two should always be in sync, but nothing currently enforces it).