Lesson 1: Start Simple and Refactor Often

The Union Hub
1 __start__ present_card lookup_member not_found verified __end__ 2 __start__ magic_link admin_console bulk_import ⏸ before audit_log __end__ 3 __start__ intake chief_steward precedent_checkbefore advising step_meeting⏸ interrupt = after stewards member_review⏸ interrupt = before __end__ 4 __start__ understand_question roster_lookup knowledge_search answer_with_precedent __end__ v0.1 — the card verifies v0.2 — the roster, gated imports v0.3 — casework: every irreversible step pauses for a human v0.4 — questions answered from the Local's own history

The same product, four refactors on — each graph shipped before the next was drawn.

The Approach

The Union Hub
member · steward · admin question answer Edge Middleware tenant_resolve magic_link rewrite/tenants/… The app · static HTML + ES modules · no build Institutional memory precedents case_history knowledge_notes audit_log Data modules admin-members.js admin-grievances.js admin-knowledge.js site-render.js tenant-scoped query rows · RLS-filtered Data · Supabase Postgres Structured · RLS on every table members dues grievance_cases cba_articles policy: role AND tenant_id — always both Unstructured · document vault agreements · LOUs arbitration_awards meeting_minutes Pre-process docs categorize link_to_case

Edge Middleware:

  • Acts as the orchestrator
  • Resolves the tenant from the hostname before a single page is served
  • Rewrites to that Local's template and stamps the tenant context on the request
  • Never grants data access by itself — the header alone opens nothing

Row-Level Security:

Every query carries the tenant id, and Postgres enforces role AND tenant on every row. The client is never trusted to filter — isolation is a database guarantee, not an application habit.

Knowledge module:

Translates one search into four tenant-scoped reads — past cases, how they resolved, steward notes, and the agreement itself — and returns them together, so a search for “Attendance” surfaces the whole cluster.

Domain Knowledge

The Union Hub

Structured Data →Members, dues, grievance cases — Postgres, RLS on every table

Unstructured Data →Steward notes, meeting minutes, agreements, awards

The Agreement & Precedents →CBA articles linked to every case, and how each one resolved

Lesson 2: Validation-Driven Development

The Union Hub

→ A multi-tenant system for members' data earns trust with proof, not promises

→ Every schema change ships with an isolation test before it ships to a Local

  • Baseline first: the suite is expected to FAIL on the old policies — a test that can't fail proves nothing.
  • Role AND tenant: every policy is tested for both — an admin of Local A must read zero rows of Local B.
  • Syntax-reviewed ≠ observed: nothing is called verified until the gate has actually been run and watched.
  • Human in the loop: the apply runbook pauses for a backup and an operator's eyes before anything touches production.
tenant-isolation suiteillustrative — the gate runs at apply time
members_admin_read
00400041
verifications
00400041
dues_collections
00400041
audit_log
00400041
cross_tenant_admin_readrole + tenantFAIL @ 0040PASS @ 0041
anon_lookup_scopesingle row, no enumerationPASSPASS
steward_case_readmember-gatedPASSPASS
knowledge_admin_readwhole-Local memoryPASSPASS

Lesson 3: Human Steward in the Loop

The Union Hub
<50%

Where Locals
start

Paper files, one binder, and whatever the retiring steward remembers

80%

Quick
wins

  • Roster digitized
  • Cases on the record
  • Documents searchable
90%

Compound
memory

  • Cases linked to articles
  • Precedents captured at close
  • Notes written at handoff
100%

Last
mile

  • The hardest part
  • No software replaces judgment
  • Every irreversible step pauses for a steward

Graph: The Grievance Workflow

The Union Hub
__start__ intake informal_resolution_flow __start__ unit_steward member_conversation employer_meeting⏸ interrupt = after precedent_check resolved? __end__ most issues end here — resolved before a case number exists formal_grievance_flow __start__ chief_steward file_grievance step_1 deadline_watchtimelines never slip step_2 arbitration⏸ interrupt = before settle_early __end__ precedent_capture __end__ ← every closed case teaches the next one

The workflow the app enforces — interrupts are the moments a human must decide; nothing irreversible happens without one.