Skip to content

m0hill/honostar

Repository files navigation

HonoStar

A runtime-agnostic meta-framework for building hypermedia-driven web applications with real-time updates. Built on Hono and Datastar.

Philosophy

HonoStar embraces server-rendered HTML as the source of truth. Every navigation is a real link, every state change happens on the server, and real-time updates flow through Server-Sent Events (SSE). No optimistic updates, no client-side sync enginesβ€”just declarative HTML that self-heals.

  • Multi-Page App (MPA): Traditional navigation with progressive enhancement via View Transitions
  • Server Authority: All mutations happen server-side; the server re-renders and broadcasts canonical HTML
  • Real-Time SSE: Live updates via SSE patches morph DOM without full page reloads
  • Declarative Reactivity: Datastar signals for ephemeral UI state, data-* attributes for behavior
  • Fat Patches: Broadcast entire regions so clients self-heal after missed events or reconnects

Key Features

🎯 Zero-Config Hypermedia MPA

  • File-based routing (src/pages/) with Next.js-style conventions
  • Type-safe route helpers with automatic parameter extraction
  • SSE event bus for real-time updates (in-memory, Redis, or NATS)
  • Built-in CSRF protection and CSP with nonce support

πŸ”„ Declarative Real-Time Updates

  • Replies (c.var.fx.reply()) - Tab-scoped updates for validation errors, toasts, modal state
  • Domain events (c.var.fx.publish(topic, name, payload)) - Commands publish; queries re-render fat patches on SSE
  • Fat patches - Re-render entire regions so clients self-heal after reconnects

🎨 Modern DX

  • Hono JSX for server-side rendering
  • shadcn/ui components pre-converted for Hono JSX
  • Theme system with zero-FOUC server/client coordination
  • Runtime-agnostic: works with Node.js, Bun, Deno

πŸ” Security Built-In

  • CSRF token validation with configurable exemptions
  • CSP with per-request nonces
  • SSE topic signing to prevent unauthorized subscriptions
  • HMAC-based topic authorization (zero shared state)

Quick Start

# Install dependencies
bun install

# Run development server
bun run dev

# Open http://localhost:3000

Core Concepts

Pages & Handlers

Pages render full HTML documents:

// src/pages/issues.tsx
export default defineQueryPage({
  use: [requireAuth], // Optional middleware
  loader: async (c) => ({
    issues: await fetchIssues(c)
  }),
  component: (props) => <IssuesList issues={props.issues} />,
  topics: [topics.issues.list()], // Auto-subscribe to SSE topic
  queries: [[topics.issues.list(), issuesListQuery]], // Re-render on domain events
})

Handlers (commands) mutate state and publish domain events:

// src/pages/issues.tsx
export const POST = defineCommand({
  use: [requireAuth],
  async handler(c) {
    const input = await c.req.json()
    const issue = await createIssue(input)

    // CQRS: publish domain event; queries re-render subscribed regions.
    c.var.fx.publish(topics.issues.list(), 'issue:created', { id: issue.id })

    return c.var.fx.reply([
      ['patch-signals', { modal: { open: false } }]
    ])
  }
})

Replies vs Domain Events

Use When API
reply() Tab-scoped feedback (validation errors, toasts) Targets the initiating tab only
publish(topic, name, payload) Shared state changes (create/update/delete) Fans out an event; queries re-render canonical HTML

Rule: Every shared state change must publish through a topic defined in src/lib/topics.ts.

reply() now inspects the incoming request and, when it comes from a Datastar action, automatically returns HTTP patches for simple built-in effects (patch-elements, patch-elements-seq, patch-signals). The response includes the required datastar-* headers so the client can morph the DOM without needing an SSE connection. More complex replies (custom effects, execute-script, multi-effect responses) continue to use SSE just like before.

SSE Patch Discipline

// βœ… Good: Query returns a fat patch (default outer morph)
export const issuesListQuery: QueryHandler = async ({ c }) => {
  const allIssues = await fetchIssues(c)
  return [['patch-elements', <IssuesList issues={allIssues} />]]
}

// βœ… Good: Append modal to overlay container
c.var.fx.reply([
  ['patch-elements', <Modal />, { selector: '#ds-overlays', mode: 'append' }]
])

// ❌ Bad: Redundant explicit mode
['patch-elements', component, { mode: 'outer' }] // Remove mode option

Fat Patches Principle: Send entire regions (lists, tables, cards) so clients can self-heal after missed events or reconnects. Avoid incremental append/prepend unless absolutely necessary (infinite scroll, chat).

Datastar Attributes

HonoStar uses Datastar's declarative attribute API:

<form
  data-on:submit="@post('/issues', { contentType: 'json' })"
  data-signals:creating="false"
  data-indicator:creating.class.opacity-50="creating"
>
  <input data-bind:title type="text" />
  <button type="submit">Create Issue</button>
</form>

<div
  id="issues-list"
  data-show="$issuesLoaded"
  style="display:none"
>
  {/* Server renders this, SSE morphs it on updates */}
</div>

Key Rules:

  • Signals are ephemeral UI state, never persistence
  • data-computed must be pure (use data-effect for side-effects)
  • data-show requires style="display:none" to prevent FOUC
  • Indicators must come before data-init so signals exist before requests start

Configuration

Zero-config works out of the box. Override via HonostarConfig:

// src/index.ts
const config = {
  assets: {
    css: '/styles.css',
    runtime: '/runtime.js',
    datastar: '/datastar.js'
  },
  endpoints: { sse: '/_/events' },
  security: {
    csp: "script-src 'self' 'unsafe-eval' 'nonce-${nonce}';",
    csrf: {
      cookieName: 'ds_csrf',
      exceptPaths: ['/webhooks']
    },
    topics: {
      secretEnv: 'HONOSTAR_SIGNING_SECRET', // Required in production
      maxAgeSec: 300
    }
  }
}

app.use('*', csrf(config))
app.use('*', renderer(config))
app.get('/_/events', createSseEndpoint(config))

Multi-Instance Scaling

HonoStar supports horizontal scaling via Redis or NATS for distributed SSE:

# Option 1: NATS (checked first)
export NATS_URL="nats://localhost:4222"
# Or Honostar-specific
export HONOSTAR_NATS_URL="nats://user:pass@host:4222"

# Option 2: Redis (checked second)
export REDIS_URL="redis://localhost:6379"
# Or Honostar-specific
export HONOSTAR_REDIS_URL="redis://user:pass@host:6379"

Bus Priority: NATS β†’ Redis β†’ MemoryBus (in-process fallback)

All three implementations satisfy the same PubSubBus interface, so your application code remains identical regardless of which bus is active.

Type-Safe Routing

Routes are auto-generated from src/pages/ file structure:

import { routes } from '@/routes'

// Type-safe links
<a href={routes.issues.href()}>All Issues</a>
<a href={routes.issues.id.href({ id: 123 })}>Issue #123</a>

// In handlers
return c.redirect(routes.issues.id.href({ id: created.id }))

Route manifest regenerates automatically on dev/build.

No Optimistic Updates, No Sync Engine

HonoStar's architecture eliminates the complexity of client-side state synchronization:

  • Server renders canonical HTML - The server is the single source of truth
  • SSE broadcasts patches - Clients receive morphing instructions, not raw data
  • Fat patches self-heal - Missed events don't cause drift; next patch includes full state
  • No reconciliation logic - Datastar morphs DOM; no React-style diffing or manual sync

This means:

  • No "loading β†’ optimistic β†’ actual" state juggling
  • No conflict resolution or rollback logic
  • No client-side caching/invalidation strategies
  • Clients are always eventually consistent with server state

Architecture

src/
β”œβ”€β”€ core/                  # Framework internals
β”‚   β”œβ”€β”€ router/           # File-based routing + manifest generator
β”‚   β”œβ”€β”€ datastar/         # SSE bus, responders, formatters
β”‚   β”œβ”€β”€ security/         # CSRF, CSP, topic signing
β”‚   └── runtime/          # Client-side runtime (theme, modals, etc.)
β”œβ”€β”€ pages/                # Routes (auto-discovered)
β”œβ”€β”€ components/           # Reusable UI components
β”‚   └── ui/              # shadcn/ui (Hono JSX)
β”œβ”€β”€ middleware/           # App middleware (auth, bus, db)
└── lib/                  # Utilities, topics, auth

Runtime Compatibility

HonoStar runs on any platform supported by Hono:

  • βœ… Bun (recommended for development)
  • βœ… Node.js (v18+)
  • βœ… Deno
  • βœ… Cloudflare Workers
  • βœ… Vercel Edge
  • βœ… AWS Lambda

Documentation

For detailed engineering guidelines, see AGENTS.md.

License

MIT

About

Runtime-agnostic meta-framework for hypermedia apps with real-time SSE updates

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages