127 KiB
Professional Management Platform
Full REST-First System Design Plan
Revision: v4.1 — Consistency and Implementation-Contract Cleanup
Status: Locked broad architecture baseline with implementation-blocking contradictions resolved. Subsequent detail belongs in ADRs, OpenAPI, migrations, domain specifications, and backlog items.
Primary vertical: Engineering
API style: REST + JSON + OpenAPI 3.1
Backend style: Modular monolith
Data: PostgreSQL + profession-specific tables
Tenant model: Organization-scoped, explicit tenant context
v4.1 Cleanup Notes
v4.1 does not redesign the platform. It resolves contradictions and fills implementation contracts discovered during detailed review.
Resolved:
- raw UUIDv7 is now the serialized/API identifier format
- database columns remain UUID; prefixed strings are not public IDs
- all tenant-owned subresources carry direct
organization_id - design-version document cardinality is explicit and relational
- engineering time entries can be attributed to a specific work item
- time-entry work-item links are constrained to the time entry's project
- engineering specifications use explicit many-document link records
- project-level
budget_minoris removed in favor of the dedicated budget model - duplicate
legal_documentsownership is removed - legal matter/case document-link schemas are defined
- batch custom-action paths use one documented convention
- membership invitations can pre-assign multiple roles
- missing design
revisecommand is added - inspection follow-ups now have a table and lifecycle
- change requests are moved out of the initial Engineering schema until specified
- service accounts and API keys are defined for machine access
- service-account role assignments have an explicit relational schema
- design assignments, portal review requests, inspection documents, payments, and refunds have explicit schemas
- shared billing accounts replace vague or polymorphic invoice references
- resource status and type vocabularies required by migrations/OpenAPI are defined
- API JSON, query parameters, and path parameter names use camelCase; database columns use snake_case
- professional profiles support multiple professional credentials
- deletion/archival/revocation/unlink behavior is globally defined
- invoice-item fields are specified
- deferred healthcare placeholder tables receive minimum schemas or explicit deferral notes
- document upload and multipart DTOs are defined
- webhook subscription fields are defined
POST /auth/logoutis restored- portal review-request and portal capability schemas are defined
- project-role, task-status, priority, and inspection-outcome values are defined
- document-category uniqueness has a version-independent fallback
- retention policy fields are defined
- retention-period null semantics are explicit
- pagination defaults are explicit
assignedauthorization scope has resource-specific resolution rules- feature-flag behavior is clarified
- audit privacy minimization/anonymization strategy is documented
- outbox correlation and causation IDs are added
- webhook delivery event references and job reference envelopes are defined
- CORS and web-security configuration is moved into a required ADR
- project
stageduplication is removed; project phases remain authoritative - phase reordering, site listing, appointment locations, and encounter reason fields are clarified
- jobs remain tenant-scoped through the standard organization header rather than path nesting
- job types use a registered handler-and-schema vocabulary
2. Core Architecture Decision
The platform will use:
- REST
- JSON
- OpenAPI
- Versioned endpoints
- PostgreSQL
- Modular monolith backend
- Profession-specific frontends
- Profession-specific database tables
- Shared identity, security, billing, documents, audit, and infrastructure
Base API path:
/api/v1
GraphQL is not part of v1.
3. High-Level Architecture
FRONTENDS
┌──────────────────┼──────────────────┐
│ │ │
Engineering Web Legal Web Healthcare Web
│ │ │
└──────────────────┼──────────────────┘
│
▼
REST API
/api/v1
│
┌───────────┼───────────┐
│ │ │
Core Engineering Legal
│ │ │
│ Healthcare │
│ │ │
└───────────┼───────────┘
│
PostgreSQL
│
┌───────────────┼────────────────┐
│ │ │
Shared Tables Profession Tables Audit/Event Tables
Shared infrastructure:
PostgreSQL
Redis
Object Storage
Queue / Workers
Audit
Notifications
Billing
Observability
4. System Architecture Strategy
Start with a modular monolith.
Do not start with microservices.
Initial deployment:
Frontend Apps
│
▼
Backend API
│
├── PostgreSQL
├── Redis
├── Object Storage
└── Worker Queue
Benefits:
- simpler transactions
- easier development
- easier deployment
- clearer domain boundaries
- lower operational burden
- easier refactoring
- future service extraction remains possible
5. Repository Structure
Recommended monorepo:
professional-platform/
│
├── apps/
│ ├── engineering-web/
│ ├── legal-web/
│ ├── healthcare-web/
│ ├── platform-admin/
│ ├── api/
│ └── workers/
│
├── packages/
│ ├── ui/
│ ├── api-client/
│ ├── auth-client/
│ ├── validation/
│ ├── types/
│ ├── config/
│ └── testing/
│
├── database/
│ ├── migrations/
│ ├── seeds/
│ └── scripts/
│
├── infrastructure/
│ ├── docker/
│ ├── deployment/
│ └── monitoring/
│
└── docs/
├── architecture/
├── api/
├── security/
└── domains/
6. Frontend Strategy
Every profession receives its own frontend application.
Avoid one giant frontend filled with profession checks.
Engineering Frontend
Suggested navigation:
Dashboard
Clients
Projects
Project Phases
Project Team
Sites
Designs
Design Reviews
Inspections
Specifications
Tasks
Documents
Timesheets
Billing
Reports
Administration
Legal Frontend
Suggested navigation:
Dashboard
Clients
Matters
Cases
Hearings
Courts
Deadlines
Documents
Conflict Checks
Time Tracking
Retainers
Billing
Reports
Administration
Healthcare Frontend
Suggested navigation:
Dashboard
Patients
Appointments
Practitioners
Encounters
Clinical Records
Diagnoses
Prescriptions
Insurance
Documents
Billing
Reports
Administration
Platform Admin Frontend
Suggested functions:
Organizations
Users
Profession Modules
Subscriptions
System Health
Audit
Support
Global Configuration
Platform administrators and organization administrators are separate concepts.
7. REST API Structure
Shared endpoints:
/api/v1/auth
/api/v1/me
/api/v1/organizations
/api/v1/memberships
/api/v1/membership-invitations
/api/v1/roles
/api/v1/permissions
/api/v1/documents
/api/v1/invoices
/api/v1/payments
/api/v1/audit-events
Engineering:
/api/v1/engineering/clients
/api/v1/engineering/projects
/api/v1/engineering/project-members
/api/v1/engineering/phases
/api/v1/engineering/sites
/api/v1/engineering/tasks
/api/v1/engineering/designs
/api/v1/engineering/inspections
/api/v1/engineering/specifications
/api/v1/engineering/time-entries
Legal:
/api/v1/legal/clients
/api/v1/legal/matters
/api/v1/legal/cases
/api/v1/legal/hearings
/api/v1/legal/deadlines
/api/v1/legal/conflict-checks
/api/v1/legal/retainers
/api/v1/legal/time-entries
Healthcare:
/api/v1/healthcare/patients
/api/v1/healthcare/practitioners
/api/v1/healthcare/appointments
/api/v1/healthcare/encounters
/api/v1/healthcare/clinical-records
/api/v1/healthcare/diagnoses
/api/v1/healthcare/prescriptions
/api/v1/healthcare/insurance
8. REST Conventions
All APIs use JSON over HTTPS.
Typical tenant-scoped request:
Authorization: Bearer <access-token>
X-Organization-Id: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c1d
X-Request-Id: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
Content-Type: application/json
API Naming Convention
Public API representation:
JSON properties: camelCase
query parameters: camelCase
path parameter names in documentation: camelCase
HTTP headers: conventional HTTP header casing
Database representation:
table names: snake_case
column names: snake_case
constraint/index names: snake_case
Example:
GET /api/v1/engineering/tasks?assignedToUserId=<uuid>&createdAfter=2026-08-01T00:00:00Z
{
"assignedToUserId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c3d",
"createdAt": "2026-08-26T12:00:00Z"
}
maps internally to columns such as:
assigned_to_user_id
created_at
Organization Context
X-Organization-Id is mandatory for every tenant-scoped endpoint.
Global endpoints such as these do not require tenant context:
POST /api/v1/auth/login
POST /api/v1/auth/token/refresh
GET /api/v1/me
GET /api/v1/me/organizations
GET /api/v1/auth/sessions
Tenant-context resolution:
header_missing_on_tenant_endpoint:
status: 400
code: ORGANIZATION_CONTEXT_REQUIRED
organization_not_found:
status: 404
code: RESOURCE_NOT_FOUND
membership_not_found:
status: 404
code: RESOURCE_NOT_FOUND
membership_inactive:
status: 403
code: AUTHZ_MEMBERSHIP_INACTIVE
organization_inactive:
status: 403
code: AUTHZ_ORGANIZATION_INACTIVE
resource_organization_mismatch:
status: 404
code: RESOURCE_NOT_FOUND
Idempotency
Use:
Idempotency-Key: 8f7d6c5e-4b3a-4b1c-9d8e-7f6a5b4c3d2e
Required where duplicate execution can create material side effects.
PostgreSQL is authoritative for critical idempotency records.
Redis may accelerate lookup.
Batch Custom-Action Convention
For collection-level custom commands use:
/{collection}/batch/{action}
Examples:
POST /api/v1/engineering/tasks/batch/assign
POST /api/v1/engineering/tasks/batch/complete
POST /api/v1/engineering/time-entries/batch/submit
Do not mix batch-assign, colon-style custom methods, and /batch/assign in the same API.
Rate-Limit Responses
429 Too Many Requests
Retry-After: <seconds-or-http-date>
Additional rate-limit metadata may be exposed according to the selected gateway/standard.
Do not freeze legacy X-RateLimit-* names here.
Error Standard Decision
The current error envelope remains:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Resource not found.",
"details": {},
"requestId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff"
}
}
ADR-005 decides whether OpenAPI v1 aligns this with RFC 9457 Problem Details.
Do not silently change the envelope during implementation.
9. Standard Response Format
Single resource:
{
"data": {
"id": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c5d",
"name": "Central Tower"
}
}
Collection:
{
"data": [],
"meta": {
"pagination": {
"nextCursor": null,
"hasMore": false
}
}
}
Standard error:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Resource not found.",
"details": {},
"requestId": "req_123"
}
}
Clients depend on error.code, not message text.
Error Taxonomy
Authentication:
AUTH_INVALID_CREDENTIALS
AUTH_TOKEN_EXPIRED
AUTH_TOKEN_INVALID
AUTH_MFA_REQUIRED
AUTH_SESSION_REVOKED
AUTH_REFRESH_TOKEN_REUSED
Authorization:
AUTHZ_PERMISSION_DENIED
AUTHZ_ORGANIZATION_INACTIVE
AUTHZ_MEMBERSHIP_INACTIVE
AUTHZ_CREDENTIAL_INVALID
AUTHZ_SCOPE_MISMATCH
Tenant context:
ORGANIZATION_CONTEXT_REQUIRED
Resource/state:
RESOURCE_NOT_FOUND
RESOURCE_ALREADY_EXISTS
RESOURCE_CONCURRENT_MODIFICATION
RESOURCE_INVALID_STATE
RESOURCE_ARCHIVED
Validation:
VALIDATION_ERROR
VALIDATION_REQUIRED_FIELD
VALIDATION_INVALID_FORMAT
VALIDATION_BUSINESS_RULE
Idempotency:
IDEMPOTENCY_KEY_REQUIRED
IDEMPOTENCY_KEY_CONFLICT
Rate limiting:
RATE_LIMIT_EXCEEDED
System/dependency:
INTERNAL_ERROR
SERVICE_UNAVAILABLE
DATABASE_UNAVAILABLE
DEPENDENCY_FAILED
Validation example:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"requestId": "req_123",
"details": {
"fields": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
}
]
}
}
}
Business-state example:
{
"error": {
"code": "RESOURCE_INVALID_STATE",
"message": "Cannot approve design in current state.",
"requestId": "req_123",
"details": {
"resourceType": "engineering_design",
"resourceId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c6d",
"currentState": "draft",
"requiredState": "under_review",
"allowedActions": [
"submit_review"
]
}
}
}
Do not expose internal stack traces, SQL, policy internals, secrets, or cross-tenant information.
10. HTTP Status Rules
200 Success
201 Created
202 Accepted
204 No Content
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Validation Error
429 Too Many Requests
500 Internal Server Error
Cross-tenant resource access should return 404.
11. API Versioning
Current API:
/api/v1
Breaking changes require:
/api/v2
Additive fields generally do not require a new version.
11A. Identifier Convention
The serialized identifier standard is raw UUIDv7.
Example:
0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c1d
Database:
id UUID PRIMARY KEY
API:
{
"id": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c1d"
}
Do not serialize IDs as:
org_<uuid>
user_<uuid>
project_<uuid>
unless a future ADR explicitly changes the public identifier contract.
Human-friendly resource references use separate fields such as:
projectNumber
matterNumber
patientNumber
invoiceNumber
This separates machine identity from business/display references.
UUID generation is decided by ADR-004:
PostgreSQL-native UUIDv7 when supported and selected
or
application-generated UUIDv7
The API format is identical either way.
12. Authentication
Initial human authentication:
Email
+
Password
+
Short-Lived Access Token
+
Opaque Refresh Token
+
Server-Side Session
REST:
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/logout
POST /api/v1/auth/token/refresh
POST /api/v1/auth/token/revoke
POST /api/v1/auth/token/revoke-all
GET /api/v1/auth/sessions
DELETE /api/v1/auth/sessions/{sessionId}
GET /api/v1/me
POST /auth/logout revokes the current session.
DELETE /auth/sessions/{sessionId} allows a user to revoke a specific session, such as another device.
Access Token
format: JWT
lifetime: short-lived
signed: true
encrypted: false
claims:
- sub
- sessionId
- issuer
- audience
- issuedAt
- expiresAt
Organization context is not trusted from the token as authorization authority.
Sessions
sessions
├── id
├── user_id
├── device metadata
├── created_at
├── last_active_at
├── expires_at
├── revoked_at
└── revocation_reason
Refresh Tokens
refresh_tokens
├── id
├── session_id
├── family_id
├── token_hash
├── issued_at
├── expires_at
├── rotated_at
├── replaced_by_token_id
├── revoked_at
└── revocation_reason
Constraints/indexes:
UNIQUE(token_hash)
INDEX(family_id)
INDEX(session_id)
family_id is not unique.
Refresh Reuse Detection
Use of a previously rotated token triggers:
revoke token family
revoke affected session
security audit event
reauthentication
Policy may escalate to all-session revocation.
Future human authentication:
- MFA
- WebAuthn/passkeys
- OIDC/SSO
- enterprise identity providers
12A. Service Accounts and API Keys
Machine-to-machine access is separate from human sessions.
Use:
service_accounts
api_keys
service_account_roles
Service Account
Suggested fields:
id
organization_id
name
description
status
created_by_user_id
created_at
updated_at
revoked_at
Service-account status values:
active
revoked
API Key
Suggested fields:
id
organization_id
service_account_id
key_prefix
secret_hash
created_at
expires_at
last_used_at
revoked_at
revocation_reason
Raw API-key secrets are shown only once.
Store only a secure hash of the secret.
key_prefix is safe display material for identifying a key in administration screens.
Service Account Role
service_account_roles uses the same organization-scoped role registry as human RBAC assignments.
Suggested fields:
id
organization_id
service_account_id
role_id
created_at
Unique:
(organization_id, service_account_id, role_id)
Tenant-safe foreign keys require the service account and role to belong to the same organization as the assignment.
Authorization
Service accounts use explicit organization-scoped permissions, preferably through:
service_account_roles
with the same registered permission vocabulary used by RBAC.
They do not become fake human memberships.
Audit
Audit actors support:
actor_type = user
actor_type = service_account
actor_type = system
Machine authentication is required when public/integration API access is implemented; it does not block the earliest internal Engineering UI slice.
13. Shared Core Backend
Recommended modules:
core/
├── auth/
├── users/
├── organizations/
├── memberships/
├── roles/
├── permissions/
├── authorization/
├── documents/
├── billing/
├── notifications/
├── audit/
└── events/
Dependency rule:
Profession module → Core
Never:
Core → Profession module
14. Organizations
Organizations are tenants.
Examples:
Atlas Structural Engineering
Smith & Associates Law
North Shore Medical Practice
Suggested fields:
id
name
slug
status
country_code
timezone
currency_code
created_at
updated_at
Organization status values:
active
suspended
pending_deletion
15. Profession Enablement
Use:
organization_professions
Suggested fields:
organization_id
profession
enabled_at
configuration
Possible professions:
engineering
legal
healthcare
An organization may eventually enable more than one profession module.
16. Users and Memberships
Users are global identities.
A user gains tenant access through membership.
User
│
▼
Membership
│
▼
Organization
Suggested users fields:
id
email
first_name
last_name
phone
avatar_url
status
created_at
updated_at
Suggested memberships fields:
id
organization_id
user_id
status
joined_at
created_at
updated_at
User status values:
active
inactive
pending_verification
Membership status values:
active
inactive
pending
17. Membership Invitations
Keep invitations separate from memberships.
Tables:
membership_invitations
membership_invitation_roles
membership_invitations:
id
organization_id
email
invited_by_user_id
expires_at
accepted_at
revoked_at
created_at
membership_invitation_roles:
organization_id
invitation_id
role_id
created_at
Use tenant-aware foreign keys so invitation roles cannot reference another organization's role.
Flow:
Invitation + Intended Roles
↓
Accepted
↓
User
↓
Membership
↓
Membership Roles
At acceptance:
- validate invitation token and expiry
- validate invited email/account policy
- create membership
- copy valid intended roles to membership-role assignments
- mark invitation accepted
- audit
- emit outbox event
If an intended role was revoked/deleted before acceptance, acceptance fails safely or drops that role according to explicit organization policy.
18. Authorization
Use:
RBAC
+
Permission Scope
+
Resource Policies
+
Professional Qualification Policies
+
Domain State Rules
Decision flow:
Authenticated User
↓
Explicit Organization Context
↓
Active Membership
↓
Enabled Profession Module
↓
Roles
↓
Permissions
↓
Permission Scope
↓
Tenant-scoped Resource Query
↓
Resource Policy
↓
Credential/Jurisdiction Policy
↓
Domain State Rule
↓
ALLOW / DENY
Default decision:
DENY
Authorization rules:
- Controllers never perform ad-hoc role comparisons.
- Tenant resource queries always include
organization_id. - Do not load an arbitrary resource first and then discover it belongs to another tenant.
- High-risk professional actions perform credential checks at command execution time.
- A permission grants the ability to attempt an action, not a guarantee the domain state allows it.
- Cross-tenant resources appear nonexistent.
- Profession module enablement is checked before profession-specific authorization.
19. Roles and Permissions
Roles are organization-scoped collections of permissions.
Example roles:
Owner
Administrator
Project Manager
Engineer
Reviewer
Inspector
Lawyer
Paralegal
Doctor
Nurse
Billing Manager
Viewer
Roles are not professional credentials.
Engineering Permissions
engineering.clients.read
engineering.clients.create
engineering.clients.update
engineering.clients.archive
engineering.projects.read
engineering.projects.create
engineering.projects.update
engineering.projects.activate
engineering.projects.close
engineering.projects.archive
engineering.project_members.manage
engineering.phases.manage
engineering.tasks.manage
engineering.sites.manage
engineering.documents.read
engineering.documents.upload
engineering.documents.delete
engineering.designs.read
engineering.designs.create
engineering.designs.update
engineering.designs.review
engineering.designs.approve
engineering.designs.reject
engineering.designs.supersede
engineering.inspections.read
engineering.inspections.manage
engineering.inspections.complete
engineering.time_entries.manage
engineering.reports.read
Legal Permissions
legal.clients.read
legal.clients.create
legal.clients.update
legal.matters.read
legal.matters.create
legal.matters.update
legal.matters.close
legal.matters.reopen
legal.cases.read
legal.cases.manage
legal.hearings.manage
legal.deadlines.manage
legal.documents.read
legal.documents.upload
legal.conflicts.manage
legal.conflicts.approve
legal.retainers.manage
legal.time_entries.manage
Healthcare Permissions
healthcare.patients.read
healthcare.patients.create
healthcare.patients.update
healthcare.appointments.read
healthcare.appointments.manage
healthcare.encounters.read
healthcare.encounters.manage
healthcare.records.read
healthcare.records.write
healthcare.records.sign
healthcare.records.amend
healthcare.records.access_log.read
healthcare.prescriptions.read
healthcare.prescriptions.write
healthcare.prescriptions.sign
healthcare.insurance.read
healthcare.insurance.manage
Shared Permissions
documents.read
documents.upload
billing.read
invoices.create
invoices.issue
invoices.void
payments.record
payments.refund
members.read
members.invite
members.update
members.remove
roles.read
roles.manage
audit.read
Avoid vague permissions such as admin_everything in normal tenant RBAC.
20. Permission Scopes
Initial scopes:
assigned
organization
Example:
Engineer:
engineering.projects.read = assigned
Principal Engineer:
engineering.projects.read = organization
assigned is not magic. Each resource policy defines how assignment is resolved.
Engineering Project
Assigned when:
engineering_project_members.user_id = ctx.userId
AND engineering_project_members.left_at IS NULL
or when the user is the active project manager, if project-manager assignment is modeled separately.
Engineering Task
Assigned when:
engineering_tasks.assigned_to_user_id = ctx.userId
For tasks linked to a project, parent-project access may also be required.
Engineering Design
Assigned when an active row exists in:
engineering_design_assignments
for the user and an allowed assignment role.
Engineering Inspection
Assigned when:
engineering_inspections.inspector_user_id = ctx.userId
or an explicit inspection assignment exists if the model later supports multiple inspectors.
Derived Client Access
An assigned professional may access a client only through a policy that derives access from authorized projects.
Project assignment must not automatically grant access to every project belonging to that client.
Future scopes may include:
owned
team
department
restricted
Do not add them before a real workflow requires them.
21. Professional Credentials
Professional identity and credentials are separate from RBAC.
Use:
professional_profiles
professional_credentials
Professional Profile
One organization/user/profession relationship.
Suggested fields:
id
organization_id
user_id
profession
title
status
created_at
updated_at
Professional-profile status values:
active
suspended
inactive
Credential expiry is represented by credential status and expires_at; a profile itself does not become expired merely because one credential expires.
Professional Credential
One profile may hold many credentials.
Suggested fields:
id
organization_id
professional_profile_id
credential_type
credential_number
issuing_authority
jurisdiction
discipline
status
valid_from
expires_at
verified_at
verified_by_user_id
created_at
updated_at
Professional-credential status values:
pending_verification
active
suspended
expired
revoked
Examples:
professional engineering license in jurisdiction A
professional engineering license in jurisdiction B
specialty certification
medical license
controlled-substance prescribing registration where applicable
Credential policy evaluates the set of active credentials rather than one primary_license_number.
High-risk actions such as design approval, record signing, or prescribing use authoritative or revocation-aware credential state.
Prescribing remains jurisdiction/scope-of-practice policy, not a hard-coded profession test.
22. Database Architecture
Use PostgreSQL.
Start with:
One database
+
Shared schema
+
Profession-specific tables
Do not begin with database-per-profession or database-per-customer unless compliance or residency requirements force that choice.
23. Shared Tables
Recommended shared tables:
organizations
organization_professions
users
user_credentials
sessions
service_accounts
api_keys
service_account_roles
memberships
membership_invitations
roles
permissions
role_permissions
membership_roles
professional_profiles
professional_credentials
documents
document_versions
document_categories
retention_policies
billing_accounts
invoices
invoice_items
payments
payment_refunds
notifications
notification_deliveries
audit_events
outbox_events
webhooks
webhook_event_subscriptions
webhook_deliveries
jobs
24. Multi-Tenancy Rule
Every tenant-owned row must contain:
organization_id
Examples:
engineering_projects.organization_id
legal_matters.organization_id
healthcare_patients.organization_id
Enforce tenant boundaries at:
- API layer
- authorization layer
- repository/query layer
- database constraints
25. Tenant-Safe Foreign Keys
Use composite tenant-aware foreign keys when possible.
Example:
engineering_projects
organization_id
client_id
references:
engineering_clients
organization_id
id
This prevents linking a resource from one organization to another organization's data.
21A. Deletion, Archival, Revocation, and Unlink Policy
DELETE does not have one universal persistence meaning.
Use four lifecycle behaviors.
Archive / Domain Inactivation
For business records whose history matters:
engineering clients
engineering projects
legal matters
healthcare patients
documents where retention requires history
Typical fields:
status
archived_at
archived_by_user_id
Restore is permitted only when domain, retention, and organization policy allow it.
Revoke
For access/security resources:
sessions
refresh tokens
API keys
membership invitations
portal grants
webhook credentials
Use:
revoked_at
revoked_by
revocation_reason
Temporal Unlink
For relationship records where the historical relationship matters:
project documents
project members
design assignments
portal document publications
Use:
unlinked_at
left_at
unassigned_at
revoked_at
rather than deleting historical evidence.
Hard Delete
Reserved for genuinely disposable or never-committed data, such as:
expired pending upload artifacts
failed temporary staging objects
unreferenced draft configuration where audit/retention does not require history
Hard deletion of financial, professional, audit, signed clinical, or issued business records is forbidden unless an explicit retention/privacy policy defines the operation.
Every resource specification must declare its lifecycle behavior.
Engineering Domain
26. Engineering Tables
Initial Engineering MVP tables:
engineering_clients
engineering_client_contacts
engineering_projects
engineering_project_members
engineering_project_phases
engineering_sites
engineering_tasks
engineering_designs
engineering_design_assignments
engineering_design_versions
engineering_design_version_documents
engineering_design_reviews
engineering_inspections
engineering_inspection_findings
engineering_inspection_followups
engineering_inspection_documents
engineering_specifications
engineering_specification_documents
engineering_time_entries
engineering_client_billing_accounts
engineering_invoice_projects
engineering_invoice_item_time_entries
Later Engineering extensions:
engineering_project_budgets
engineering_project_budget_items
engineering_project_commitments
engineering_project_cost_entries
engineering_change_requests
engineering_change_requests is not part of the initial schema until its lifecycle, relationships, and REST contract are specified.
27. Engineering Clients
Suggested core client fields:
id
organization_id
client_type
display_name
legal_name
status
created_at
updated_at
version
Initial client_type values:
corporate
government
individual
Engineering-client status values:
active
archived
Do not permanently squeeze all contacts into one email, one phone, and one contact_name.
Engineering customers commonly have multiple:
technical contacts
billing contacts
executive contacts
site contacts
contract contacts
Use:
engineering_client_contacts
Suggested contact fields:
id
organization_id
client_id
name
title
department
email
phone
contact_type
is_primary
created_at
updated_at
Client REST:
GET /api/v1/engineering/clients
POST /api/v1/engineering/clients
GET /api/v1/engineering/clients/{clientId}
PATCH /api/v1/engineering/clients/{clientId}
POST /api/v1/engineering/clients/{clientId}/archive
POST /api/v1/engineering/clients/{clientId}/restore
GET /api/v1/engineering/clients/{clientId}/projects
GET /api/v1/engineering/clients/{clientId}/invoices
Contact REST:
GET /api/v1/engineering/clients/{clientId}/contacts
POST /api/v1/engineering/clients/{clientId}/contacts
PATCH /api/v1/engineering/clients/{clientId}/contacts/{contactId}
DELETE /api/v1/engineering/clients/{clientId}/contacts/{contactId}
Delete may be implemented as archival when contact history matters.
Client restore is allowed only when organization policy and retention rules permit it.
27A. Engineering Client Portal
External clients are not internal organization members.
Use shared authentication identities where practical, but create a separate authorization boundary.
User
│
├── Internal Membership
│ ↓
│ Organization Staff Access
│
└── Client Portal Account
↓
Engineering Client Contact
↓
Project Access Grants
Suggested tables:
engineering_client_portal_accounts
engineering_client_portal_project_grants
engineering_project_document_publications
engineering_client_review_requests
Portal Account
Suggested fields:
id
organization_id
user_id
engineering_client_contact_id
status
invited_by_user_id
invited_at
accepted_at
revoked_at
revoked_by_user_id
Portal-account status values:
invited
active
expired
revoked
Portal accounts are not placed in memberships.
Project Grant
Suggested fields:
id
organization_id
portal_account_id
project_id
access_profile
granted_by_user_id
granted_at
expires_at
revoked_at
Initial access_profile values and capabilities:
viewer
project.status.read
project.documents.read_published
contributor
all viewer capabilities
project.comments.create
project.files.submit
reviewer
all contributor capabilities
client_review.respond
Capabilities are assigned through the three profiles above in v1; callers cannot submit an arbitrary capability list.
The access model may later normalize capabilities into a grant table if simple profiles become insufficient.
Client Review Request
engineering_client_review_requests fields:
id
organization_id
project_id
design_id nullable
portal_account_id
requested_by_user_id
status
response nullable
response_notes nullable
requested_at
responded_at nullable
created_at
updated_at
version
Status values:
pending
responded
withdrawn
expired
Response values:
accepted
changes_requested
declined
Only a portal account with an active grant for the same project and the client_review.respond capability may respond. A response sets status = responded, response, and responded_at atomically. Client responses never perform professional engineering approval.
Separate Frontend
Recommended:
apps/
├── engineering-web/
└── engineering-client-portal/
The internal engineering frontend and external portal do not share authorization assumptions.
Client Acceptance Is Not Engineering Approval
Never represent client acceptance with:
engineering.designs.approve
Professional engineering approval is reserved for qualified internal/authorized professionals.
Client-facing review should use separate concepts such as:
engineering.client_reviews.request
engineering.client_reviews.respond
engineering.client_reviews.accept
engineering.client_reviews.request_changes
Example:
POST /api/v1/engineering/client-review-requests/{reviewId}/accept
POST /api/v1/engineering/client-review-requests/{reviewId}/request-changes
A client acceptance may be commercially meaningful without being a professional engineering approval.
Portal Security Rules
- portal access is deny-by-default
- every portal request remains organization-scoped
- portal users only access explicitly granted projects
- project membership does not apply to portal users
- internal RBAC roles do not automatically apply to portal users
- portal account revocation is immediate
- portal grants may expire
- sensitive document access requires explicit publication
- portal activity is audited according to organization policy
- professional approval endpoints are never exposed through portal grants
27B. External Document Publication
A document being linked to an engineering project does not make it externally visible.
Use:
engineering_project_document_publications
Suggested fields:
id
organization_id
project_document_link_id
audience_type
portal_account_id nullable
client_id nullable
published_by_user_id
published_at
expires_at
revoked_at
revoked_by_user_id
Possible audiences:
all_active_client_portal_accounts_for_project
specific_portal_account
specific_client_contact
External download checks:
authenticated portal user
+
active portal account
+
active project grant
+
active document publication
+
publication not expired/revoked
+
document classification allows publication
+
download permission
This prevents an internal project document from appearing in the client portal merely because it is linked to the project.
28. Engineering Projects
Suggested fields:
id
organization_id
client_id
project_number
name
description
discipline
status
project_manager_user_id
start_date
expected_completion_date
completed_date
created_at
updated_at
version
stage is removed from the project row because project phases are the authoritative workflow decomposition.
Engineering-project status values:
draft
active
closed
archived
If the frontend needs a "current stage", derive it from the ordered phase records. engineering_projects does not store current_phase_id in v1; adding that denormalized pointer would require a later migration plus transactional reconciliation rules.
Project budget_minor is also removed.
Detailed project budgets belong to the dedicated budget model.
REST:
GET /api/v1/engineering/projects
POST /api/v1/engineering/projects
GET /api/v1/engineering/projects/{projectId}
PATCH /api/v1/engineering/projects/{projectId}
POST /api/v1/engineering/projects/{projectId}/activate
POST /api/v1/engineering/projects/{projectId}/close
POST /api/v1/engineering/projects/{projectId}/archive
Purpose-built reads:
GET /api/v1/engineering/projects/{projectId}/summary
GET /api/v1/engineering/projects/{projectId}/timeline
GET /api/v1/engineering/projects/{projectId}/budget
The budget endpoint reads from the budget module when that module exists.
29. Engineering Project Members
Suggested fields:
id
organization_id
project_id
user_id
project_role
joined_at
left_at
Initial project-role vocabulary:
project_manager
engineer
designer
reviewer
inspector
viewer
contractor
Project role describes participation in one project.
It is not a substitute for RBAC permission.
REST:
GET /api/v1/engineering/projects/{projectId}/members
POST /api/v1/engineering/projects/{projectId}/members
PATCH /api/v1/engineering/projects/{projectId}/members/{memberId}
DELETE /api/v1/engineering/projects/{projectId}/members/{memberId}
DELETE means end participation by setting left_at, not erase historical participation.
PATCH may change only project_role in v1. joined_at is server-assigned and immutable; left_at is set only by DELETE. Rejoining uses POST to create a new temporal membership record, and a partial unique index permits at most one active row per (organization_id, project_id, user_id) where left_at IS NULL.
30. Engineering Project Phases
Suggested fields:
id
organization_id
project_id
name
sequence
status
start_date
end_date
created_at
updated_at
version
Typical initial statuses:
planned
active
completed
cancelled
Example phases:
Concept
Preliminary Design
Detailed Design
Construction
Inspection
Closeout
REST:
GET /api/v1/engineering/projects/{projectId}/phases
POST /api/v1/engineering/projects/{projectId}/phases
PATCH /api/v1/engineering/projects/{projectId}/phases/{phaseId}
POST /api/v1/engineering/projects/{projectId}/phases/{phaseId}/complete
POST /api/v1/engineering/projects/{projectId}/phases/reorder
Reorder request:
{
"projectVersion": 12,
"orderedPhaseIds": [
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b301",
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b302"
]
}
Reordering is transactional.
Sequences remain unique within a project after commit.
31. Engineering Sites
Suggested fields:
id
organization_id
project_id
name
address
latitude
longitude
created_at
updated_at
REST:
GET /api/v1/engineering/sites
GET /api/v1/engineering/sites/{siteId}
POST /api/v1/engineering/projects/{projectId}/sites
GET /api/v1/engineering/projects/{projectId}/sites
PATCH /api/v1/engineering/sites/{siteId}
Global site listing is still tenant-scoped through X-Organization-Id.
32. Engineering Tasks
Suggested fields:
id
organization_id
project_id
title
description
status
priority
created_by_user_id
assigned_to_user_id
due_at
completed_at
created_at
updated_at
version
Statuses:
todo
in_progress
completed
cancelled
Priorities:
low
medium
high
urgent
REST:
POST /api/v1/engineering/tasks
GET /api/v1/engineering/tasks
GET /api/v1/engineering/tasks/{taskId}
PATCH /api/v1/engineering/tasks/{taskId}
POST /api/v1/engineering/tasks/{taskId}/complete
POST /api/v1/engineering/tasks/{taskId}/reopen
POST /api/v1/engineering/tasks/{taskId}/cancel
32A. Engineering Batch Operations
Batch operations are useful for repetitive engineering workflows, but they must not bypass per-resource authorization or domain rules.
Examples:
POST /api/v1/engineering/tasks/batch/assign
POST /api/v1/engineering/tasks/batch/complete
POST /api/v1/engineering/time-entries/batch/submit
Batch Execution Modes
Every batch command explicitly defines one of:
atomic
partial
Atomic:
all resources succeed
or
entire operation fails
Partial:
each resource is evaluated independently
successful items commit
failed items return individual errors
Do not leave this behavior implicit.
Example request:
{
"taskIds": [
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c81",
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c82",
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c83"
],
"assigneeUserId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c3d",
"mode": "partial"
}
Example response:
{
"data": {
"succeeded": [
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c81",
"0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c82"
],
"failed": [
{
"id": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c83",
"code": "RESOURCE_INVALID_STATE"
}
]
}
}
Authorization
Each resource is evaluated for:
tenant
permission
scope
resource access
state validity
credential policy where applicable
Never authorize the first item and assume the remaining batch is equivalent.
Synchronous vs Asynchronous
Small batches may execute synchronously.
Large batches become jobs:
202 Accepted
with:
jobId
The synchronous/asynchronous threshold is configuration based on:
batch size
operation cost
database load
side effects
product tier
Financial or regulated batch actions require stricter idempotency and audit rules than ordinary task updates.
33. Engineering Designs
Suggested fields:
id
organization_id
project_id
design_number
title
description
discipline
status
owner_user_id
prepared_by_user_id
approved_by_user_id
approved_at
created_at
updated_at
version
States:
draft
under_review
changes_requested
approved
rejected
cancelled
withdrawn
superseded
REST:
GET /api/v1/engineering/projects/{projectId}/designs
POST /api/v1/engineering/projects/{projectId}/designs
GET /api/v1/engineering/designs/{designId}
PATCH /api/v1/engineering/designs/{designId}
POST /api/v1/engineering/designs/{designId}/submit-review
POST /api/v1/engineering/designs/{designId}/request-changes
POST /api/v1/engineering/designs/{designId}/approve
POST /api/v1/engineering/designs/{designId}/reject
POST /api/v1/engineering/designs/{designId}/revise
POST /api/v1/engineering/designs/{designId}/cancel
POST /api/v1/engineering/designs/{designId}/withdraw
POST /api/v1/engineering/designs/{designId}/supersede
POST /api/v1/engineering/designs/{designId}/assign
POST /api/v1/engineering/designs/{designId}/unassign
GET /api/v1/engineering/designs/{designId}/versions
POST /api/v1/engineering/designs/{designId}/versions
GET /api/v1/engineering/designs/{designId}/reviews
POST /api/v1/engineering/designs/{designId}/reviews
Design Assignment
engineering_design_assignments fields:
id
organization_id
design_id
user_id
assignment_role
notes nullable
assigned_by_user_id
assigned_at
unassigned_at nullable
Initial assignment-role values:
owner
preparer
reviewer
contributor
approver is not an assignment role: approval authority is credential- and policy-driven, and the actual approver is recorded by the approval transition.
Active assignments are unique by (organization_id, design_id, user_id, assignment_role) where unassigned_at IS NULL. POST /assign accepts userId, assignmentRole, and optional notes. POST /unassign accepts assignmentId and sets unassigned_at; it never hard-deletes the assignment. The design's primary owner_user_id and prepared_by_user_id, when set, must correspond to active owner and preparer assignments respectively.
Assignment listing:
GET /api/v1/engineering/designs/{designId}/assignments
State machine:
draft
├── submit-review ─────────────► under_review
└── cancel ────────────────────► cancelled
under_review
├── request-changes ───────────► changes_requested
├── approve ───────────────────► approved
├── reject ────────────────────► rejected
└── withdraw ──────────────────► withdrawn
changes_requested
├── submit-review ─────────────► under_review
└── withdraw ──────────────────► withdrawn
rejected
└── revise ────────────────────► draft
approved
└── supersede ─────────────────► superseded
Approval remains credential-aware, audited, and idempotent.
Designs do not use generic archive/restore endpoints. Their professional lifecycle terminates through explicit state-machine outcomes such as cancelled, withdrawn, and superseded. Terminal designs remain queryable and auditable and are not hard-deleted through ordinary workflows.
34. Design Versions and Reviews
A design version is a logical professional revision.
It may have multiple document files.
Use:
engineering_design_versions
engineering_design_version_documents
engineering_design_reviews
Design Version
id
organization_id
design_id
version_number
created_by_user_id
created_at
Unique:
(organization_id, design_id, version_number)
Design Version Documents
id
organization_id
design_version_id
document_id
document_role
linked_by_user_id
linked_at
unlinked_at
Possible document_role values:
primary_drawing
calculation
supporting_document
specification
attachment
A design version therefore supports one or many documents without putting document_id directly on the version.
Design Review
id
organization_id
design_id
design_version_id
reviewer_user_id
status
comments
reviewed_at
created_at
Statuses:
pending
approved
changes_requested
rejected
All three tables are tenant-owned and carry direct organization_id.
35. Engineering Inspections
Inspection fields:
id
organization_id
project_id
site_id
inspection_type
inspector_user_id
status
outcome
scheduled_at
started_at
performed_at
cancelled_at
summary
created_at
updated_at
version
Lifecycle:
draft
scheduled
in_progress
completed
cancelled
Outcome:
passed
passed_with_observations
followup_required
failed
inspection_type is an application/domain registry rather than a PostgreSQL enum.
Initial common keys may include:
structural
mechanical
electrical
safety
final
Organizations/modules may add supported types through controlled configuration later.
REST:
GET /api/v1/engineering/projects/{projectId}/inspections
POST /api/v1/engineering/projects/{projectId}/inspections
GET /api/v1/engineering/inspections/{inspectionId}
PATCH /api/v1/engineering/inspections/{inspectionId}
POST /api/v1/engineering/inspections/{inspectionId}/schedule
POST /api/v1/engineering/inspections/{inspectionId}/start
POST /api/v1/engineering/inspections/{inspectionId}/complete
POST /api/v1/engineering/inspections/{inspectionId}/cancel
GET /api/v1/engineering/inspections/{inspectionId}/findings
POST /api/v1/engineering/inspections/{inspectionId}/findings
GET /api/v1/engineering/inspections/{inspectionId}/followups
POST /api/v1/engineering/inspections/{inspectionId}/followups
Inspection completion may create follow-up records.
Lifecycle and outcome remain separate.
36. Inspection Findings
engineering_inspection_findings:
id
organization_id
inspection_id
severity
description
status
resolved_at
resolved_by_user_id
created_at
updated_at
version
Severity:
observation
minor
major
critical
Status:
open
in_progress
resolved
accepted_risk
REST:
POST /api/v1/engineering/inspections/{inspectionId}/findings
PATCH /api/v1/engineering/inspection-findings/{findingId}
POST /api/v1/engineering/inspection-findings/{findingId}/resolve
organization_id is direct even though tenant ownership is also derivable through the inspection.
Follow-Up Resource
Use:
engineering_inspection_followups
Fields:
id
organization_id
inspection_id
followup_type
linked_task_id nullable
linked_inspection_id nullable
status
created_by_user_id
created_at
completed_at
cancelled_at
followup_type:
corrective_task
followup_inspection
both
status:
open
in_progress
completed
cancelled
Tenant-safe foreign keys apply to the original inspection and any linked task/inspection.
37. Engineering Specifications
Use:
engineering_specifications
engineering_specification_documents
Suggested specification fields:
id
organization_id
project_id
specification_number
title
version
status
superseded_by_specification_id nullable
archived_from_status nullable
archived_at nullable
archived_by_user_id nullable
created_at
updated_at
Status values:
draft
active
superseded
archived
Specification document links:
id
organization_id
specification_id
document_id
document_role
linked_by_user_id
linked_at
unlinked_at
A specification may therefore have one or many current or historical document links. document_role is an application registry with initial values such as primary, attachment, and supporting_document. Tenant-safe foreign keys apply to both the specification and shared document.
The specification-document document_role registry is distinct from the design-version document-role registry in §34. Identical column names do not imply one shared vocabulary.
Specification REST:
GET /api/v1/engineering/projects/{projectId}/specifications
POST /api/v1/engineering/projects/{projectId}/specifications
GET /api/v1/engineering/specifications/{specificationId}
PATCH /api/v1/engineering/specifications/{specificationId}
POST /api/v1/engineering/specifications/{specificationId}/activate
POST /api/v1/engineering/specifications/{specificationId}/supersede
POST /api/v1/engineering/specifications/{specificationId}/archive
POST /api/v1/engineering/specifications/{specificationId}/restore
GET /api/v1/engineering/specifications/{specificationId}/documents
POST /api/v1/engineering/specifications/{specificationId}/documents
DELETE /api/v1/engineering/specification-documents/{documentLinkId}
Lifecycle rules:
draft → activate → active
draft → archive → archived
active → supersede → superseded
active → archive → archived
archived → restore → archived_from_status
superseded → terminal
Archive stores the prior draft or active value in archived_from_status; restore clears the archive fields and returns to that value. Supersede requires supersededBySpecificationId, linking a same-project replacement specification, and is audited and idempotent.
38. Engineering Change Requests
Deferred from the initial Engineering schema.
Change requests are a valid future engineering capability, but v4.1 does not create the table until these are specified:
relationship to project
relationship to design/specification
request origin
impact analysis
cost/schedule effects
review workflow
approval authority
state machine
document links
REST commands
audit requirements
Future candidate:
engineering_change_requests
This belongs in the Engineering extension backlog rather than a half-defined initial migration.
38A. Engineering Project Budgets
A single budget_minor column is sufficient only for a very early project total.
When budget management enters scope, introduce:
engineering_project_budgets
engineering_project_budget_items
engineering_project_commitments
engineering_project_cost_entries
Budget
Suggested fields:
id
organization_id
project_id
name
currency_code
status
approved_by_user_id
approved_at
created_at
updated_at
version
Budget Item
Suggested fields:
id
organization_id
budget_id
category
description
allocated_amount_minor
created_at
updated_at
Do not casually store mutable:
spent_amount_minor
committed_amount_minor
as independent sources of truth if those values are derived from time entries, expenses, purchase commitments, or invoices.
Prefer:
authoritative cost/commitment records
↓
derived budget projections
If denormalized totals are needed for performance, update them transactionally and reconcile them.
Potential REST:
GET /api/v1/engineering/projects/{projectId}/budgets
POST /api/v1/engineering/projects/{projectId}/budgets
GET /api/v1/engineering/budgets/{budgetId}
PATCH /api/v1/engineering/budgets/{budgetId}
POST /api/v1/engineering/budgets/{budgetId}/approve
GET /api/v1/engineering/budgets/{budgetId}/items
POST /api/v1/engineering/budgets/{budgetId}/items
Budget approval is an explicit command.
39. Engineering Time Entries
Suggested fields:
id
organization_id
project_id
user_id
work_date
duration_minutes
description
billable
billing_rate_minor nullable
currency_code nullable
phase_id nullable
task_id nullable
design_id nullable
inspection_id nullable
created_at
updated_at
version
The project is always required.
A time entry may also identify one primary work item.
Database check:
at most one of:
phase_id
task_id
design_id
inspection_id
Each optional foreign key is tenant- and project-aware. For example:
(organization_id, project_id, task_id)
→ engineering_tasks(organization_id, project_id, id)
and similarly for phase, design, and inspection. Supporting unique constraints on (organization_id, project_id, id) are required on each target table.
This is a database-enforced invariant, not only an application validation rule: whenever an optional work-item ID is present, that work item must belong to the same organization and project_id as the time entry.
This preserves relational integrity instead of using an unconstrained polymorphic reference_type/reference_id.
REST:
POST /api/v1/engineering/time-entries
GET /api/v1/engineering/time-entries
GET /api/v1/engineering/time-entries/{timeEntryId}
PATCH /api/v1/engineering/time-entries/{timeEntryId}
POST /api/v1/engineering/time-entries/batch/submit
Duration is integer minutes.
Billing snapshot constraint:
billable = true
→ billing_rate_minor MUST be greater than zero
→ currency_code MUST be non-null
billable = false
→ billing_rate_minor MUST be null
→ currency_code MUST be null
The rate and currency are captured on the time entry so later billing-rate changes do not rewrite historical work.
40. Legal Tables
Initial legal-domain tables:
legal_clients
legal_matters
legal_matter_members
legal_cases
legal_case_parties
legal_courts
legal_hearings
legal_deadlines
legal_time_entries
legal_retainers
legal_conflict_checks
legal_conflict_parties
legal_conflict_matches
legal_matter_documents
legal_case_documents
legal_client_billing_accounts
legal_invoice_matters
There is no separate legal_documents ownership table.
Documents remain shared infrastructure:
documents
document_versions
Legal relationships use:
legal_matter_documents
legal_case_documents
legal_matter_documents fields:
id
organization_id
matter_id
document_id
linked_by_user_id
linked_at
unlinked_at
legal_case_documents fields:
id
organization_id
case_id
document_id
linked_by_user_id
linked_at
unlinked_at
Both tables use tenant-safe foreign keys to their Legal parent and the shared documents table. unlinked_at preserves link history without deleting the shared document.
REST namespace:
/api/v1/legal
Legal remains a later vertical.
41. Legal Matters
Suggested fields:
id
organization_id
client_id
matter_number
title
practice_area
responsible_lawyer_user_id
status
opened_date
closed_date
created_at
updated_at
42. Legal Cases
Suggested fields:
id
organization_id
matter_id
case_number
court_id
jurisdiction
case_type
status
filed_date
created_at
updated_at
43. Legal Hearings
Suggested fields:
id
organization_id
case_id
hearing_type
scheduled_at
courtroom
judge
status
notes
44. Legal Conflict Checks
Suggested tables:
legal_conflict_checks
legal_conflict_parties
legal_conflict_matches
Conflict-check fields:
id
organization_id
potential_client_name
matter_description
requested_by_user_id
reviewed_by_user_id
status
decision
decision_reason
created_at
reviewed_at
version
Request example:
{
"potentialClientName": "Acme Corporation",
"relatedParties": [
{
"name": "John Smith",
"relationship": "CEO"
},
{
"name": "Acme Subsidiary LLC",
"relationship": "Subsidiary"
}
],
"matterDescription": "Corporate acquisition"
}
Response may contain possible matches:
{
"data": {
"id": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cbd",
"status": "pending_review",
"potentialConflicts": [
{
"type": "possible_direct_adversity",
"partyName": "Acme Corporation",
"existingMatterId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2ccd",
"existingMatterNumber": "MAT-2026-089"
}
]
}
}
The system should distinguish:
automated possible match
from:
lawyer-approved conflict determination
The software may assist discovery; it should not silently make the professional judgment.
Approvals and declines are auditable commands.
45. Healthcare Tables
Healthcare remains a later vertical.
Minimum planned tables:
healthcare_patients
healthcare_patient_contacts
healthcare_patient_addresses
healthcare_practitioners
healthcare_locations
healthcare_rooms
healthcare_appointments
healthcare_encounters
healthcare_clinical_records
healthcare_clinical_record_versions
healthcare_clinical_record_amendments
healthcare_diagnoses
healthcare_prescriptions
healthcare_insurance_policies
healthcare_allergies
healthcare_medications
All tenant-owned tables carry direct organization_id.
Detailed healthcare interoperability, terminology, and jurisdiction rules require healthcare-specific design before implementation.
46. Healthcare Patients
Core patient:
id
organization_id
patient_number
first_name
middle_name
last_name
date_of_birth
administrative_gender nullable
sex_at_birth nullable
gender_identity nullable
status
created_at
updated_at
version
Exact demographic terminology and allowed values are finalized in the healthcare-domain specification.
Do not make every field mandatory merely because it exists.
Patient Contact
healthcare_patient_contacts:
id
organization_id
patient_id
contact_type
value
is_primary
created_at
updated_at
Patient Address
healthcare_patient_addresses:
id
organization_id
patient_id
address_type
line_1
line_2
city
region
postal_code
country_code
is_primary
created_at
updated_at
Sensitive subresources remain permission-controlled.
47. Healthcare Practitioners
Suggested fields:
id
organization_id
user_id
professional_profile_id
specialty
status
created_at
updated_at
Professional licenses are not duplicated here.
Multiple licenses/credentials live in:
professional_credentials
48. Healthcare Appointments
Suggested fields:
id
organization_id
patient_id
practitioner_id
location_id nullable
room_id nullable
appointment_type
starts_at
ends_at
status
reason
created_at
updated_at
version
Planned supporting tables:
healthcare_locations:
id
organization_id
name
address fields
timezone
status
healthcare_rooms:
id
organization_id
location_id
name
status
Exact scheduling rules are deferred to the healthcare vertical.
49. Healthcare Encounters
Suggested fields:
id
organization_id
patient_id
practitioner_id
appointment_id nullable
encounter_type
reason_for_visit nullable
started_at
ended_at
status
created_at
updated_at
version
Do not add a generic free-form notes field as a substitute for clinical records.
Clinical narrative belongs in governed clinical-record structures.
50. Clinical Records
Use:
healthcare_clinical_records
healthcare_clinical_record_versions
healthcare_clinical_record_amendments
Clinical Record
id
organization_id
patient_id
encounter_id
author_practitioner_id
record_type
sensitivity_level
status
signed_by_practitioner_id
signed_at
created_at
updated_at
version
Clinical Record Version
id
organization_id
record_id
version_number
content_reference or governed content payload
created_by_practitioner_id
created_at
Clinical Record Amendment
id
organization_id
record_id
source_version_id
result_version_id
amended_by_practitioner_id
amendment_type
amendment_reason
created_at
Possible amendment types:
correction
addendum
clarification
Signed/finalized history is preserved.
REST:
POST /api/v1/healthcare/encounters/{encounterId}/clinical-records
GET /api/v1/healthcare/clinical-records/{recordId}
PATCH /api/v1/healthcare/clinical-records/{recordId}
# Draft/editable only.
POST /api/v1/healthcare/clinical-records/{recordId}/sign
POST /api/v1/healthcare/clinical-records/{recordId}/amend
GET /api/v1/healthcare/clinical-records/{recordId}/history
GET /api/v1/healthcare/clinical-records/{recordId}/access-log
51. Documents
Shared document infrastructure:
documents
document_versions
document_categories
retention_policies
Binary data lives in S3-compatible object storage.
Document
id
organization_id
name
category_id
classification
retention_policy_id
current_version_id
created_by_user_id
created_at
updated_at
Classification:
public
internal
confidential
restricted
regulated
Document Version
id
organization_id
document_id
version_number
storage_key
mime_type
size_bytes
content_hash
hash_algorithm
uploaded_by_user_id
created_at
Checksum is version-level authoritative data.
Document Category
id
organization_id
profession nullable
name
parent_category_id
created_at
Uniqueness requirement:
shared category:
unique organization_id + name where profession IS NULL
profession category:
unique organization_id + profession + name where profession IS NOT NULL
Implementation options:
PostgreSQL null-aware unique constraint when supported
or
two partial unique indexes
The partial-index fallback does not depend on selecting PostgreSQL 18.
Retention Policy
id
organization_id
name
profession nullable
classification nullable
retention_period_days nullable
action
created_at
updated_at
Initial actions:
review
archive
delete_when_legally_permitted
retain_indefinitely
retention_period_days has one meaning only:
action = retain_indefinitely
→ retention_period_days MUST be null
all other actions
→ retention_period_days MUST be a positive integer
Null does not mean inherit, unconfigured, or unknown. Policy inheritance or an unconfigured state must be represented outside a persisted retention-policy row and specified separately before implementation.
A retention policy describes configured behavior.
Actual deletion remains subject to domain, contractual, privacy, and jurisdiction requirements.
Metadata
JSONB is allowed only for genuinely extensible, non-authoritative metadata.
Do not put authorization, lifecycle, retention state, or ownership into arbitrary JSON.
52. Document Upload Flow
Standard Upload
Request:
POST /api/v1/documents/upload-url
{
"name": "structural-calculations.pdf",
"categoryId": null,
"classification": "confidential",
"mimeType": "application/pdf",
"sizeBytes": 2457600,
"contentHash": "sha256:..."
}
Response:
{
"data": {
"documentId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2d01",
"documentVersionId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2d02",
"uploadUrl": "https://object-storage.example/...",
"expiresAt": "2026-08-26T13:00:00Z"
}
}
The frontend uploads directly to object storage.
Finalize:
POST /api/v1/documents/{documentId}/complete-upload
{
"documentVersionId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2d02",
"contentHash": "sha256:..."
}
Multipart Initialization
POST /api/v1/documents/multipart-uploads
Request:
{
"name": "building-model.bin",
"categoryId": null,
"classification": "confidential",
"mimeType": "application/octet-stream",
"sizeBytes": 2147483648,
"contentHash": null
}
Response:
{
"data": {
"documentId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2d10",
"documentVersionId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2d11",
"uploadId": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2d12",
"recommendedPartSizeBytes": 67108864,
"expiresAt": "2026-08-27T12:00:00Z"
}
}
Request Signed Part URLs
POST /api/v1/documents/{documentId}/multipart-uploads/{uploadId}/parts
{
"partNumbers": [1, 2, 3, 4]
}
Response:
{
"data": [
{
"partNumber": 1,
"uploadUrl": "https://object-storage.example/..."
}
]
}
Binary parts go directly to object storage.
Complete Multipart Upload
POST /api/v1/documents/{documentId}/multipart-uploads/{uploadId}/complete
{
"parts": [
{
"partNumber": 1,
"etag": "..."
}
],
"contentHash": "sha256:..."
}
Abort:
DELETE /api/v1/documents/{documentId}/multipart-uploads/{uploadId}
Upload state:
initiated
uploading
completing
completed
aborted
expired
Workers clean up abandoned multipart uploads.
Upload policy validates:
declared MIME
extension
content signature
size
checksum
quota
classification
malware status
53. Profession-Specific Document Links
Use explicit relationship tables.
Engineering:
engineering_project_documents
engineering_design_version_documents
engineering_inspection_documents
Legal:
legal_matter_documents
legal_case_documents
Healthcare:
healthcare_patient_documents
healthcare_encounter_documents
engineering_design_version_documents is authoritative for files belonging to a specific design revision.
Do not also maintain an ambiguous engineering_design_documents relation to the unversioned design unless a later requirement introduces a separate clearly named supporting-document relationship.
Project Documents
engineering_project_documents
├── id
├── organization_id
├── project_id
├── document_id
├── category
├── linked_by_user_id
├── linked_at
└── unlinked_at
REST:
GET /api/v1/engineering/projects/{projectId}/documents
POST /api/v1/engineering/projects/{projectId}/documents
DELETE /api/v1/engineering/project-documents/{documentLinkId}
DELETE temporally unlinks the relation when history must be preserved.
Inspection Documents
engineering_inspection_documents fields:
id
organization_id
inspection_id
document_id
category
linked_by_user_id
linked_at
unlinked_at nullable
Both the inspection and document references use tenant-safe foreign keys. Active links are unique by (organization_id, inspection_id, document_id) where unlinked_at IS NULL.
REST:
GET /api/v1/engineering/inspections/{inspectionId}/documents
POST /api/v1/engineering/inspections/{inspectionId}/documents
DELETE /api/v1/engineering/inspection-documents/{documentLinkId}
DELETE sets unlinked_at; it does not delete the shared document or erase inspection history.
54. Billing
Shared financial core:
billing_accounts
invoices
invoice_items
payments
payment_refunds
Billing Account
Invoices reference a shared bill-to identity rather than an unconstrained profession-owned resource.
billing_accounts fields:
id
organization_id
display_name
legal_name nullable
billing_email nullable
billing_address nullable
status
created_at
updated_at
Billing-account status values:
active
inactive
Profession modules own explicit links from their parties to billing accounts. Initial link shapes are:
engineering_client_billing_accounts
├── id
├── organization_id
├── client_id
├── billing_account_id
├── linked_at
└── unlinked_at nullable
legal_client_billing_accounts
├── id
├── organization_id
├── client_id
├── billing_account_id
├── linked_at
└── unlinked_at nullable
Each reference is tenant-safe. Engineering v1 permits one active billing-account link per client where unlinked_at IS NULL; later verticals may define stricter payer rules. Matter, project, encounter, and time-entry provenance remains in profession-owned invoice or invoice-item link tables. Shared billing therefore never depends on profession table internals.
Billing-account REST:
GET /api/v1/billing-accounts
POST /api/v1/billing-accounts
GET /api/v1/billing-accounts/{billingAccountId}
PATCH /api/v1/billing-accounts/{billingAccountId}
Invoice
Core fields include:
id
organization_id
billing_account_id
bill_to_name
bill_to_email nullable
bill_to_address_snapshot nullable
invoice_number
status
currency_code
subtotal_minor
tax_total_minor
total_minor
issued_at
due_at
paid_at
created_at
updated_at
version
Invoice status values:
draft
issued
partially_paid
paid
overdue
void
The bill-to fields are immutable snapshots once the invoice is issued. Updating a billing account later does not rewrite an issued invoice.
(organization_id, billing_account_id) uses a tenant-safe foreign key to billing_accounts(organization_id, id).
Invoice Item
id
organization_id
invoice_id
description
quantity
unit_price_minor
total_amount_minor
position
created_at
updated_at
quantity uses fixed-precision numeric semantics, not floating point.
Money uses integer minor units.
The invoice determines currency; invoice items do not independently choose a different currency unless multi-currency invoicing is intentionally designed later.
Profession-Specific Source Links
The shared billing module does not use unconstrained:
reference_type
reference_id
to profession-owned tables.
Profession modules create explicit links, for example:
engineering_invoice_item_time_entries
├── organization_id
├── invoice_item_id
└── time_entry_id
This preserves the rule that shared core does not depend on profession-table internals.
Invoice-level work context also uses explicit profession-owned links, for example:
engineering_invoice_projects
├── id
├── organization_id
├── invoice_id
├── project_id
└── linked_at
legal_invoice_matters
├── id
├── organization_id
├── invoice_id
├── matter_id
└── linked_at
Healthcare invoice context is deferred to the healthcare billing design. The shared invoice table does not contain reference_type or reference_id.
Payment
payments fields:
id
organization_id
invoice_id
amount_minor
currency_code
payment_method
status
transaction_reference nullable
paid_at nullable
created_at
updated_at
Payment status values:
pending
succeeded
failed
partially_refunded
refunded
voided
payment_method is an application registry with initial values bank_transfer, card, check, cash, and other. amount_minor must be positive, and currency_code must equal the invoice currency.
Payment Refund
Refunds are separate records so partial or repeated refunds remain auditable.
payment_refunds fields:
id
organization_id
payment_id
amount_minor
status
transaction_reference nullable
reason nullable
requested_by_user_id
created_at
refunded_at nullable
Refund status values:
pending
succeeded
failed
The sum of successful refunds may not exceed the successful payment amount. Payment and refund commands are transactional, idempotent, and append audit/outbox records.
REST:
POST /api/v1/invoices
GET /api/v1/invoices
GET /api/v1/invoices/{invoiceId}
PATCH /api/v1/invoices/{invoiceId}
POST /api/v1/invoices/{invoiceId}/issue
POST /api/v1/invoices/{invoiceId}/void
POST /api/v1/invoices/{invoiceId}/payments
POST /api/v1/payments/{paymentId}/refund
55. Money Representation
Use integer minor units:
{
"amountMinor": 12550,
"currency": "USD"
}
Meaning:
$125.50
Never use floating point for money.
56. Audit Logging
Use:
audit_events
Suggested fields:
id
organization_id
actor_type
actor_user_id nullable
actor_service_account_id nullable
action
resource_type
resource_id
request_id
correlation_id
ip_address
user_agent
metadata
occurred_at
Audit records are append-only from normal application workflows.
Mandatory Examples
Engineering:
engineering.projects.create
engineering.projects.close
engineering.designs.approve
engineering.inspections.complete
Legal:
legal.matters.create
legal.matters.close
legal.conflicts.approve
Healthcare:
healthcare.records.read
healthcare.records.write
healthcare.records.sign
healthcare.records.amend
Privacy / Erasure Handling
Append-only audit does not mean "store unlimited personal data forever."
Audit metadata must be minimized at write time.
Where privacy, contractual, or retention obligations require removal of personally identifying material, use a governed privacy process such as:
pseudonymize actor references
null/remove nonessential PII fields
replace identifiers with irreversible privacy references where appropriate
retain the security/business event itself when permitted/required
The exact action depends on jurisdiction and retention policy and must be reviewed before healthcare/legal production.
Do not place passwords, tokens, full clinical content, secret keys, or unnecessary payment data in audit metadata.
REST:
GET /api/v1/audit-events
No public mutation endpoints.
57. Domain Events and Transactional Outbox
Use:
outbox_events
Fields:
id
organization_id nullable for truly global events
event_type
aggregate_type
aggregate_id
payload
request_id nullable
correlation_id
causation_id nullable
occurred_at
available_at
processed_at
attempt_count
last_error
dead_lettered_at
correlation_id groups one logical workflow across requests/jobs/events.
causation_id identifies the event/command that directly caused this event when applicable.
Transaction:
BEGIN
business change
audit event
outbox event
COMMIT
Delivery semantics are at-least-once.
Worker claim uses row locking such as:
SELECT id
FROM outbox_events
WHERE processed_at IS NULL
AND dead_lettered_at IS NULL
AND available_at <= now()
ORDER BY occurred_at
FOR UPDATE SKIP LOCKED
LIMIT 100;
Every external side-effect consumer must be idempotent.
FOR UPDATE SKIP LOCKED prevents simultaneous claiming; it does not prevent duplicate side effects after a worker crash.
57A. Webhooks and External Integrations
Shared tables:
webhooks
webhook_event_subscriptions
webhook_deliveries
Webhook
id
organization_id
url
status
secret_ciphertext or signing_key_reference
created_by_user_id
created_at
updated_at
Webhook status values:
active
paused
disabled
Subscription
id
organization_id
webhook_id
event_type
created_at
Unique:
(organization_id, webhook_id, event_type)
Only registered externally publishable event types may be subscribed.
Delivery
id
organization_id
webhook_id
event_id
attempt_number
request_timestamp
response_status
response_summary
delivered_at
failed_at
next_attempt_at
event_id references outbox_events.id. Because webhook deliveries are tenant-owned, only publishable outbox events with the same non-null organization_id may be delivered:
(organization_id, event_id)
→ outbox_events(organization_id, id)
The webhook publisher allowlists externally publishable event_type values before creating delivery records. The stable outbox event ID is also the consumer deduplication key.
Configuration REST:
GET /api/v1/webhooks
POST /api/v1/webhooks
GET /api/v1/webhooks/{webhookId}
PATCH /api/v1/webhooks/{webhookId}
DELETE /api/v1/webhooks/{webhookId}
POST /api/v1/webhooks/{webhookId}/test
POST /api/v1/webhooks/{webhookId}/rotate-secret
Delivery REST:
GET /api/v1/webhook-deliveries
GET /api/v1/webhook-deliveries/{deliveryId}
POST /api/v1/webhook-deliveries/{deliveryId}/retry
If HMAC signing is used, signing material is encrypted/recoverable with managed key protection.
A one-way secret hash is insufficient for outbound HMAC signing.
Webhook consumers deduplicate using stable event IDs.
58. Background Jobs
Workers handle:
notifications
reports/PDFs
file scanning
document processing
imports
exports
bulk operations
webhooks
search indexing
large data operations
Use shared tenant-owned:
jobs
Fields:
id
organization_id
requested_by_user_id
job_type
status
input_reference
result_reference
progress_percent
created_at
started_at
completed_at
failed_at
error_code
error_summary
Initial job_type values:
project_export
time_entry_import
report_generation
bulk_operation
job_type is an application registry, not a client-defined free-form value or a PostgreSQL enum. Each registered type defines its handler, input-reference schema, result-reference schema, authorization policy, retry policy, and idempotency behavior. Unknown job types are rejected before a job row is created.
input_reference and result_reference are nullable typed JSONB reference envelopes, not arbitrary blobs or public URLs. Their schema is registered per job_type.
Allowed reference kinds initially include:
document_version
object_storage_key
query_snapshot
job
Object-storage references contain internal storage keys; APIs generate time-limited signed URLs when access is authorized. Resource IDs inside an envelope are validated for tenant ownership when the job is created. Large inputs and outputs live in documents or object storage rather than inside the job row.
States:
queued
running
completed
failed
cancelled
REST:
GET /api/v1/jobs/{jobId}
GET /api/v1/jobs/{jobId}/result
POST /api/v1/jobs/{jobId}/cancel
These endpoints are tenant-scoped through the standard:
X-Organization-Id
They do not need /organizations/{id}/jobs because the platform already chose header-based tenant context.
Large import/export operations return:
202 Accepted
with a job ID.
59. Redis
Use Redis as an acceleration and coordination layer, not the authoritative system of record.
Appropriate uses:
job queue
rate-limit counters
short-lived authorization caches
organization configuration cache
session lookup acceleration
idempotency lookup acceleration
distributed locks when justified
Cache Layers
L1 optional application-memory cache:
static permission definitions
non-sensitive configuration
L2 Redis shared cache:
organization settings
membership snapshots
role permission snapshots
rate-limit counters
session lookup cache
recent idempotency lookups
CDN:
frontend static assets
explicitly public assets only
Do not cache private professional API responses at a CDN by default.
Cache Invalidation
Invalidate or version caches when:
membership changes
role permissions change
organization settings change
professional credentials change
session is revoked
profession module enablement changes
High-risk authorization decisions must not depend solely on stale cached credential state.
Idempotency Durability
Redis may improve idempotency lookup latency, but PostgreSQL remains authoritative for high-risk commands.
60. Pagination
Use cursor pagination.
Defaults:
default limit = 25
maximum limit = 100
offset pagination = not supported
Example:
GET /api/v1/engineering/projects?limit=25
Response:
{
"data": [],
"meta": {
"pagination": {
"nextCursor": null,
"hasMore": false
}
}
}
Rules:
cursor is opaque
sort order must be deterministic
cursor encodes/represents the selected sort position
unsupported limits return validation errors rather than silent huge responses
61. Filtering
Use explicit resource-specific filters.
Examples:
GET /api/v1/engineering/projects?status=active&discipline=structural
GET /api/v1/engineering/tasks?status=todo&assignedToUserId=0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c3d
Do not build a generic query DSL in v1.
62. Sorting
Examples:
GET /api/v1/engineering/projects?sort=createdAt
GET /api/v1/engineering/projects?sort=-createdAt
Only explicitly supported fields may be sorted.
63. Search
Start with PostgreSQL search.
Engineering search may cover:
project number
project name
client name
Legal:
matter number
client
case number
Healthcare:
patient number
patient identity
Healthcare search requires stricter privacy and authorization controls.
Potential PostgreSQL capabilities:
- B-tree indexes for exact/filter queries
- PostgreSQL full-text search where appropriate
pg_trgmonly when fuzzy search requirements justify it
Do not introduce Elasticsearch/OpenSearch until real query volume, relevance requirements, or indexing features justify another distributed system.
Do not create every conceivable search index on day one. Indexes cost memory, storage, and write performance.
64. Optimistic Concurrency
Important mutable resources should use a version field.
Example:
{
"id": "0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c5d",
"version": 6
}
Update:
{
"version": 6,
"name": "Central Tower Phase II"
}
If the current database version differs:
409 CONCURRENT_MODIFICATION
65. Domain-Oriented REST
Important state transitions use explicit command endpoints.
Good:
POST /engineering/projects/{id}/close
POST /engineering/designs/{id}/approve
POST /engineering/tasks/{id}/complete
POST /engineering/inspections/{id}/complete
POST /invoices/{id}/issue
Avoid:
PATCH /resource/{id}
{
"status": "approved"
}
when the change has significant rules or side effects.
66. Transaction Boundaries
Create project:
BEGIN
create project
assign project manager
write audit event
write outbox event
COMMIT
Approve design:
BEGIN
validate permission
validate project access
validate credentials
validate design state
create review result
mark approved
write audit event
write outbox event
COMMIT
67. Request Context
Every authenticated request should resolve:
RequestContext
{
requestId
userId
sessionId
organizationId
membershipId
permissions
}
Profession modules consume this context.
68. Request IDs
Every request has:
X-Request-Id
If missing, the server generates one.
Use it in:
- logs
- audit context
- error diagnostics
- asynchronous correlation
69. OpenAPI
Maintain:
openapi.yaml
Use OpenAPI 3.1.
Production server example:
servers:
- url: https://api.example.com/api/v1
The server URL and path definitions must remain consistent with the platform base path.
OpenAPI defines:
- routes
- request DTOs
- response DTOs
- security schemes
- organization header
- request IDs
- idempotency header
- pagination
- filters
- error schemas
- examples
- profession tags
Security scheme:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Reusable headers/parameters:
X-Organization-Id
X-Request-Id
Idempotency-Key
limit
cursor
CI must validate the OpenAPI document.
Contract tests should detect drift between implementation and specification.
Generated clients may be used by the separate frontends, but generated transport code should not dictate frontend domain architecture.
70. DTO Rule
Database models are not public API contracts.
Use:
Request DTO
Response DTO
A database migration should not accidentally change the public API.
71. Backend Module Structure
Recommended:
src/
├── core/
│ ├── auth/
│ ├── organizations/
│ ├── memberships/
│ ├── authorization/
│ ├── documents/
│ ├── billing/
│ ├── audit/
│ └── events/
│
├── engineering/
│ ├── clients/
│ ├── projects/
│ ├── project-members/
│ ├── phases/
│ ├── sites/
│ ├── tasks/
│ ├── designs/
│ ├── inspections/
│ └── specifications/
│
├── legal/
│ ├── clients/
│ ├── matters/
│ ├── cases/
│ ├── hearings/
│ ├── conflicts/
│ └── retainers/
│
└── healthcare/
├── patients/
├── practitioners/
├── appointments/
├── encounters/
├── records/
└── prescriptions/
72. Internal Module Structure
Example:
projects/
├── domain/
│ ├── project.entity.ts
│ ├── project-status.ts
│ └── project.errors.ts
│
├── application/
│ ├── commands/
│ │ ├── create-project.ts
│ │ ├── update-project.ts
│ │ └── close-project.ts
│ │
│ └── queries/
│ ├── get-project.ts
│ └── list-projects.ts
│
├── infrastructure/
│ └── project.repository.ts
│
└── api/
├── project.controller.ts
├── project.request.ts
└── project.response.ts
73. Controllers
Controllers should handle:
HTTP
authentication context
input DTO parsing
application command/query invocation
response mapping
Controllers should not contain:
business rules
raw SQL
role logic
transaction orchestration
email sending
audit implementation
74. Commands and Queries
Mutations use commands.
Examples:
CreateEngineeringProjectCommand
ApproveEngineeringDesignCommand
CloseLegalMatterCommand
CompleteHealthcareEncounterCommand
Reads use queries.
Examples:
GetEngineeringProjectQuery
ListLegalMattersQuery
GetHealthcarePatientQuery
75. Repositories
Use domain-specific repositories.
Examples:
EngineeringProjectRepository
LegalMatterRepository
HealthcarePatientRepository
Avoid one massive generic repository abstraction that eventually needs dozens of flags.
76. Security Baseline
Minimum controls:
TLS everywhere
strong password hashing
short-lived access tokens
refresh-token rotation/reuse detection
server-side session revocation
rate limiting
anti-automation controls
RBAC
resource policies
credential-aware authorization
tenant isolation
input validation
SQL injection protection
signed object-storage URLs
file-content validation
malware scanning
audit trails
secret management
encryption at rest
dependency/image scanning
request/correlation IDs
backup and restore testing
Web Security / CORS
ADR-009 defines environment-specific web security.
Baseline requirements:
explicit CORS allowlist
no wildcard credentialed CORS
allowed methods/headers documented
preflight behavior tested
HSTS at the edge for production HTTPS
X-Content-Type-Options: nosniff
secure cookie attributes when cookies are used
CSP on browser frontends
frame-ancestor/clickjacking policy on frontends
referrer policy appropriate to the frontend
Security headers belong at the appropriate application/CDN/gateway layer.
Rate Limiting
Policies are endpoint-specific and configurable.
Return:
429 Too Many Requests
Retry-After: ...
Secrets
Production secrets live outside source control, preferably in managed secret/key systems.
JWT signing keys support rotation.
77. Data Classification
Suggested classes:
Public
marketing configuration
Internal
organization settings
tasks
Confidential
engineering documents
legal matters
billing
Highly Sensitive
clinical records
professional credentials
authentication secrets
78. Healthcare Security
Before healthcare production use, define:
privacy model
minimum-necessary access model
clinical access policies
break-glass/emergency access policy if required
audit policy
record-signing policy
amendment policy
retention policy
credential policy
scope-of-practice policy
jurisdiction requirements
encryption strategy
consent requirements
data residency requirements
backup/restore handling
export/portability requirements
breach-response requirements
Healthcare is a stricter security tier.
Key rules:
- default patient responses do not contain all available PHI
- clinical record reads may be auditable events
- signed records are immutable except through explicit amendment/version workflows
- prescribing authorization is jurisdiction-specific
- privileged clinical commands revalidate professional authority
- caches must not allow revoked credentials to remain effective for high-risk writes
- healthcare search results themselves are protected data
- access logs may require dedicated permissions
- do not claim regulatory compliance from architecture alone
79. Observability
Use:
structured logs
metrics
distributed tracing
request IDs
correlation IDs
Recommended:
OpenTelemetry
Core Metrics
API:
api_requests_total
api_errors_total
api_request_duration_seconds
Authentication:
auth_login_attempts_total
auth_token_refresh_total
auth_refresh_reuse_detections_total
auth_sessions_revoked_total
Authorization/security:
cross_tenant_access_attempts_total
tenant_isolation_invariant_failures_total
authorization_denials_total
credential_policy_denials_total
rate_limit_events_total
Important distinction:
cross_tenant_access_attempt
=
request attempted another tenant's resource
This may be a stale link, mistake, or attack.
tenant_isolation_invariant_failure
=
our system nearly or actually created/returned cross-tenant data
That is a high-severity internal correctness/security incident.
Outbox/jobs/webhooks:
outbox_events_pending
outbox_events_failed_total
outbox_processing_duration_seconds
jobs_queued
jobs_failed_total
job_duration_seconds
webhook_delivery_attempts_total
webhook_delivery_failures_total
webhook_delivery_latency_seconds
Database:
db_pool_active
db_pool_waiting
db_query_duration_seconds
db_transaction_duration_seconds
Business metrics may include:
engineering_projects_created_total
engineering_designs_approved_total
engineering_inspections_completed_total
invoices_issued_total
Avoid patient-specific or sensitive identifiers in metric labels.
Alerts
Examples:
refresh token reuse detected
tenant isolation invariant failure
outbox backlog exceeds SLO
webhook failure spike
database pool saturation
error-rate spike
latency regression
backup failure
malware scanner unavailable
Thresholds are calibrated from real environments rather than copied from a review document.
SLOs
Define by endpoint class.
Interactive CRUD, reports, file orchestration, and background jobs should not share one arbitrary latency target.
80. Logging
Useful fields:
request_id
route
method
status
duration
user_id when appropriate
organization_id when appropriate
Never log:
passwords
tokens
clinical record text
full sensitive documents
payment secrets
81. Testing Strategy
Unit Tests
Test:
domain rules
state transitions
authorization policies
credential policies
money calculations
idempotency request hashing
Property-Based Tests
Use property-based testing for high-value domain state machines.
Candidates:
engineering design lifecycle
engineering inspection lifecycle
invoice lifecycle
payment state transitions
membership/role invariants
Correct properties:
every successful transition ends in a valid state
every forbidden transition is rejected
terminal states reject prohibited actions
required invariants survive every valid transition
transition sequences never bypass required approval/credential rules
Do not assert that every random state/action pair succeeds. Many are supposed to fail.
Integration Tests
Test:
repositories
tenant-aware foreign keys
PostgreSQL constraints
transactions
outbox persistence
idempotency persistence
cache invalidation
job persistence
webhook delivery persistence
API Tests
Every important endpoint covers:
happy path
request validation
authentication
organization context
permission denial
scope denial
credential denial where relevant
cross-tenant access
concurrent modification
invalid state transition
idempotent replay
idempotency conflict
audit creation
outbox creation
Outbox Reliability / Chaos Tests
Test:
worker crash before side effect
worker crash after side effect but before marking processed
two workers competing for same row
temporary dependency outage
retry/backoff behavior
dead-letter behavior
consumer idempotency
lost worker wake-up
replay
The dangerous scenario is:
external side effect succeeds
worker dies
event retries
Tests must prove the consumer does not create an unacceptable duplicate.
Tenant Security Tests
Test both:
external cross-tenant access attempts
and:
internal cross-tenant data invariant failures
These are different classes of failure.
Performance Tests
Create realistic profiles:
interactive reads
interactive writes
search
dashboard read models
reporting
file upload orchestration
outbox processing
webhook bursts
notification bursts
Measure:
p50
p95
p99
throughput
error rate
database saturation
queue backlog
Set production SLO gates only after a realistic baseline exists.
Coverage
Track code coverage.
Do not treat a single percentage such as 90% as proof of quality.
Critical-path expectations are stronger:
all tenant-isolation paths tested
all financial commands tested
all regulated commands tested
all state transitions tested
all critical authorization policies tested
82. Tenant Security Tests
For every major resource, attempt:
Organization A resource
using Organization B context
Test:
read
update
delete/action
list filtering
search
documents
Expected result:
404 / denied
83. Engineering MVP
Engineering is the first vertical.
Initial features:
Authentication
Organization management
Users / memberships / roles
Engineering clients
Projects
Project members
Project phases
Tasks
Sites
Documents
Basic design records
Inspections
Time entries
Basic billing
Audit history
Do not initially build:
advanced CAD integration
BIM integration
full document markup
advanced resource planning
procurement
complex accounting
AI design analysis
IoT integrations
84. Engineering MVP Workflow
User registers
↓
Creates engineering organization
↓
Invites engineer
↓
Assigns role
↓
Creates client
↓
Creates project
↓
Assigns project team
↓
Creates project phases
↓
Creates tasks
↓
Uploads documents
↓
Creates design
↓
Reviews / approves design
↓
Schedules inspection
↓
Records inspection findings
↓
Records engineering time
↓
Creates invoice
↓
Records payment
↓
Closes project
↓
Audit history contains lifecycle
85. Development Phases
Phase 0: Architecture Foundation
Deliver:
domain boundaries
database conventions
REST conventions
authorization model
session/token model
idempotency strategy
error taxonomy
OpenAPI skeleton
engineering state machines
migration conventions
threat model
initial ADRs
risk register
Phase 1: Shared Platform Core
Build:
auth
sessions
refresh-token families
token rotation/revocation
users
organizations
organization professions
membership invitations
memberships
roles
permissions
authorization
audit
outbox
request context
idempotency
rate limiting
observability
Phase 2: Engineering CRM
Build:
engineering clients
engineering client contacts
client archive/restore
Phase 3: Engineering Projects
Build:
projects
project members
project phases
activation/close/archive
Phase 4: Work and Site Management
Build:
tasks
task batch operations
sites
Phase 5: Documents
Build:
documents
versions
categories
classification
retention references
signed uploads
multipart uploads
content verification
malware scanning
engineering document links
Phase 6: Engineering Designs
Build:
designs
assignments
versions
reviews
cancel/withdraw semantics
credential-aware approval
audit
outbox
idempotency
Phase 7: Engineering Inspections
Build:
inspection lifecycle
inspection outcome
findings
corrective work
follow-up inspections
attachments
audit
outbox
idempotency
Phase 8: Time, Budgets, and Billing
Build:
time entries
batch timesheet submission
project budgets when required
invoices
payments
financial idempotency
reconciliation
Phase 9: Notifications, Jobs, and Webhooks
Build:
notifications
email
async jobs
imports/exports
webhooks
delivery/retry
dead-letter handling
Phase 10: Reporting and Search
Build:
project status
overdue work
inspection status
billable time
revenue
outstanding invoices
dashboard read models
Phase 11: Engineering Client Portal
Build:
portal account invitations
external project grants
published project documents
client review/acceptance workflow
portal audit
portal-specific frontend
Do not expose professional approval actions to client portal accounts.
Phase 12: Legal Vertical
Validate shared core against:
matters
cases
conflicts
deadlines
retainers
restricted access / ethical walls
Phase 13: Healthcare Readiness and Vertical
Before implementation:
healthcare threat model
privacy review
jurisdiction analysis
scope-of-practice policy
record signing/amendment model
retention model
audit requirements
Estimation Rule
These are dependency-ordered milestones.
They are not calendar promises.
Calendar estimates require:
team size
frontend/UX scope
cloud decisions
third-party providers
security requirements
QA capacity
domain-expert availability
86. Legal Expansion
Only after engineering proves the shared platform assumptions.
Build:
Legal Client
↓
Matter
↓
Case
↓
Hearings / Deadlines / Documents
Do not redesign engineering around legal terminology.
Extract only genuinely reusable infrastructure.
87. Healthcare Expansion
Healthcare comes after:
- core platform is stable
- audit model is proven
- permission model is proven
- tenant isolation is tested
- retention and encryption strategies are defined
Healthcare should be treated as its own security and compliance workstream.
88. Deployment Environments
Use:
development
testing
staging
production
Each environment has independent:
database
object storage
secrets
queues
API keys
89. Initial Deployment Architecture
CDN
│
├── Engineering Web
├── Legal Web
└── Healthcare Web
Load Balancer
│
Backend API
│
├── PostgreSQL
├── Redis
├── Object Storage
└── Queue
│
Workers
Prefer managed infrastructure where practical.
90. Backup Strategy
Database:
automated backups
point-in-time recovery
tested restores
Object storage:
versioning
retention policies
backup or replication where required
A backup strategy is incomplete until restoration is tested.
91. Migration Strategy
Use explicit immutable migration files.
Recommended naming:
YYYYMMDDHHMMSS_description.sql
Example:
20260826010000_create_organizations.sql
20260826011000_create_users.sql
20260826012000_create_memberships.sql
20260826013000_create_rbac.sql
20260826014000_create_audit_outbox.sql
20260826015000_create_engineering_clients.sql
UUID Standard
The platform uses UUIDv7.
Supported implementation choices:
PostgreSQL 18+:
use native uuidv7() if database-generated identifiers are desired
Earlier PostgreSQL:
generate UUIDv7 in the application or use a controlled extension
Database columns remain PostgreSQL UUID.
The rule is consistency, not ideological loyalty to one generation layer.
Do not silently fall back to UUIDv4 while documenting UUIDv7.
Production Migration Rules
Use expand/contract:
1. add backward-compatible schema
2. deploy code supporting old + new schema
3. backfill/migrate
4. switch reads/writes
5. observe
6. remove obsolete schema later
For destructive changes:
backup/restore plan
compatibility window
production-like dry run
explicit approval
post-migration verification
Do not assume a destructive database migration can always be reversed by a simple down migration.
Never use automatic ORM schema synchronization in production.
91A. Architecture Decision Records
v4 stops treating technology suggestions as automatically settled architecture.
Create ADRs before implementation locks in:
ADR-001 Backend Framework
ADR-002 SQL / ORM / Query Layer
ADR-003 Queue Implementation
ADR-004 PostgreSQL Minimum Version
ADR-005 Error Format / RFC 9457 Compatibility
ADR-006 Rate-Limit Header Convention
ADR-007 Webhook Signing Strategy
ADR-008 Object Storage Provider / Multipart Strategy
ADR-009 Web Security / CORS / Browser Headers
ADR-010 Machine Authentication / API Key Policy
Each ADR should include:
context
decision
alternatives considered
tradeoffs
security impact
operational impact
migration/exit path
date
status
The architecture currently fixes capabilities and boundaries.
It does not require a framework merely because a review document described it positively.
92. Technology Recommendation
The following are preferred candidates, not all final decisions.
Fixed Platform Choices
API style: REST
Contract: OpenAPI 3.1
Primary language: TypeScript
Primary database: PostgreSQL
Architecture: Modular Monolith
Observability standard: OpenTelemetry
Object storage model: S3-compatible
Container model: Docker/OCI
ADR-Gated Choices
Backend framework candidates:
NestJS
Fastify-centered custom application structure
SQL / persistence candidates:
Drizzle
Kysely
Prisma
direct SQL for specialized queries
Queue candidates:
BullMQ / Redis
managed cloud queue
PostgreSQL baseline:
PostgreSQL 18+
is attractive because of native UUIDv7 and current capabilities, but the minimum supported version must be confirmed against:
hosting provider availability
operations policy
extension requirements
upgrade policy
support lifecycle
Do not claim one ORM is categorically "faster" or "better" without workload-specific evidence.
The selected stack should preserve:
transaction control
explicit SQL visibility
tenant-safe query design
migration control
observability
testability
93. REST API Milestones
Milestone 1: Platform Access and Security
POST /auth/register
POST /auth/login
POST /auth/token/refresh
POST /auth/token/revoke
POST /auth/token/revoke-all
GET /auth/sessions
DELETE /auth/sessions/{sessionId}
GET /me
POST /organizations
GET /me/organizations
POST /membership-invitations
GET /memberships
GET /roles
POST /roles
GET /permissions
Includes:
explicit organization context
session revocation
refresh-token reuse detection
audit foundation
outbox foundation
idempotency foundation
rate limiting
Milestone 2: Engineering Clients
GET /engineering/clients
POST /engineering/clients
GET /engineering/clients/{id}
PATCH /engineering/clients/{id}
POST /engineering/clients/{id}/archive
POST /engineering/clients/{id}/restore
GET /engineering/clients/{id}/projects
Milestone 3: Engineering Projects
GET /engineering/projects
POST /engineering/projects
GET /engineering/projects/{id}
PATCH /engineering/projects/{id}
POST /engineering/projects/{id}/activate
POST /engineering/projects/{id}/close
POST /engineering/projects/{id}/archive
GET /engineering/projects/{id}/summary
Timeline and budget read models follow when the frontend requires them.
Milestone 4: Collaboration
POST /engineering/projects/{id}/members
GET /engineering/projects/{id}/members
POST /engineering/tasks
GET /engineering/tasks
POST /engineering/tasks/{id}/complete
Milestone 5: Sites and Documents
Build:
engineering sites
signed file uploads
document versions
malware scanning
project document links
Milestone 6: Designs
Build:
design lifecycle
versions
reviews
submit-review
request-changes
approve
reject
supersede
credential validation
audit + outbox + idempotency
Milestone 7: Inspections
Build:
schedule
start
complete
cancel
findings
finding resolution
audit + outbox + idempotency
Milestone 8: Commercial Workflows
Build:
time entries
invoices
payments
refunds
financial idempotency
reports
94. Architecture Rules to Freeze
- REST is the primary frontend and integration API.
- Base path is
/api/v1. - OpenAPI 3.1 is the public API contract.
- GraphQL is not part of v1.
- Start as one modular monolith backend.
- Each profession has its own frontend.
- Each profession owns its domain tables and state machines.
- Shared modules provide infrastructure, not forced domain abstractions.
- Public serialized IDs are raw UUIDv7.
- Database ID columns use PostgreSQL UUID.
- Human-readable business references are separate from resource IDs.
- Every tenant-owned row carries direct
organization_id. - Tenant-scoped requests require explicit
X-Organization-Id. - Tenant boundaries are enforced in queries and database constraints.
- Cross-tenant resources appear nonexistent.
- API JSON/query parameter names use camelCase; DB identifiers use snake_case.
- Authorization is server-side and deny-by-default.
assignedscope is defined per resource policy, never inferred generically.- Roles and professional credentials are separate.
- A professional profile may own multiple credentials.
- Sessions and refresh tokens are separate resources.
- Refresh tokens rotate within families and support reuse detection.
- Machine identities use service accounts/API keys, not fake human memberships.
- Important domain transitions use explicit REST command endpoints.
- High-risk commands use durable idempotency.
- Batch custom actions use
/{collection}/batch/{action}. - Every batch defines atomic or partial semantics.
- Every batch item receives independent authorization/domain validation.
- Large batches become asynchronous jobs.
- Project phases are authoritative; duplicated project
stageandcurrent_phase_idare not stored in v1. - Project budgets use the dedicated budget model and project
budget_minoris not authoritative; shared invoices referencebilling_accountsand never unconstrained polymorphic profession resources. - Engineering time entries may attribute time to one explicit primary work item using tenant- and project-consistent composite foreign keys.
- Design versions and engineering specifications use explicit document-link tables with one-to-many cardinality and separate role registries.
- All design/assignment/review/version/inspection-document/finding/follow-up subresources carry
organization_id. - Inspection lifecycle and inspection outcome are separate.
- Inspection follow-ups are explicit resources.
- Engineering change requests remain deferred until fully specified.
- Shared documents own document records; profession modules own link tables.
- Legal does not duplicate shared document ownership.
- Large files use object-storage multipart uploads.
- Application servers do not proxy multi-gigabyte chunks.
- Document checksums belong to document versions.
- Document classification is multi-level.
- Document retention is explicit policy.
- Document category uniqueness must work for nullable profession values on the selected PostgreSQL version.
- Project document linkage does not imply client-portal publication.
- External publication requires explicit publication records.
- Client portal accounts are not internal memberships.
- Client acceptance is not professional engineering approval.
- Domain events use a transactional outbox.
- Outbox delivery is at-least-once.
- Outbox events carry correlation/causation identifiers.
- External side-effect consumers are idempotent.
- Webhook subscriptions and deliveries are tenant-owned.
- HMAC signing secrets are securely recoverable/encrypted, not only hashed.
- Jobs are tenant-scoped by the standard organization header, and every
job_typeis registered with schemas, authorization, retry, and idempotency behavior. - PostgreSQL is the authoritative transactional datastore.
- Redis is acceleration/coordination, not critical source of truth.
- Search starts with PostgreSQL.
- Collections use cursor pagination, default 25 and max 100.
- Important mutable resources use optimistic concurrency.
- Database entities are not serialized directly.
- Errors use stable codes.
429responses useRetry-After; exact quota headers are an API decision.- Business records use explicit archive/revoke/unlink/hard-delete lifecycle policies.
- Financial/professional/audit records are not casually hard-deleted.
- Audit metadata is minimized and supports governed privacy transformation when required.
- Important/regulated actions are audited.
- Signed clinical records use sign/amend/version workflows.
- Prescribing authority remains jurisdiction/scope-of-practice policy.
- Production migrations use expand/contract.
- Destructive changes are not assumed trivially reversible.
- Secrets remain outside source control.
- CORS and browser security policy are explicit ADR/configuration.
- Rate limits are calibrated by evidence.
- CI validates types, tests, OpenAPI, migrations, and security checks.
- Property-based tests cover high-value state machines.
- Outbox/job/webhook reliability is tested under failure/concurrency.
- Critical-path tests matter more than vanity coverage percentages.
- Framework/ORM/queue/PostgreSQL-minimum choices require ADRs.
- Engineering is the first vertical.
- Client portal follows internal Engineering MVP foundations.
- Legal follows after Engineering validates shared assumptions.
- Healthcare requires dedicated privacy/security/domain design before implementation.
- Architecture documentation never equates "designed for" with "certified/compliant".
95. Required Design Artifacts
Maintain:
01_PROJECT_ARCHITECTURE.md
02_DATABASE_CONVENTIONS.md
03_AUTHORIZATION_MODEL.md
04_AUTH_SESSION_MODEL.md
05_ENGINEERING_DOMAIN.md
06_ENGINEERING_DATABASE_SCHEMA.md
07_ENGINEERING_STATE_MACHINES.md
08_API_CONVENTIONS.md
09_ENGINEERING_API_SPEC.md
10_OPENAPI.yaml
11_FRONTEND_ARCHITECTURE.md
12_CLIENT_PORTAL_SECURITY_MODEL.md
13_DOCUMENT_SECURITY_MODEL.md
14_LARGE_FILE_UPLOAD_MODEL.md
15_WEBHOOK_INTEGRATION_MODEL.md
16_ASYNC_JOB_MODEL.md
17_SECURITY_MODEL.md
18_DEPLOYMENT_ARCHITECTURE.md
19_OBSERVABILITY_MODEL.md
20_TESTING_STRATEGY.md
21_ARCHITECTURE_DECISION_RECORDS/
22_RISK_REGISTER.md
23_MVP_BACKLOG.md
Important ADRs:
backend framework
persistence/query layer
queue implementation
PostgreSQL minimum version
error format
rate-limit headers
webhook signing
object-storage provider
96. Recommended Implementation Order
Foundation
↓
Authentication
↓
Organizations
↓
Memberships
↓
RBAC
↓
Engineering Clients
↓
Engineering Projects
↓
Project Team
↓
Tasks
↓
Sites
↓
Documents
↓
Designs
↓
Inspections
↓
Time Tracking
↓
Billing
↓
Notifications
↓
Reports
↓
Legal Vertical
↓
Healthcare Vertical
97A. Database Indexing Strategy
All tenant-owned tables need efficient tenant scoping.
Baseline:
(organization_id, id)
Common list access often benefits from:
(organization_id, created_at)
Query-specific examples:
(organization_id, status)
(organization_id, client_id)
(organization_id, project_id)
(organization_id, assigned_to_user_id)
Rules
- every index corresponds to a known query, ordering, or constraint
- column order follows real predicates
- validate with
EXPLAIN (ANALYZE, BUFFERS) - include production-like cardinality in testing
- measure write amplification
- do not index every field
- introduce trigram/full-text indexes only for actual search requirements
Potential later tools:
covering indexes
materialized views
read replicas
table partitioning
external search
These are evidence-driven scaling mechanisms, not baseline dependencies.
Document Category Uniqueness
If a nullable field such as profession participates in uniqueness:
organization_id
profession nullable
name
do not assume plain uniqueness treats NULL as one shared value.
Use PostgreSQL-supported null-aware uniqueness or partial unique indexes according to the selected PostgreSQL version.
97B. CI/CD and Deployment Gates
Pipeline stages:
lint/typecheck
↓
unit tests
↓
integration tests
↓
OpenAPI validation + contract tests
↓
security/dependency scan
↓
container build + image scan
↓
migration compatibility check
↓
deploy development
↓
smoke tests
↓
deploy staging
↓
E2E + performance/security baseline
↓
manual production approval
↓
production deployment
↓
post-deploy verification
Production deployment should support:
rolling or blue/green application deployment
backward-compatible database migrations
health checks
fast application rollback
feature flags for incomplete features
observability gates
Database schema rollback is not treated as equivalent to application rollback.
Feature Flags
Feature flags used for deployment safety are operational configuration, not automatically a business database table.
Initial implementation may use:
environment/config-service flags
for global rollout and kill switches.
If per-organization feature rollout is later required, introduce an explicit tenant-owned model such as:
organization_feature_flags
through an ADR/migration.
Do not overload organization_professions with unrelated product experiments.
Configuration and Secrets
Non-secret configuration may use environment variables.
Secrets should use a managed secret store where possible:
database credentials
Redis credentials
JWT/private signing keys
object storage credentials
SMTP/API provider credentials
monitoring credentials
Do not publish real secrets in sample configuration.
Organization profession enablement remains primarily data-driven through organization_professions.
Global feature flags may be used for staged rollout, kill switches, or incomplete features.
97C. Review-Driven Deferred Decisions
The following ideas are valid possibilities but are explicitly not frozen into v1:
read replicas
materialized views
Elasticsearch/OpenSearch
universal 100 MB file limit
fixed 100 req/min user limit
fixed 1000 req/hour organization limit
specific cache-hit-ratio target
specific p95 latency promise
database-per-tenant
microservices
GraphQL
These require evidence from:
load tests
security analysis
customer requirements
compliance requirements
real production workloads
This prevents benchmark-shaped guesses from becoming architecture law.
97D. Provisional Performance Objectives
Performance numbers in architecture are starting hypotheses, not guarantees.
Initial engineering objectives may begin with:
Interactive read:
target p95 <= 500 ms
Interactive mutation:
target p95 <= 750 ms
Simple list/search:
target p95 <= 800 ms
Upload authorization:
target p95 <= 300 ms
Background outbox pickup:
target <= 5 seconds under normal operating conditions
These are revised after realistic testing.
Track:
p50
p95
p99
throughput
error rate
database saturation
queue backlog
outbox lag
Different endpoint classes receive different SLOs.
Do not use file-transfer completion time as an API SLO when bytes travel directly between client and object storage.
97E. Risk Register
Maintain a living risk register.
Suggested structure:
| Risk | Impact | Mitigation | Owner | Phase | Status |
|---|---|---|---|---|---|
| Cross-tenant data exposure | Critical | Tenant-aware FKs, scoped queries, security tests | Backend/Security | P0 | Open |
| Non-idempotent outbox side effect | Critical | Consumer dedupe, provider idempotency, chaos tests | Backend | P0 | Open |
| Migration failure | High | Expand/contract, dry runs, backups | Backend/Platform | P0 | Open |
| Engineering workflow mismatch | High | Domain expert validation | Product/Engineering SME | MVP | Open |
| Portal authorization leak | Critical | Separate external access model, publication grants | Backend/Security | Portal | Open |
| Webhook delivery instability | Medium | Retry, dead-letter, replay, metrics | Backend | Integrations | Open |
| Large upload abandonment | Medium | Multipart expiry and cleanup | Backend/Platform | Documents | Open |
| Documentation drift | Medium | OpenAPI validation, ADRs, CI | Engineering | Continuous | Open |
Do not pretend likelihood labels are quantitative unless the team defines and uses a scoring method.
97F. Architecture Change Governance
v4 is the last broad platform-architecture revision before Engineering MVP implementation.
New discoveries should normally become:
ADR
OpenAPI change
database migration
domain-state-machine update
security decision
backlog item
runbook
rather than a new full architecture rewrite.
Reopen the broad architecture only when a discovery invalidates one of these foundational assumptions:
tenant model
profession separation
shared-core boundary
REST API model
data ownership
security trust boundary
deployment topology
database architecture
This prevents design review from becoming an infinite recursion problem.
97G. Production Readiness Gates
Architecture being coherent does not mean production is safe.
Before production, require evidence in these categories.
Security
TLS configured
password hashing configured
refresh rotation/reuse detection tested
session revocation tested
tenant isolation tests passing
authorization/credential policies tested
rate limiting active
secrets managed outside source control
file security scanning active
security review completed
Reliability
database backups automated
restore tested
object storage recovery strategy tested
outbox monitoring active
job queue monitoring active
webhook retry/dead-letter behavior tested
health checks configured
dependency failures tested
Data Integrity
tenant-aware foreign keys present where required
financial invariants tested
migration tested on production-like data
idempotency tested for high-risk commands
optimistic concurrency tested
audit integrity tested
Contract / API
OpenAPI validates
contract tests pass
error schema consistent
versioning rules documented
client SDK generation validated if used
Performance
load test executed
realistic SLOs defined
database pool configured
key queries analyzed
outbox/job backlogs remain within SLO
Critical Domain Coverage
Rather than a magic overall coverage number, require explicit test coverage for:
tenant boundaries
design approval
inspection completion
invoice issue
payment/refund
membership privilege changes
clinical record signing/amendment when healthcare exists
prescribing authorization when healthcare exists
Release Gate Principle
No single metric such as:
90% test coverage
is sufficient evidence of production readiness.
Quality gates are based on critical behavior, not vanity percentages.
97. Final Design Position
The platform is:
One Shared Platform
│
├── Shared Identity / Sessions
├── Shared Security / Authorization
├── Shared Documents / Multipart Uploads
├── Shared Financial Core
├── Shared Audit / Outbox
├── Shared Jobs / Webhooks / Notifications
│
├── Engineering Internal Product
│ ├── Engineering Frontend
│ ├── Engineering REST APIs
│ ├── Engineering State Machines
│ └── Engineering Tables
│
├── Engineering Client Portal
│ ├── External Portal Frontend
│ ├── Portal Accounts
│ ├── Project Grants
│ ├── Published Documents
│ └── Client Review / Acceptance
│
├── Legal Product
│ ├── Legal Frontend
│ ├── Legal REST APIs
│ └── Legal Tables
│
└── Healthcare Product
├── Healthcare Frontend
├── Healthcare REST APIs
├── Healthcare Security Policies
└── Healthcare Tables
The system shares infrastructure where reuse is valuable while preserving profession-specific domain semantics and trust boundaries.
v4 is the final broad architecture baseline for Engineering MVP implementation.
From this point forward, architecture detail should primarily move into:
ADRs
OpenAPI
database schema/migrations
state-machine specifications
security policies
implementation backlog
runbooks
rather than repeatedly rewriting the entire architecture plan.
This document does not itself prove:
regulatory compliance
production certification
security certification
performance at a specific scale
Those require implementation evidence, security review, domain validation, operational testing, restore testing, and measured production-like workloads.
v4.1 Changelog
v4.1 resolves implementation-contract issues without changing the core architecture.
✓ raw UUIDv7 API ID contract
✓ camelCase API / snake_case database naming convention
✓ direct organization_id on tenant subresources
✓ invitation role assignments
✓ service accounts and hashed API keys
✓ multiple professional credentials per profile
✓ global archive/revoke/unlink/hard-delete policy
✓ project stage duplication removed
✓ project budget_minor removed
✓ project phase reorder command
✓ global engineering site listing
✓ task status and priority vocabularies
✓ design revise endpoint
✓ design-assignment schema and temporal unassignment
✓ design-version many-document cardinality
✓ design review/version tenant keys
✓ inspection finding tenant keys
✓ explicit inspection follow-up table
✓ inspection-document link schema and REST
✓ change requests deferred until fully specified
✓ time-entry work-item attribution
✓ time-entry project/work-item consistency constraints
✓ specification many-document cardinality and status values
✓ specification lifecycle commands and document REST
✓ legal_documents duplication removed
✓ legal matter/case document-link schemas
✓ healthcare placeholder schemas clarified
✓ invoice-item schema defined
✓ shared billing-account invoice reference
✓ payment and refund schemas
✓ document retention-policy schema
✓ explicit retention-period null semantics
✓ version-independent document-category uniqueness fallback
✓ standard upload DTO
✓ multipart-init/parts/complete DTOs
✓ webhook subscription schema
✓ webhook status vocabulary
✓ webhook delivery references outbox events
✓ typed job input/result references
✓ registered job-type vocabulary
✓ organization, user, membership, profile, service-account, invoice, and client vocabularies
✓ portal access profiles and client-review-request schema
✓ project-member PATCH contract
✓ derived project phase; no current_phase_id in v1
✓ time-entry billing snapshot constraint
✓ logout endpoint
✓ assigned-scope resolution rules
✓ audit privacy transformation strategy
✓ outbox correlation and causation IDs
✓ CORS/browser-security ADR
✓ feature-flag strategy clarified
✓ jobs confirmed tenant-scoped via X-Organization-Id
The next artifacts should be implementation-specific:
ADRs
Engineering OpenAPI
Engineering database migrations
Engineering state-machine spec
Engineering MVP backlog