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.
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.
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
| Layer | Technology | Purpose |
|---|---|---|
| Framework | React 19 + TypeScript | UI layer with StrictMode |
| Build | Vite 7 | Fast dev server and production bundling |
| Styling | Tailwind CSS 3.4 | Utility-first CSS |
| Components | shadcn/ui (Radix primitives) | Accessible, composable UI primitives |
| Routing | react-router 7 | Declarative client-side routing |
| Backend | Supabase (Postgres + Auth + Storage) | Database, auth, file storage, realtime |
| Maps | Leaflet + react-leaflet + markercluster | Interactive complex directory map |
| Charts | recharts | Data visualisation |
| Icons | lucide-react | Consistent iconography |
| Data source | ACT Government open data | Unit 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.
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
- User hits any route →
SiteGatewraps the entire app. - If not signed in → rendered lock screen with username/password form.
- Username maps to
<user>@dwell.appemail for Supabase Auth. - If signed in but email is not
artie@dwell.app→ "Not authorised" screen. - Authorised user → full app renders with
AuthProvidersession 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
| Path | Purpose |
|---|---|
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-*.png | UI verification screenshots |
verify_*.py | Playwright/webbridge verification scripts |
App Directory (app/src/)
| Directory | Contents |
|---|---|
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>:
| Route | Component | Purpose |
|---|---|---|
/ | Home.tsx | Complex directory — Leaflet map + filterable list |
/about | About.tsx | Marketing / about page |
/portal | PortalHub.tsx | Role picker hub |
/portal/:role | RolePortal.tsx | Per-role portal (profile, complex/unit pickers) |
/complex/:id | ComplexHome.tsx | Per-complex home with tabbed features |
/login | redirect → / | 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.localwith your Supabase URL and anon key
Run the Dev Server
cd app npm install npm run dev
Build for Production
npm run build
Lint
npm run lint
Preview Production Build
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:
- Loading — pulsing Dwell logo while session is checked.
- Sign-in form — username + password, with a branded gradient sidebar on desktop.
- Not authorised — shown if the signed-in email is not
artie@dwell.app. - App — children render normally.
Roles
Four portal roles exist. A single user can hold multiple role profiles:
| Role | Property Rule | Manager Privileges |
|---|---|---|
| tenant | Lives in one home | None |
| landlord | Owns multiple units | None |
| property-manager | Manages individual units (unlimited) | Maintenance + Contractors (per-unit) |
| strata-manager | Manages whole complexes | All tabs (Community, Documents, Levies, Polls, Maintenance, Contractors) |
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:
| # | File | Content |
|---|---|---|
| 001 | 001_schema.sql | ACT directory tables + app tables (profiles, posts, events, maintenance, documents, levies, polls) |
| 002 | 002_seed_directory.sql | Generated district/division/unit-plan seed |
| 003 | 003_seed_units.sql | Generated unit-level seed |
| 004 | 004_drop_directory_write_policies.sql | Clean up temporary seeding policies |
| 005 | 005_auth.sql | Auth model, public reads, SECURITY DEFINER RPCs, RLS manager rules |
| 006 | 006_cleanup_test_users.sql | Housekeeping |
| 007 | 007_backlog_phase1.sql | Work orders, contractors, call logs, tasks, audit log, guarded-transition RPCs |
| 008 | 008_private_lockdown.sql | Owner-only preview policies |
| 009 | 009_property_managers_per_unit.sql | Per-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_ofRLS 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:
Complexinterface — 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:
Roleunion:'tenant' | 'landlord' | 'property-manager' | 'strata-manager'ROLESordered array for UI iteration.ROLE_META— labels, icon names, taglines, property rules, counterpart descriptions.Profileunion — 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)— viacreate_postRPC.addComment(postId, role, body)— viaadd_commentRPC.deleteComment(postId, commentId)— own comment or strata manager.toggleReaction(postId, role, emoji)— viatoggle_reactionRPC.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)— viaset_rsvpRPC.
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 tocomplex-documentsbucket, 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)— viacast_voteRPC.
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
| Type | Label | Use Case |
|---|---|---|
work-order | Work order | Standard dispatch to a contractor |
quote-request | Quote request | Formal quote before work begins |
cost-estimate | Cost estimate | Preliminary cost assessment |
warranty | Work under warranty | Covered by existing warranty |
Lifecycle Flow
- Creation — Any signed-in user creates a request. Fields: title, description, category, priority, unit number, spend limit, site contact, access notes.
- Cloning —
cloneTemplate(source)copies trade category, location, site contact, access protocol, and description from a past job. - 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.
- Quote decision —
decideQuote(order, approve)— manager or committee approves or rejects the reverted quote. - Completion —
completeWorkOrder(order, contractor)marks the job done and triggers the automated vendor invoicing email sequence. - Invoice logging —
logInvoice(order, {fileName, amount})records a received invoice. - Invoice approval —
approveInvoice(order, destination)routes the invoice PDF to the accounting destination (e.g.invoices@xyz.com.au) and logs it in the audit trail. - 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
| Status | Meaning |
|---|---|
| new | Just created, not yet dispatched |
| quoted | Quote received, awaiting decision |
| scheduled | Dispatched to a contractor |
| in-progress | Work has started |
| done | Completed |
| awaiting-approval | Auto-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
| Field | Purpose |
|---|---|
legalName / tradingName | Entity and brand name |
abn / acn | Australian Business / Company Number |
trade | Maintenance category (plumbing, electrical, etc.) |
licenceNumber / licenceExpiry | Trade licence with expiry date |
insurer / insuranceExpiry | Public liability insurer with expiry |
preferredComplexIds | Unit plan numbers where this contractor is preferred |
email / phone | Contact 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
- User clicks "Phone call" → non-modal popup opens with a live timer.
- Caller name, property association (searchable), and free-text notes are captured.
- The page behind stays fully navigable during the call.
- 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.
- 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
| Component | Location | Purpose |
|---|---|---|
SiteGate | components/SiteGate.tsx | Private-preview lock screen |
DirectoryMap | components/DirectoryMap.tsx | Leaflet map + markercluster |
DetailPanel | components/DetailPanel.tsx | Selected complex detail card |
ComplexCard | components/ComplexCard.tsx | List item for directory sidebar |
QuickActions | components/quickactions/QuickActions.tsx | Global Plus menu + phone popup |
UserChip | components/UserChip.tsx | Signed-in user avatar + dropdown |
Brandmark | components/Brandmark.tsx | Dwell logo component |
SignInPrompt | components/SignInPrompt.tsx | Reusable auth CTA |
Complex Tabs
Each tab in components/complex/ is a self-contained feature panel:
| Tab | Component | Manager Write Access |
|---|---|---|
| Community | CommunityTab.tsx | Strata managers only |
| Events | EventsTab.tsx | Strata managers only |
| Maintenance | MaintenanceTab.tsx | Strata + Property managers |
| Documents | DocumentsTab.tsx | Strata managers only |
| Levies | LeviesTab.tsx | Strata managers only |
| Polls | PollsTab.tsx | Strata managers only |
| Contractors | ContractorsTab.tsx | Strata + 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
-
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. -
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. -
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. -
CamelCase in app, snake_case in DB. Map only in
src/data/modules. Pages and components never see snake_case. -
Verify UI changes with screenshots. Follow the
verify-*.pngpattern. Scripts likeverify_backlog.pyautomate verification against reference screenshots. -
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
| Term | Definition |
|---|---|
| Unit Plan | An ACT registered strata / owners corporation, identified by a unit plan number. |
| Complex | A building or group of buildings under a single unit plan. |
| Strata Manager | A professional manager responsible for the administration of an owners corporation. |
| Property Manager | A manager responsible for individual rental units (not the whole complex). |
| RLS | Row-Level Security — Postgres feature for per-row access control. |
| RPC | Remote Procedure Call — Supabase function for server-side logic. |
| SECURITY DEFINER | Postgres function attribute that runs with the privileges of the function owner, not the caller. |
| Dual Persistence | Pattern of trying Supabase first, falling back to localStorage if schema is missing. |
| Compliance Lock | Blocking work order assignment when a contractor's licence or insurance has expired. |
| Spend Cap | Pre-authorised financial limit on a work order that triggers quote reversion when exceeded. |