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, defaultgen_random_uuid()). name,email,specialization(text, NOT NULL).ownerId(nullable uuid) — referencescompanyMembers.idlogically; nullable to allow the bootstrap flow Create Company → Create Member → Update Owner (per schema comment). No DB-level FK declared.logoUrl(nullable text, defaultNULL).type(enumcompany_type:SELF_EMPLOYED,COMPANY, defaultCOMPANY).
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,specializationNOT NULL (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/companies.schema.ts).ownerIdis 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.
ownerIdis 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.createsets 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(roleOWNER) plus onecompanySubscription(plan=free,status=trialing). All four succeed together or roll back together. - Deletion cascades within the
companiesPostgres 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 (withownerId=NULL), insert OWNER member, updateCompany.ownerId, insertcompanySubscription(free/trialing). - Update:
name,email,logoUrl,specialization,type— viaPATCH /api/business/companies/{id}from the settings UI. - Delete: hard delete. Cascades within the
companiesPostgres 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 nullableownerIdback-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
| Surface | Exposed | Notes |
|---|---|---|
| client | yes — /companies/{id}, /companies/{companyId}/me (CompanyResponseDto, CompanyBriefDto, CompanyIdParam) | Swagger UI |
| business | yes — managed via libs/features/companies/src/lib/companies-admin.module.ts | Swagger UI |
| super-admin | no | — |
Known gotchas / open questions
ownerIdis 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 (
findOneForClientincompanies.service.ts) that hidesemailandownerId— per ADRweb-mobile-logic-portDecision 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.yamlextract — confirm withlibs/features/companies/src/lib/companies-admin.module.ts. - Company deletion today is hard and destructive. There is no soft-delete or
isActiveflag 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 NULLpost-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
CompanyTypeEnumvalues (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 NULLor with no OWNERcompanyMember(the two should always be in sync, but nothing currently enforces it).