GitBook

Dwell Documentation

A private, invite-only ACT strata and unit-plan directory + community management platform. This guide covers architecture, development conventions, the data layer, and every feature from the directory map to the work-order lifecycle.

React 19 + TypeScript + Vite Supabase Postgres Private Preview

Dwell unifies property records, maintenance dispatching, resident communications, meeting governance, and portfolio management into a single ecosystem. It targets the Canberra (ACT) strata market first, built on ACT open data for unit plans and building blocks, with Supabase-backed community features per complex.

Key Principle

The platform is currently a private preview — nothing renders before sign-in, and only the single authorised account (artie@dwell.app) can access the app. All data mutations are gated by role-aware RLS policies and SECURITY DEFINER RPCs.

What This Guide Covers

Architecture

Tech stack, system diagram, and the reasoning behind every major technology choice.

Project Structure

How the workspace is organised, from the React app to database migrations and data pipelines.

Database & Auth

Schema design, Row-Level Security, auth model, and the migration run order.

Data Layer

Every service module: complexes, profiles, community, work orders, contractors, quick actions.

Feature Deep Dives

Work-order lifecycle, contractor compliance locking, phone-call triage, and the Plus menu.

Roadmap

Phased implementation strategy from core backlog through AI intelligence and IoT telemetry.

Architecture

Tech Stack

LayerTechnologyPurpose
FrameworkReact 19 + TypeScriptUI layer with StrictMode
BuildVite 7Fast dev server and production bundling
StylingTailwind CSS 3.4Utility-first CSS
Componentsshadcn/ui (Radix primitives)Accessible, composable UI primitives
Routingreact-router 7Declarative client-side routing
BackendSupabase (Postgres + Auth + Storage)Database, auth, file storage, realtime
MapsLeaflet + react-leaflet + markerclusterInteractive complex directory map
ChartsrechartsData visualisation
Iconslucide-reactConsistent iconography
Data sourceACT Government open dataUnit plans, addresses, geometries

System Overview

Dwell is a single-page application (SPA) that sits entirely in the browser. The React app communicates with Supabase for all persistent data. The ACT directory data (complexes, addresses, geo-coordinates) is served as a static JSON file built by the Python ETL pipeline.

Dual-Persistence Pattern

Every Phase-1 backlog module (work orders, contractors, quick actions) tries Supabase first. If the migration hasn't been applied yet, the module detects the schema-missing error and transparently falls back to a per-key localStorage store. Once the migration is applied, the same code uses the live database with zero client changes.

Auth Flow

  1. User hits any route → SiteGate wraps the entire app.
  2. If not signed in → rendered lock screen with username/password form.
  3. Username maps to <user>@dwell.app email for Supabase Auth.
  4. If signed in but email is not artie@dwell.app → "Not authorised" screen.
  5. Authorised user → full app renders with AuthProvider session context.

Environment Variables

Create app/.env.local:

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key

The anon key is browser-safe and cannot run DDL. All schema changes are SQL files in database/supabase/ that the owner runs manually in the Supabase SQL Editor.

Project Structure

Workspace Root

PathPurpose
app/The React + TypeScript web application
database/supabase/Numbered, idempotent SQL migrations
data/Raw ACT datasets + Python ETL scripts
design/Brand assets and pitch-deck renders
dwell_pitch/Investor pitch decks
verify-*.pngUI verification screenshots
verify_*.pyPlaywright/webbridge verification scripts

App Directory (app/src/)

DirectoryContents
pages/Top-level route components: Home, About, PortalHub, RolePortal, ComplexHome
components/Reusable UI: DirectoryMap, DetailPanel, SiteGate, QuickActions, tab components
components/ui/Stock shadcn/ui primitives — compose, don't hand-edit
components/complex/One tab component per complex feature
components/portal/ComplexPicker, UnitPicker, ProfileEditor
components/quickactions/Global Plus menu + phone-call triage popup
data/Service layer — all Supabase access + type mapping
lib/Cross-cutting infra: supabase client, auth, remote.ts, bus.ts
hooks/Custom React hooks

Path Alias

Vite is configured with @/src/. Import shadcn primitives as:

import { Button } from '@/components/ui/button'

Routing

All routes are declared in src/App.tsx inside <BrowserRouter>:

RouteComponentPurpose
/Home.tsxComplex directory — Leaflet map + filterable list
/aboutAbout.tsxMarketing / about page
/portalPortalHub.tsxRole picker hub
/portal/:roleRolePortal.tsxPer-role portal (profile, complex/unit pickers)
/complex/:idComplexHome.tsxPer-complex home with tabbed features
/loginredirect → /Auth redirect

Deep-linking to a complex works via ?c=<id> on the home page. The map centres and the detail panel opens automatically.

Getting Started

Prerequisites

  • Node.js 20+ and npm
  • A Supabase project with the migrations applied
  • .env.local with your Supabase URL and anon key

Run the Dev Server

Terminal
cd app
npm install
npm run dev

Build for Production

Terminal
npm run build

Lint

Terminal
npm run lint

Preview Production Build

Terminal
npm run preview

Authentication

AuthProvider

AuthProvider wraps the entire component tree in main.tsx. It manages Supabase session state, exposes signIn, signUp, and signOut, and auto-refreshes via onAuthStateChange.

Consume auth state with the useAuth() hook from auth-context.ts.

SiteGate

SiteGate is the private-preview lock. It renders one of three states:

  1. Loading — pulsing Dwell logo while session is checked.
  2. Sign-in form — username + password, with a branded gradient sidebar on desktop.
  3. Not authorised — shown if the signed-in email is not artie@dwell.app.
  4. App — children render normally.

Roles

Four portal roles exist. A single user can hold multiple role profiles:

RoleProperty RuleManager Privileges
tenantLives in one homeNone
landlordOwns multiple unitsNone
property-managerManages individual units (unlimited)Maintenance + Contractors (per-unit)
strata-managerManages whole complexesAll tabs (Community, Documents, Levies, Polls, Maintenance, Contractors)
Domain Rule

Property managers are per unit, not per complex. They must not appear as complex-level contacts or links on complex pages. Strata managers are per complex. See migration 009_property_managers_per_unit.sql for the RLS implementation.

Database

Migration Run Order

All migrations live in database/supabase/ and are idempotent. Run them in order:

#FileContent
001001_schema.sqlACT directory tables + app tables (profiles, posts, events, maintenance, documents, levies, polls)
002002_seed_directory.sqlGenerated district/division/unit-plan seed
003003_seed_units.sqlGenerated unit-level seed
004004_drop_directory_write_policies.sqlClean up temporary seeding policies
005005_auth.sqlAuth model, public reads, SECURITY DEFINER RPCs, RLS manager rules
006006_cleanup_test_users.sqlHousekeeping
007007_backlog_phase1.sqlWork orders, contractors, call logs, tasks, audit log, guarded-transition RPCs
008008_private_lockdown.sqlOwner-only preview policies
009009_property_managers_per_unit.sqlPer-unit RLS, is_property_manager_of_unit, guarded RPC updates

Auth Model

  • Public reads — complex pages, posts, events, and the directory are readable by anyone.
  • RPC-only writes for community — posts, comments, reactions, and RSVPs are written only through SECURITY DEFINER RPCs. Authorship is stamped server-side from the caller's profile. Clients cannot spoof names or roles.
  • RLS for manager tables — announcements, events, documents, levies, and polls use is_manager_of RLS policies.
  • Per-unit RLS for maintenance — property managers can only manage work orders for units they manage, enforced by 009_property_managers_per_unit.sql.

Schema Conventions

  • App types are camelCase + epoch millis timestamps.
  • DB columns are snake_case + timestamptz / date.
  • Mapping happens only in src/data/ modules — pages/components never see snake_case.

Data Layer

Every domain has a single file in src/data/ that owns all Supabase access. Pages and components never call Supabase directly.

complexes.ts

Loads the ACT directory from a static ./data/complexes.json file (cached after first fetch). Exports:

  • Complex interface — id, name, suburb, district, addresses, units, geo, etc.
  • displayName(c) — falls back to "Unit Plan {id}" when name is null.
  • primaryAddress(c) — first address or suburb fallback.
  • loadComplexes() — cached promise returning all complexes.

profiles.ts

One profile row per (user, role). Exports:

  • Role union: 'tenant' | 'landlord' | 'property-manager' | 'strata-manager'
  • ROLES ordered array for UI iteration.
  • ROLE_META — labels, icon names, taglines, property rules, counterpart descriptions.
  • Profile union — role-specific shapes (home, properties, complexIds).
  • listMyRoles(), loadProfile(role), createProfile(role, seed), saveProfile(role, profile)
  • getActiveRole() / setActiveRole(role) — localStorage UI preference.
  • getIdentity(role) — derived identity for authorship stamping.
  • loadManagerContacts(complexId) — public read of strata managers for a complex.

community.ts

The largest module (~930 lines). Covers per-complex community features:

Posts (RPC-only writes)

  • listPosts(complexId) — newest first.
  • createPost(complexId, role, body) — via create_post RPC.
  • addComment(postId, role, body) — via add_comment RPC.
  • deleteComment(postId, commentId) — own comment or strata manager.
  • toggleReaction(postId, role, emoji) — via toggle_reaction RPC.
  • deletePost(postId) — own post or strata manager.

Events

  • listEvents(complexId) — chronological by date then time.
  • addEvent(event), updateEvent(complexId, updated), deleteEvent(complexId, id) — managers only (RLS).
  • setRsvp(eventId, role, status) — via set_rsvp RPC.

Announcements

  • listAnnouncements(complexId) — pinned first, then newest.
  • addAnnouncement(a), updateAnnouncement(complexId, updated), deleteAnnouncement(complexId, id) — managers only.

Maintenance Requests

  • listMaintenance(complexId) — newest first.
  • addMaintenance(request) — any signed-in user (RLS: reported_by_id = auth.uid()).
  • updateMaintenance(complexId, updated), deleteMaintenance(complexId, id) — managers only.

Documents

  • listDocuments(complexId) — newest first.
  • uploadDocument(meta, file) — uploads to complex-documents bucket, then inserts metadata row.
  • documentUrl(storagePath) — public download URL.
  • deleteDocument(complexId, id) — removes metadata + storage object.

Levies

  • listLevies(complexId) — by due date, soonest first.
  • addLevy(levy), updateLevy(complexId, updated), deleteLevy(complexId, id) — managers only.

Polls

  • listPolls(complexId) — newest first.
  • addPoll(poll), updatePoll(complexId, updated), deletePoll(complexId, id) — managers only.
  • castVote(pollId, role, optionId) — via cast_vote RPC.

Work Order Lifecycle

The Phase-1 work order system extends the community maintenance_requests table with financial safeguards, contractor assignment, and invoice routing.

Request Types

TypeLabelUse Case
work-orderWork orderStandard dispatch to a contractor
quote-requestQuote requestFormal quote before work begins
cost-estimateCost estimatePreliminary cost assessment
warrantyWork under warrantyCovered by existing warranty

Lifecycle Flow

  1. Creation — Any signed-in user creates a request. Fields: title, description, category, priority, unit number, spend limit, site contact, access notes.
  2. CloningcloneTemplate(source) copies trade category, location, site contact, access protocol, and description from a past job.
  3. Dispatch — Manager assigns a contractor and provides an estimated cost. dispatchWorkOrder() enforces two rules:
    • Compliance lock — if the contractor's licence or insurance has expired, assignment is blocked.
    • Spend-cap reversion — if the quoted amount exceeds the pre-authorised spend limit, the request is automatically converted to a Quote Request and routed back for approval.
  4. Quote decisiondecideQuote(order, approve) — manager or committee approves or rejects the reverted quote.
  5. CompletioncompleteWorkOrder(order, contractor) marks the job done and triggers the automated vendor invoicing email sequence.
  6. Invoice logginglogInvoice(order, {fileName, amount}) records a received invoice.
  7. Invoice approvalapproveInvoice(order, destination) routes the invoice PDF to the accounting destination (e.g. invoices@xyz.com.au) and logs it in the audit trail.
  8. Rating — Post-job contractor rating prompt (5-star + qualitative feedback).

Audit Log

Every significant action is recorded in the audit_log table with actor, action, entity, and detail JSON. Exportable as CSV via auditLogCsv(entries).

Status Values

StatusMeaning
newJust created, not yet dispatched
quotedQuote received, awaiting decision
scheduledDispatched to a contractor
in-progressWork has started
doneCompleted
awaiting-approvalAuto-reverted to quote request

Contractor Directory

The vendor register tracks legal entity details, trade licences, public liability insurance, preferred-complex flags, and aggregated 5-star ratings.

Contractor Model

FieldPurpose
legalName / tradingNameEntity and brand name
abn / acnAustralian Business / Company Number
tradeMaintenance category (plumbing, electrical, etc.)
licenceNumber / licenceExpiryTrade licence with expiry date
insurer / insuranceExpiryPublic liability insurer with expiry
preferredComplexIdsUnit plan numbers where this contractor is preferred
email / phoneContact details

Compliance Locking

complianceStatus(c) returns one of three states:

  • compliant — licence and insurance both valid and not expiring within 60 days.
  • expiring — at least one expires within 60 days (warning UI).
  • lapsed — at least one has expired (assignment blocked).

Smart Preferred Hierarchy

rankForDispatch(contractors, trade, complexId) returns contractors in the requested trade, sorted by: preferred-for-this-complex first, lapsed-compliance contractors last. The UI disables lapsed contractors in dispatch dropdowns.

Ratings

Post-job ratings aggregate in the contractor directory. analyticsFor(contractorId, ratings, jobsCompleted) returns average stars, rating count, and completed job count. Ratings are written via the rate_contractor RPC so authorship is server-stamped.

Quick Actions

The Plus menu is a persistent floating shortcut button that stays accessible across all screens. It opens single-click actions for four primary workflows:

Quick Note

Log a note or to-do item with title and optional detail.

Phone Call

Interactive call triage with notes, property association, timer, and post-call action decisions.

Service Request

Create a maintenance request on the current complex, or pick a complex if outside one.

Reminder

Set a follow-up reminder with optional due date, or log as FYI (no alert).

Phone Call Triage

  1. User clicks "Phone call" → non-modal popup opens with a live timer.
  2. Caller name, property association (searchable), and free-text notes are captured.
  3. The page behind stays fully navigable during the call.
  4. User clicks "End call" → "What's next?" decision panel appears:
    • Draft a maintenance request — pre-fills from call notes and navigates to the complex.
    • Create a task — logs a to-do item.
    • Set reminder / Log as FYI — with optional follow-up date.
  5. The call log is saved with outcome, property reference, and follow-up details.

Event Bus

The quick actions and phone popup communicate with complex pages through a tiny CustomEvent bus in lib/bus.ts. openServiceRequest({complexId, prefill}) fires a dwell:open-service-request event that the MaintenanceTab subscribes to, decoupling the Plus menu from the tab component.

UI Components

Key Components

ComponentLocationPurpose
SiteGatecomponents/SiteGate.tsxPrivate-preview lock screen
DirectoryMapcomponents/DirectoryMap.tsxLeaflet map + markercluster
DetailPanelcomponents/DetailPanel.tsxSelected complex detail card
ComplexCardcomponents/ComplexCard.tsxList item for directory sidebar
QuickActionscomponents/quickactions/QuickActions.tsxGlobal Plus menu + phone popup
UserChipcomponents/UserChip.tsxSigned-in user avatar + dropdown
Brandmarkcomponents/Brandmark.tsxDwell logo component
SignInPromptcomponents/SignInPrompt.tsxReusable auth CTA

Complex Tabs

Each tab in components/complex/ is a self-contained feature panel:

TabComponentManager Write Access
CommunityCommunityTab.tsxStrata managers only
EventsEventsTab.tsxStrata managers only
MaintenanceMaintenanceTab.tsxStrata + Property managers
DocumentsDocumentsTab.tsxStrata managers only
LeviesLeviesTab.tsxStrata managers only
PollsPollsTab.tsxStrata managers only
ContractorsContractorsTab.tsxStrata + Property managers

shadcn/ui Primitives

All UI primitives live in components/ui/ and are generated/managed by shadcn/ui. Do not hand-edit beyond normal shadcn patterns. Compose them rather than inventing new primitives. Examples: Button, Dialog, Input, Tabs, DropdownMenu, Sheet, Command.

Development Conventions

  1. Schema changes = new numbered SQL file in database/supabase/. Every file must be idempotent (IF NOT EXISTS, CREATE OR REPLACE, DROP POLICY IF EXISTS). The owner runs them manually in the Supabase SQL Editor. Never expect the client to do DDL.
  2. New features use the dual-persistence pattern from remote.ts. Try Supabase first, detect schema-missing errors, fall back to localStorage so features work in preview before migrations are applied.
  3. Public reads, guarded writes. Complex pages are readable by all. Community writes go through SECURITY DEFINER RPCs (authorship stamped server-side). Manager-only tables use RLS is_manager_of.
  4. CamelCase in app, snake_case in DB. Map only in src/data/ modules. Pages and components never see snake_case.
  5. Verify UI changes with screenshots. Follow the verify-*.png pattern. Scripts like verify_backlog.py automate verification against reference screenshots.
  6. Compose shadcn primitives. Use components/ui/ stock components. Don't invent new primitives when an existing one can be composed.

Adding a New Data Module

Follow this pattern:

// 1. Define app types (camelCase, epoch millis)
export interface MyFeature { id: string; complexId: number; title: string; createdAt: number }

// 2. Row mapping (snake_case DB ↔ camelCase app)
interface MyFeatureRow { id: string; complex_id: number; title: string; created_at: string }
function rowToFeature(r: MyFeatureRow): MyFeature { /* … */ }

// 3. Reads with localStorage fallback
export async function listFeatures(complexId: number): Promise<MyFeature[]> {
  if (!isLocal('myfeature')) {
    try {
      const res = await supabase.from('my_features').select('*').eq('complex_id', complexId)
      return (unwrap(res) as MyFeatureRow[]).map(rowToFeature)
    } catch (e) {
      if (!isSchemaMissing(e)) throw e
      markLocal('myfeature')
    }
  }
  return lsRead<MyFeature>(LS_KEY)
}

Roadmap

Phase 1: Core Backlog & Workflow Friction Elimination Live

High-frequency operational touchpoints: work order template pre-filling, spend threshold caps with auto-reversion to quote requests, invoice approval bot, floating phone call triage, sticky shortcut menu, and post-completion contractor evaluation prompts.

  • Work Order Management System with four request types and retrospective jobs
  • Configurable financial limits + automatic quote reversion
  • Invoice approval bot + automated vendor email sequences
  • Contractor directory with compliance locking and 5-star ratings
  • Phone call triage + quick notes + reminders
  • Audit log with CSV export

Phase 2: Governance, Mobile & Task Automation Planned

Digital meeting voting engine, 14-day Handover Handshake escrow protocol, true offline mobile inspection sync, NFC-enabled key register, 250-character comment limits, anti-branching email anchoring, and drag-and-drop Kanban task management.

  • Integrated meeting voting engine with proxy forms and auto-tabulation
  • Handover Handshake escrow protocol with digital signing
  • True offline mobile sync for inspections and defect logging
  • NFC key register with sign-in/sign-out audit trail
  • Communication guardrails (comment limits, thread anchoring, pinned emails)
  • Exportable audit logs (PDF + spreadsheet)

Phase 3: AI Intelligence, Enterprise & IoT Future

Elevate Dwell from an operational tool into an enterprise PropTech ecosystem: AI By-Law Assistant, automated lease abstraction, predictive maintenance triage, CAM reconciliation engines, bi-directional ERP sync (Xero, MYOB, QuickBooks, NetSuite, Sage), and IoT sensor integration.

  • AI-powered by-law and statutory assistant with cited answers
  • Document intelligence — automated lease abstraction
  • Predictive maintenance triage + warranty matching
  • IoT sensor integration (water pressure, leak detection, HVAC, elevators)
  • ESG utility analytics and smart waste management
  • Common Area Maintenance (CAM) and outgoings recovery engine
  • Bi-directional accounting ERP synchronization

Glossary

TermDefinition
Unit PlanAn ACT registered strata / owners corporation, identified by a unit plan number.
ComplexA building or group of buildings under a single unit plan.
Strata ManagerA professional manager responsible for the administration of an owners corporation.
Property ManagerA manager responsible for individual rental units (not the whole complex).
RLSRow-Level Security — Postgres feature for per-row access control.
RPCRemote Procedure Call — Supabase function for server-side logic.
SECURITY DEFINERPostgres function attribute that runs with the privileges of the function owner, not the caller.
Dual PersistencePattern of trying Supabase first, falling back to localStorage if schema is missing.
Compliance LockBlocking work order assignment when a contractor's licence or insurance has expired.
Spend CapPre-authorised financial limit on a work order that triggers quote reversion when exceeded.