ModulesProjectsOverview

Projects

The Projects module is the core domain model for Lacroo Pro. It manages the full lifecycle of construction projects within an organization — creation, configuration, stage tracking, member assignment, and archival. Projects serve as the parent entity for future modules (Diaries, Prestarts, Reports).

Data Model

Business Units

Org-level lookup table for grouping projects. Supports soft-delete (archive).

ColumnTypeDescription
idUUIDPrimary key
org_idUUIDForeign key to organizations
nameTEXTDisplay name (unique per org among active)
deleted_atTIMESTAMPTZSoft-delete timestamp (NULL = active)
created_atTIMESTAMPTZAuto-set on insert
updated_atTIMESTAMPTZAuto-updated via trigger

Projects

ColumnTypeDescription
idUUIDPrimary key
org_idUUIDForeign key to organizations
nameTEXTProject name (required)
geo_locationTEXTAddress string
beacon_idTEXTExternal reference ID
business_unit_idUUIDForeign key to business_units (nullable)
clientTEXTClient name (free text)
award_dateDATEContract award date
completion_dateDATEExpected completion date
statusENUMlive or archived
value_amountNUMERIC(15,2)Contract value
value_currencyTEXTISO 4217 code (default AUD)
metadataJSONBExtensible unstructured data (default {})
latitudeFLOAT8Map pin, from the address search
longitudeFLOAT8Map pin, from the address search
timezoneTEXTIANA zone, e.g. Australia/Brisbane
deleted_atTIMESTAMPTZSoft-delete timestamp
created_atTIMESTAMPTZAuto-set on insert
updated_atTIMESTAMPTZAuto-updated via trigger

Timezone

A project’s timezone decides which calendar day its sign-ons, prestarts, photos and roll calls fall on, and every date and time on the project shows in it (see Dates & Times). It comes from where the project is, and only the server sets it:

  1. The map pin decides.
  2. Otherwise the address decides: an Australian state or territory (QLD Australia/Brisbane, NSW and ACT Australia/Sydney, VIC Australia/Melbourne, TAS Australia/Hobart, SA Australia/Adelaide, NT Australia/Darwin, WA Australia/Perth), or New Zealand (Pacific/Auckland).
  3. A project with no location gets Australia/Sydney. Organisations have no zone setting of their own.

The create and save requests carry no zone. A zone a client still sends is ignored. The web and mobile apps do not work one out.

A save keeps the project’s zone, unless the project has none yet or the save moves it into a different zone. A move means the zone of the location changed. Rewording the address with the pin unchanged is not a move. A blank zone is never stored. The rule lives in one Go package, backend/pkg/projecttz (Resolve, ForSave, FromSite).

Project Stages

Append-only contract stage records. No edit or delete operations are exposed — stages form an immutable audit trail.

ColumnTypeDescription
idUUIDPrimary key
project_idUUIDForeign key to projects
org_idUUIDForeign key to organizations
contract_noTEXTContract number
contract_nameTEXTContract name
cidTEXTContract identifier
created_atTIMESTAMPTZAuto-set on insert

Project Members

Many-to-many relationship between projects and users. Access control is determined by the user’s org-level role.

ColumnTypeDescription
idUUIDPrimary key
project_idUUIDForeign key to projects
user_idUUIDForeign key to users
org_idUUIDForeign key to organizations (tenant isolation)
created_atTIMESTAMPTZAuto-set on insert

Members and Field Users

Every project member is either office staff or a field user, never both. The rule is one SQL function, project_member_role(user_id, home_org_id, project_id): a person is a field user when their role on the project (the role set for them there, otherwise their company-wide role) carries the mobile_field or mobile_supplier avenue. The supplier master admin is never a field user, and a person with no role is office staff.

The project Members tab lists office staff only (GET .../members?exclude_field_users=true), and its Add Member offers only office people and office roles. Field users are added and managed on the Field Users tab. Without the parameter the endpoint still returns everyone, because muster, prestarts and the mobile pickers need the field users too. | updated_at | TIMESTAMPTZ | Auto-updated via trigger |

Unique constraint on (project_id, user_id) prevents duplicate assignments.

Custom Field Definitions

Configurable field templates for diaries, prestarts, and activities. Uses a copy-on-inherit model: org-level templates (project_id = NULL) are auto-copied to projects on creation. See Custom Fields for full documentation.

ColumnTypeDescription
idUUIDPrimary key
org_idUUIDForeign key to organizations
project_idUUID (nullable)NULL = org template, set = project-level field
entity_typecustom_field_entityprestart_diary or activity
field_typecustom_field_typeOne of 10 field types (text, dropdown, etc.)
labelTEXTDisplay name
descriptionTEXTHelp text (optional)
requiredBOOLEANWhether field is mandatory
optionsJSONBString array for choice-based fields
sort_orderINTEGERDisplay ordering
metadataJSONBExtensible data (default {})
deleted_atTIMESTAMPTZSoft-delete timestamp
created_atTIMESTAMPTZAuto-set on insert
updated_atTIMESTAMPTZAuto-updated via trigger

Enums

  • project_status: live, archived
  • custom_field_entity: prestart_diary, activity
  • custom_field_type: text, textarea, number, checkbox, dropdown, radio, multiselect, date, time, file

API Endpoints

Projects

MethodPathAuthDescription
GET/v1/orgs/{orgId}/projectsBearerAuthList projects (admins: all, others: assigned)
POST/v1/orgs/{orgId}/projectsBearerAuthCreate project (admin only)
GET/v1/orgs/{orgId}/projects/{projectId}BearerAuthGet project detail with stages + members
PUT/v1/orgs/{orgId}/projects/{projectId}BearerAuthUpdate project (admin only, partial)
DELETE/v1/orgs/{orgId}/projects/{projectId}BearerAuthSoft-delete project (admin only)

Stages

MethodPathAuthDescription
GET/v1/orgs/{orgId}/projects/{projectId}/stagesBearerAuthList all stages
POST/v1/orgs/{orgId}/projects/{projectId}/stagesBearerAuthAdd stage (admin only)

Members

MethodPathAuthDescription
GET/v1/orgs/{orgId}/projects/{projectId}/membersBearerAuthList project members (exclude_field_users)
GET/v1/orgs/{orgId}/projects/{projectId}/members/optionsBearerAuthMembers for a person picker
POST/v1/orgs/{orgId}/projects/{projectId}/membersBearerAuthAdd existing user (admin only)
POST/v1/orgs/{orgId}/projects/{projectId}/members/from-entraBearerAuthAdd from Entra directory (admin only)
DELETE/v1/orgs/{orgId}/projects/{projectId}/members/{userId}BearerAuthRemove member (admin only)

Business Units

MethodPathAuthDescription
GET/v1/orgs/{orgId}/business-unitsBearerAuthList units (?include_archived)
POST/v1/orgs/{orgId}/business-unitsBearerAuthCreate unit (admin only)
PUT/v1/orgs/{orgId}/business-units/{businessUnitId}BearerAuthRename unit (admin only)
DELETE/v1/orgs/{orgId}/business-units/{businessUnitId}BearerAuthSoft-delete unit (admin only)

Custom Fields (Org Templates)

MethodPathAuthDescription
GET/v1/orgs/{orgId}/custom-fieldsBearerAuthList templates (?entity_type)
POST/v1/orgs/{orgId}/custom-fieldsBearerAuthCreate template + auto-copy (admin only)
PUT/v1/orgs/{orgId}/custom-fields/{fieldId}BearerAuthUpdate template (admin only)
DELETE/v1/orgs/{orgId}/custom-fields/{fieldId}BearerAuthSoft-delete template (admin only)

Custom Fields (Project-Level)

MethodPathAuthDescription
GET/v1/orgs/{orgId}/projects/{projectId}/custom-fieldsBearerAuthList fields (?entity_type)
POST/v1/orgs/{orgId}/projects/{projectId}/custom-fieldsBearerAuthCreate field (admin only)
PUT/v1/orgs/{orgId}/projects/{projectId}/custom-fields/{fieldId}BearerAuthUpdate field (admin only)
DELETE/v1/orgs/{orgId}/projects/{projectId}/custom-fields/{fieldId}BearerAuthSoft-delete field (admin only)

Users & Entra

MethodPathAuthDescription
GET/v1/orgs/{orgId}/usersBearerAuthList org users (?role, ?search, ?project_id, ?include_directory)

WebSocket

MethodPathAuthDescription
GET/v1/orgs/{orgId}/wsBearerAuthWebSocket upgrade for real-time updates

Architecture

Role-Based Access Control

Admin roles (master_admin, organisational_manager) can create, update, delete projects, manage members, and manage business units. Non-admin users can only view projects they are assigned to as members.

ActionAdminNon-Admin (assigned)Non-Admin (unassigned)
List projectsAllAssigned onlyNone
View project detailYesYesForbidden
Create/update/deleteYesNoNo
Manage membersYesNoNo
Manage stagesYesNoNo
Manage business unitsYesNoNo
Manage custom fieldsYesNoNo

Real-Time Updates

Project changes propagate in real-time via Postgres LISTEN/NOTIFY:

Database trigger fires → pg_notify('project_changes', payload)
Go WebSocket hub receives notification → broadcasts to org room
Browser WebSocket client receives message → TanStack Query cache invalidation

Triggers fire on INSERT, UPDATE, and DELETE for projects, project_members, project_stages, and custom_field_definitions tables. Payload contains table, action, org_id, and project_id.

BFF Pattern

All data access flows through the BFF (Next.js Server Actions):

Browser (TanStack Query) → Server Action (Zod validation) → Go API (business logic + DB)

The browser never calls the Go API directly (except for the WebSocket connection). Server Actions validate inputs with Zod, authenticate via session validation, and call the Go API with a short-lived JWT.

Security

  • Tenant isolation — Every query includes AND org_id = $N. Handlers verify JWT org_id matches the URL {orgId} parameter. Non-admins additionally require project membership.
  • Role-based mutations — Create, update, delete, and member management restricted to master_admin and organisational_manager roles.
  • Double input validation — Zod schemas in the BFF layer + Go struct validation with length limits and format checks.
  • Soft deletes — Projects, business units, and custom fields use deleted_at timestamps. No hard deletes.
  • Append-only stages — Project stages cannot be edited or deleted once created.
  • Unique constraints — Business unit names unique per org (active only). Project member assignments unique per (project, user).
  • COALESCE updates — Partial project updates use COALESCE(sqlc.narg(...), existing) to prevent overwriting fields not included in the request.
  • Membership check — Non-admin project detail access requires IsUserProjectMember verification.
  • Entra integration — Client secrets decrypted only during API calls, never logged or returned to the frontend.
  • WebSocket auth — Connections require valid BearerAuth JWT. Clients are scoped to their org room.

Key Files

FilePurpose
backend/db/migrations/003_projects.sqlSchema: 4 tables, 2 enums, LISTEN/NOTIFY triggers
backend/db/migrations/006_custom_fields.sqlSchema: custom_field_definitions table, enums, triggers
backend/db/queries/projects.sqlsqlc queries: CRUD, list (admin + user-scoped), pagination
backend/db/queries/project_stages.sqlsqlc queries: add + list stages
backend/db/queries/project_members.sqlsqlc queries: add, remove, list, membership check
backend/db/queries/business_units.sqlsqlc queries: CRUD with soft-delete
backend/db/queries/custom_fields.sqlsqlc queries: CRUD + copy-on-inherit
backend/api/internal/project/handler.goGo HTTP handlers: projects, stages, members, member options
backend/api/internal/project/service.goGo business logic: CRUD, filters, pgtype conversions
backend/api/internal/businessunit/handler.goGo HTTP handlers: business unit CRUD
backend/api/internal/customfield/handler.goGo HTTP handlers: custom fields (org + project routes)
backend/api/internal/customfield/service.goGo business logic: CRUD + fan-out copy to projects
backend/api/internal/orguser/handler.goGo HTTP handler: org user listing with filters
backend/api/internal/ws/hub.goWebSocket hub: org rooms, Postgres LISTEN, broadcast
backend/api/internal/ws/client.goWebSocket client: read/write pumps, ping/pong keepalive
apps/web/src/actions/projects.tsBFF server actions: 10 project/stage/member actions
apps/web/src/actions/business-units.tsBFF server actions: business unit CRUD
apps/web/src/actions/custom-fields.tsBFF server actions: 8 custom field actions
apps/web/src/actions/users.tsBFF server actions: org users (directory candidates included)
apps/web/src/app/org/_components/ws-provider.tsxWebSocket provider: real-time TanStack Query invalidation
apps/web/src/app/org/page.tsxProjects list page (RSC with Suspense)
apps/web/src/app/org/_components/projects-table.tsxProjects table with search, filters, pagination
apps/web/src/app/org/_components/create-project-wizard.tsx4-step project creation wizard
apps/web/src/app/admin/_components/project-detail.tsxProject detail: tabs for overview, stages, members, custom fields
apps/web/src/app/org/settings/_components/business-units-tab.tsxBusiness units CRUD in Settings
apps/web/src/app/org/configuration/_components/custom-fields-tab.tsxOrg-level custom fields CRUD
apps/web/src/app/admin/_components/project-custom-fields-tab.tsxProject-level custom fields CRUD
apps/web/src/app/org/users/_components/users-table.tsxOrg users table with role filter
apps/web/src/app/org/layout.tsxOrg layout with sidebar navigation
apps/web/src/app/org/_components/sidebar.tsxSidebar: Projects, Users, Configuration, Settings