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).
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
org_id | UUID | Foreign key to organizations |
name | TEXT | Display name (unique per org among active) |
deleted_at | TIMESTAMPTZ | Soft-delete timestamp (NULL = active) |
created_at | TIMESTAMPTZ | Auto-set on insert |
updated_at | TIMESTAMPTZ | Auto-updated via trigger |
Projects
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
org_id | UUID | Foreign key to organizations |
name | TEXT | Project name (required) |
geo_location | TEXT | Address string |
beacon_id | TEXT | External reference ID |
business_unit_id | UUID | Foreign key to business_units (nullable) |
client | TEXT | Client name (free text) |
award_date | DATE | Contract award date |
completion_date | DATE | Expected completion date |
status | ENUM | live or archived |
value_amount | NUMERIC(15,2) | Contract value |
value_currency | TEXT | ISO 4217 code (default AUD) |
metadata | JSONB | Extensible unstructured data (default {}) |
latitude | FLOAT8 | Map pin, from the address search |
longitude | FLOAT8 | Map pin, from the address search |
timezone | TEXT | IANA zone, e.g. Australia/Brisbane |
deleted_at | TIMESTAMPTZ | Soft-delete timestamp |
created_at | TIMESTAMPTZ | Auto-set on insert |
updated_at | TIMESTAMPTZ | Auto-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:
- The map pin decides.
- Otherwise the address decides: an Australian state or territory (QLD
Australia/Brisbane, NSW and ACTAustralia/Sydney, VICAustralia/Melbourne, TASAustralia/Hobart, SAAustralia/Adelaide, NTAustralia/Darwin, WAAustralia/Perth), or New Zealand (Pacific/Auckland). - 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.
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
project_id | UUID | Foreign key to projects |
org_id | UUID | Foreign key to organizations |
contract_no | TEXT | Contract number |
contract_name | TEXT | Contract name |
cid | TEXT | Contract identifier |
created_at | TIMESTAMPTZ | Auto-set on insert |
Project Members
Many-to-many relationship between projects and users. Access control is determined by the user’s org-level role.
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
project_id | UUID | Foreign key to projects |
user_id | UUID | Foreign key to users |
org_id | UUID | Foreign key to organizations (tenant isolation) |
created_at | TIMESTAMPTZ | Auto-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.
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
org_id | UUID | Foreign key to organizations |
project_id | UUID (nullable) | NULL = org template, set = project-level field |
entity_type | custom_field_entity | prestart_diary or activity |
field_type | custom_field_type | One of 10 field types (text, dropdown, etc.) |
label | TEXT | Display name |
description | TEXT | Help text (optional) |
required | BOOLEAN | Whether field is mandatory |
options | JSONB | String array for choice-based fields |
sort_order | INTEGER | Display ordering |
metadata | JSONB | Extensible data (default {}) |
deleted_at | TIMESTAMPTZ | Soft-delete timestamp |
created_at | TIMESTAMPTZ | Auto-set on insert |
updated_at | TIMESTAMPTZ | Auto-updated via trigger |
Enums
project_status:live,archivedcustom_field_entity:prestart_diary,activitycustom_field_type:text,textarea,number,checkbox,dropdown,radio,multiselect,date,time,file
API Endpoints
Projects
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/projects | BearerAuth | List projects (admins: all, others: assigned) |
POST | /v1/orgs/{orgId}/projects | BearerAuth | Create project (admin only) |
GET | /v1/orgs/{orgId}/projects/{projectId} | BearerAuth | Get project detail with stages + members |
PUT | /v1/orgs/{orgId}/projects/{projectId} | BearerAuth | Update project (admin only, partial) |
DELETE | /v1/orgs/{orgId}/projects/{projectId} | BearerAuth | Soft-delete project (admin only) |
Stages
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/projects/{projectId}/stages | BearerAuth | List all stages |
POST | /v1/orgs/{orgId}/projects/{projectId}/stages | BearerAuth | Add stage (admin only) |
Members
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/projects/{projectId}/members | BearerAuth | List project members (exclude_field_users) |
GET | /v1/orgs/{orgId}/projects/{projectId}/members/options | BearerAuth | Members for a person picker |
POST | /v1/orgs/{orgId}/projects/{projectId}/members | BearerAuth | Add existing user (admin only) |
POST | /v1/orgs/{orgId}/projects/{projectId}/members/from-entra | BearerAuth | Add from Entra directory (admin only) |
DELETE | /v1/orgs/{orgId}/projects/{projectId}/members/{userId} | BearerAuth | Remove member (admin only) |
Business Units
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/business-units | BearerAuth | List units (?include_archived) |
POST | /v1/orgs/{orgId}/business-units | BearerAuth | Create unit (admin only) |
PUT | /v1/orgs/{orgId}/business-units/{businessUnitId} | BearerAuth | Rename unit (admin only) |
DELETE | /v1/orgs/{orgId}/business-units/{businessUnitId} | BearerAuth | Soft-delete unit (admin only) |
Custom Fields (Org Templates)
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/custom-fields | BearerAuth | List templates (?entity_type) |
POST | /v1/orgs/{orgId}/custom-fields | BearerAuth | Create template + auto-copy (admin only) |
PUT | /v1/orgs/{orgId}/custom-fields/{fieldId} | BearerAuth | Update template (admin only) |
DELETE | /v1/orgs/{orgId}/custom-fields/{fieldId} | BearerAuth | Soft-delete template (admin only) |
Custom Fields (Project-Level)
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/projects/{projectId}/custom-fields | BearerAuth | List fields (?entity_type) |
POST | /v1/orgs/{orgId}/projects/{projectId}/custom-fields | BearerAuth | Create field (admin only) |
PUT | /v1/orgs/{orgId}/projects/{projectId}/custom-fields/{fieldId} | BearerAuth | Update field (admin only) |
DELETE | /v1/orgs/{orgId}/projects/{projectId}/custom-fields/{fieldId} | BearerAuth | Soft-delete field (admin only) |
Users & Entra
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/users | BearerAuth | List org users (?role, ?search, ?project_id, ?include_directory) |
WebSocket
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /v1/orgs/{orgId}/ws | BearerAuth | WebSocket 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.
| Action | Admin | Non-Admin (assigned) | Non-Admin (unassigned) |
|---|---|---|---|
| List projects | All | Assigned only | None |
| View project detail | Yes | Yes | Forbidden |
| Create/update/delete | Yes | No | No |
| Manage members | Yes | No | No |
| Manage stages | Yes | No | No |
| Manage business units | Yes | No | No |
| Manage custom fields | Yes | No | No |
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 invalidationTriggers 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 JWTorg_idmatches the URL{orgId}parameter. Non-admins additionally require project membership. - Role-based mutations — Create, update, delete, and member management restricted to
master_adminandorganisational_managerroles. - 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_attimestamps. 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
IsUserProjectMemberverification. - 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
| File | Purpose |
|---|---|
backend/db/migrations/003_projects.sql | Schema: 4 tables, 2 enums, LISTEN/NOTIFY triggers |
backend/db/migrations/006_custom_fields.sql | Schema: custom_field_definitions table, enums, triggers |
backend/db/queries/projects.sql | sqlc queries: CRUD, list (admin + user-scoped), pagination |
backend/db/queries/project_stages.sql | sqlc queries: add + list stages |
backend/db/queries/project_members.sql | sqlc queries: add, remove, list, membership check |
backend/db/queries/business_units.sql | sqlc queries: CRUD with soft-delete |
backend/db/queries/custom_fields.sql | sqlc queries: CRUD + copy-on-inherit |
backend/api/internal/project/handler.go | Go HTTP handlers: projects, stages, members, member options |
backend/api/internal/project/service.go | Go business logic: CRUD, filters, pgtype conversions |
backend/api/internal/businessunit/handler.go | Go HTTP handlers: business unit CRUD |
backend/api/internal/customfield/handler.go | Go HTTP handlers: custom fields (org + project routes) |
backend/api/internal/customfield/service.go | Go business logic: CRUD + fan-out copy to projects |
backend/api/internal/orguser/handler.go | Go HTTP handler: org user listing with filters |
backend/api/internal/ws/hub.go | WebSocket hub: org rooms, Postgres LISTEN, broadcast |
backend/api/internal/ws/client.go | WebSocket client: read/write pumps, ping/pong keepalive |
apps/web/src/actions/projects.ts | BFF server actions: 10 project/stage/member actions |
apps/web/src/actions/business-units.ts | BFF server actions: business unit CRUD |
apps/web/src/actions/custom-fields.ts | BFF server actions: 8 custom field actions |
apps/web/src/actions/users.ts | BFF server actions: org users (directory candidates included) |
apps/web/src/app/org/_components/ws-provider.tsx | WebSocket provider: real-time TanStack Query invalidation |
apps/web/src/app/org/page.tsx | Projects list page (RSC with Suspense) |
apps/web/src/app/org/_components/projects-table.tsx | Projects table with search, filters, pagination |
apps/web/src/app/org/_components/create-project-wizard.tsx | 4-step project creation wizard |
apps/web/src/app/admin/_components/project-detail.tsx | Project detail: tabs for overview, stages, members, custom fields |
apps/web/src/app/org/settings/_components/business-units-tab.tsx | Business units CRUD in Settings |
apps/web/src/app/org/configuration/_components/custom-fields-tab.tsx | Org-level custom fields CRUD |
apps/web/src/app/admin/_components/project-custom-fields-tab.tsx | Project-level custom fields CRUD |
apps/web/src/app/org/users/_components/users-table.tsx | Org users table with role filter |
apps/web/src/app/org/layout.tsx | Org layout with sidebar navigation |
apps/web/src/app/org/_components/sidebar.tsx | Sidebar: Projects, Users, Configuration, Settings |