Adds all Phase 0 content that was created but never committed: - CLAUDE.md and SCOPE.md — agent and developer orientation - specs/TailwindForInteractionHubs_v0.2.md — IHF Tailwind coding guide - docs/ — five IHP v1.5 reference guides (overview, data, controllers, realtime, ihf-mapping) - workplans/IHUB-WP-0001 — Phase 1 implementation plan (12 tasks, state-hub synced) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.0 KiB
IHP: Realtime Capabilities
IHP offers four distinct realtime mechanisms. Choosing the right one for a given IHF surface is important — they have very different trade-offs.
Summary Comparison
| Mechanism | Best For | Client Requirement | State Lives |
|---|---|---|---|
| AutoRefresh | Read-heavy live dashboards | Vanilla JS (morphdom) | Server / DB |
| DataSync | Reactive JS components embedded in server pages | JS SDK + bundler | DB (via RLS) |
| Server-Side Components | Rich interactive UI with Haskell state machine | Vanilla JS | Server (Haskell) |
| HTMX | Partial page updates triggered by user actions | htmx.js | Server |
1. AutoRefresh
Enables server-side views to automatically re-render when the underlying database changes. No client-side framework or state management required.
How It Works
- Wrap an action with
autoRefresh do ... - IHP tracks which DB tables the action's queries touched (via PostgreSQL LISTEN/NOTIFY)
- A WebSocket connection is established when the page loads
- On any INSERT/UPDATE/DELETE to tracked tables, the server re-runs the action
- If the generated HTML differs, the delta is sent over WebSocket;
morphdomapplies it to the DOM with minimal DOM mutations
action HubDashboardAction { hubId } = autoRefresh do
hub <- fetch hubId
widgets <- query @Widget |> filterWhere (#hubId, hubId) |> fetch
recentEvents <- query @InteractionEvent
|> filterWhere (#hubId, hubId)
|> orderByDesc #occurredAt
|> limit 20
|> fetch
render HubDashboardView { .. }
Layout Requirements
defaultLayout inner = [hsx|
<head>
{autoRefreshMeta}
<script src="/vendor/morphdom.js"></script>
<script src="/vendor/ihp-auto-refresh.js"></script>
</head>
...
|]
Custom SQL Tracking
For sqlQuery calls that bypass the query builder, manually register table reads:
trackTableRead "interaction_events"
IHF Use Cases
- Hub operator dashboard — live widget counts, recent interaction signals, annotation feed
- Governance ledger view — live requirement candidate list
- Triage board — live annotation queue
AutoRefresh is the right choice for all of these: server-rendered, no JS complexity, updates arrive automatically.
2. IHP DataSync
A WebSocket-based API that allows JavaScript to query the database directly, mirroring the Haskell query builder in JS. For reactive JS components embedded in otherwise server-rendered pages.
JavaScript API
// One-time fetch
const annotations = await query('annotations')
.where('widget_id', widgetId)
.orderBy('created_at', 'DESC')
.fetch();
// Realtime subscription (React hook)
const { records: annotations } = useQuery(
query('annotations')
.where('widget_id', widgetId)
.orderBy('created_at', 'DESC')
);
// Mutations
await createRecord('annotations', { widgetId, body, category });
await updateRecord('annotations', id, { body });
await deleteRecord('annotations', id);
When useQuery() establishes a subscription, all connected clients automatically re-render when the query result changes.
Security: Row Level Security
DataSync relies on PostgreSQL Row Level Security (RLS). IHP runs with two DB roles:
- Privileged owner role — used by Haskell server code
ihp_authenticated— used by DataSync (what JS clients see)
Example RLS policy:
-- Users can only see their own annotations
CREATE POLICY "Users can view own annotations" ON annotations
FOR SELECT
USING (actor_id = ihp_user_id());
-- Anyone can view annotations on public widgets
CREATE POLICY "Public widget annotations are readable" ON annotations
FOR SELECT
USING (EXISTS (
SELECT 1 FROM widgets w
WHERE w.id = annotations.widget_id
AND w.policy_scope = 'public'
));
Setup Requirements
Node.js/npm, a frontend bundler (esbuild recommended), DataSync JS SDK, WebSocket + REST API controllers enabled in FrontController.
IHF Use Cases
- Annotation composer — reactive annotation entry form with live thread updates
- Widget feedback inbox — live feed of all annotations on a specific widget for the widget owner
- Cross-tenant widget embeds — widget UIs embedded in third-party pages; RLS isolates each widget's data
3. Server-Side Components
React-like components that run entirely on the server. Rich interactive UI with a Haskell state machine — no JS state management needed.
Structure
A component has:
- State — a Haskell data type
- Actions — a sum type
- Render — HSX function from state to HTML
componentDidMount— hook for async data loading on connect
-- Web/Component/AnnotationComposer.hs
data AnnotationComposer = AnnotationComposer
{ widgetId :: !(Id Widget)
, body :: !Text
, category :: !Text
, submitted :: !Bool
}
data AnnotationComposerController
= UpdateBodyAction { body :: !Text }
| UpdateCategoryAction { category :: !Text }
| SubmitAnnotationAction
deriving (Eq, Show, Data)
instance Component AnnotationComposer AnnotationComposerController where
initialState = AnnotationComposer
{ widgetId = error "set by mount"
, body = ""
, category = "friction"
, submitted = False
}
action state UpdateBodyAction { body } = pure state { body }
action state UpdateCategoryAction { category } = pure state { category }
action state SubmitAnnotationAction = do
createRecord (newRecord @Annotation
|> set #widgetId state.widgetId
|> set #body state.body
|> set #category state.category)
pure state { submitted = True, body = "" }
render state = [hsx|
<div class="annotation-composer">
<textarea
value={state.body}
onInput={callServerAction' UpdateBodyAction { body = inputValue }}
/>
<select onInput={callServerAction' UpdateCategoryAction { category = selectValue }}>
<option value="friction">Friction</option>
<option value="wish">Wish</option>
<option value="defect">Defect</option>
</select>
<button onclick={callServerAction SubmitAnnotationAction}>Submit</button>
</div>
|]
Lifecycle
- Client triggers action:
callServerAction('SubmitAnnotationAction') - Server evaluates action handler → produces new state
- Server re-renders → diffs HTML → sends granular DOM patch over WebSocket
morphdomapplies patch (preserves third-party JS library DOM state)
Registration
-- Web/FrontController.hs
routeComponent @AnnotationComposer
-- In a view:
{component @AnnotationComposer}
Current Status
Described as "early development stage — expect bugs and API changes." Suitable for experimental use but not for core production paths yet.
IHF Use Cases
- Annotation entry UI with live category validation and preview
- Widget triage panel with drag-to-sort and inline editing
- Governance decision form with multi-step workflow state
4. HTMX Integration
IHP pairs cleanly with htmx (both are hypermedia-first). Use respondHtml (partial renders without layout) in controller actions:
action AppendAnnotationAction { widgetId } = do
let annotation = newRecord @Annotation
annotation
|> fill @'["body", "category"]
|> validateField #body nonEmpty
|> ifValid \case
Left _ -> respondHtml [hsx|<div class="error">Body required</div>|]
Right annotation -> do
annotation <- createRecord annotation
respondHtml (renderAnnotation annotation)
Reinitialise htmx after Turbolinks page transitions or AutoRefresh updates:
document.addEventListener('turbolinks:load', () => htmx.process(document.body));
IHF Use Cases
- Governance ledger append — POST an approval/rejection action → server appends to ledger and returns updated fragment
- Annotation triage status update — inline status toggle without page reload
- Requirement candidate promotion — single-button action that updates status and returns the updated card
Decision Guide for IHF Phase 1
| Surface | Recommended Mechanism | Rationale |
|---|---|---|
| Hub dashboard (live signal counts, recent events) | AutoRefresh | Server-rendered, zero JS complexity |
| Widget detail page (event history) | AutoRefresh | Same — read-heavy, aggregate view |
| Annotation feed (live thread) | AutoRefresh or DataSync | AutoRefresh if all users see the same view; DataSync if per-user filtered |
| Annotation entry form | Server-Side Components (experimental) or HTMX | SSC for rich state machine; HTMX for simple append |
| Widget registration flow | Standard IHP forms | No realtime needed |
| Triage board (drag, inline edit) | Server-Side Components | Rich interaction state |