Datapages v0.10.0 (Beta)

After 7 long months, Datapages finally leaves Alpha and enters Beta! 🥳 It's now ready for production use and ships a lot of new features, fixes and DX improvements!

In total, 8 new major features, 131 fixes, and 3 new examples are shipped. And check out that cool new logo 🤩

Note

Beta: The API is now close to stable but can still change before v1.0.

I still want to give myself time to use it in production and make some final polish, if I have to, before I commit to a stable v1.0.0.

Features

The github.com/romshark/datapages package and better API explorability.

The handler types, the HTTP error sentinels and every server option now lives in one public package. This makes Datapages easier to explore - simply import the package, type datapages. and explore the API using the intellisense features of your IDE.

Type-driven handlers and less dependencies

Parameters and return values are now matched by types from the datapages package, not by name. Path values, query values, signals, the session, events, the stream ID and the SSE writer are all recognized by their type.

The SSE writer and component renderers are now native types, and eliminate the direct dependence on Templ and the Datastar Go SDK.

Stateful pages

The officialy recommended architecture for Datastar apps is the CQRS architect. Datapages now makes this architecture even easier to implement correctly by providing a built-in per-tab server-side state mechanism.

Offline shim and cached pages

Service workers are a powerful browser API, which unfortunately requires quite a bit of JavaScript. Datapages now removes the need to write custom service workers and gives you new Go-only APIs to implement instantly loading page shims and offline cached pages.

See the new examples:

Agentic coding skills

datapages init now writes instruction and skills for agentic coding setups like Claude, Cursor, Codex, Copilot and Gemini into the project, and datapages gen keeps them up to date.

This feature can of course be opted-out from using the flag --no-ai-skills.

Multiple applications per module

One Go module can now build more than one application that conveniently share one code base. A public site and its admin panel can sit next to each other and share the same domain logic, components, assets, and also optionally one event bus. while each keeps its own pages, routes and generated code. datapages watch --app <name> runs the one you are working on.

Event dispatching

Fan-out is explicit now. One dispatch is one publish that reports its own error, instead of a slice of recipients delivered in one opaque call, which left you to decide what a partial failure means.

Delivery moved from JetStream to core NATS, which means no stream configuration needs to be maintained now.

Sessions

Expired sessions can now be garbage collected. Each store can delete expired sessions, while your application decides when cleanup runs.

You can also list and terminate all sessions for a user, making it easy to implement features like "sign out everywhere."

Session cookies now always use Secure. CSRF tokens are derived directly from the session token, which means there is no secret to configure, no CSRF_SECRET to deploy and no store read per request. CSRF protection is enabled by default for every application that uses session based authentication.

The token is an HMAC-SHA256 of the session token, and every render masks it with a fresh random value, inspired by the implementation of gorilla/csrf.

Security and stability hardening

Cross-origin action requests are rejected, panics in handlers and stream teardown are recovered, request bodies are properly size-limited, CSP nonces are now supported, and potential XSS holes have been fixed.

Windows support

datapages watch now also runs on Windows after the upgrade to Templier v0.13.0.

Performance

Several optimizations reduced per-request memory allocations and asset responses now validate the client cache.

Changelog

- f7e7f9b: feat!: Abstract Datastar behind datapages.SSE (@romshark)

- 1df1973: feat!: Provide sessions as datapages.Session (@romshark)

- 82dbb0a: feat!: Provide redirects as datapages.Redirect (@romshark)

- 405a7f4: feat!: Provide HTTP error sentinels in datapages (@romshark)

- 1b8eac0: docs(README): Remove obsolete badge (@romshark)

- e2c96b7: fix: Regenerate templ code and fix a natskv test (@romshark)

- 0be4c35: chore(example): Upgrade container images (@romshark)

- 9dc5ab9: feat!: Accept datapages.Component in handlers (@romshark)

- da793d2: fix: Prevent recursion between error handlers (@romshark)

- 37069db: fix: Acceptance test suite and 16 defects it found (@romshark)

- d1f179c: fix: Subscribe before sending SSE head (@romshark)

- ba45b3d: fix: Escape query values reflected into signals (@romshark)

- 8df664d: fix: App shapes that pass lint but do not build (@romshark)

- 266a58d: fix: Catch broken routes before the server runs (@romshark)

- e55efca: feat!: type-driven subject fields and dispatchers (@romshark)

- 6f33700: fix: Respect the broker channel buffer size conf (@romshark)

- 3917956: fix: Refuse wildcards in subject signals (@romshark)

- 9cbab75: refactor!: Publish events on core NATS (@romshark)

- a72db16: fix: Refuse wildcards in dispatched subjects (@romshark)

- d166e94: fix: Refuse wildcards in session user IDs (@romshark)

- 3de3cb7: fix: Keep empty user IDs out of stream subjects (@romshark)

- fc5b159: fix: Refuse overlapping event subjects (@romshark)

- 039b6f5: refactor: Share go/types predicates (@romshark)

- be3c776: refactor: Share struct tag extraction (@romshark)

- 7d7dc70: refactor: Move route helpers into routepattern (@romshark)

- c694a66: refactor: Single-source the subject rules (@romshark)

- 1fb2297: refactor: Simplify the init scaffolding (@romshark)

- 724a72a: refactor: Declare the watch enums in one place (@romshark)

- 9777747: fix: XSS in the data-init stream URL (@romshark)

- 03379b9: feat!: Replace patch options with PatchElementAt (@romshark)

- d8892fb: perf: Build the removal event without a slice (@romshark)

- 1fd24a7: feat!: Replace func with Dispatcher interface (@romshark)

- c46fdec: feat!: Match path, query and signals by type (@romshark)

- f32eb6d: feat!: Match streamID by type (@romshark)

- 307116c: feat: Match session by type (@romshark)

- 898c021: feat!: Match return values by type (@romshark)

- a52f81e: feat: Match the event parameter by type (@romshark)

- b05a487: feat!: Return datapages.Head from App Head method (@romshark)

- d8bf036: feat: Match RecoverError parameters by type (@romshark)

- 02761ae: feat: Match Head parameters by type (@romshark)

- a0cb037: perf: Remove per-request allocations (@romshark)

- f820e59: refactor!: Move shared code into runtime packages (@romshark)

- 89ff506: refactor: Move the shared HTTP code to pkg runtime (@romshark)

- 61c3c7e: feat!: In-code codegen config and multiapp modules (@romshark)

- 026d5c9: refactor!: Improve API naming (@romshark)

- c10ea46: feat!: Derive CSRF tokens from the session token (@romshark)

- 3132ed9: refactor: Move repeated generated code to runtime (@romshark)

- ca7080b: refactor: Move action expressions to runtime pkg (@romshark)

- 4f19834: fix: Trim wildcard URLs like /files/{rest...} (@romshark)

- e9679e5: fix: Mirror NATS Core behavior on publish counts (@romshark)

- 727a81f: fix: Accept any user ID and subject value (@romshark)

- 8e6d224: fix!: Register metrics on every registerer (@romshark)

- e07d031: fix: Recover panics in stream teardown (@romshark)

- ddf487f: fix!: Encode both halves of the session key (@romshark)

- 8be9c74: feat!: Delete and garbage collect expired sessions (@romshark)

- ef8037f: fix: Reject a dot-imported datapages package (@romshark)

- 85dcfbe: chore(deps): bump lit (#29) (@dependabot[bot])

- 3983c29: fix: Drop static WithSessionManager check (@romshark)

- 0340d26: fix: Report an unreadable .env in the skeleton (@romshark)

- 888c692: fix: Limit body size of an action that takes SSE (@romshark)

- b17a289: fix: Refuse a CSS selector containing a line break (@romshark)

- e1c175e: fix: Reject type parameters on generated types (@romshark)

- 266f8ad: feat: Reject a Page name that is not a struct (@romshark)

- 1d9a153: fix: Resolve href and action imports per file (@romshark)

- dbdc662: fix: Bound the HTTP metric labels (@romshark)

- 6d02aef: feat!: Always set Secure on the session cookie (@romshark)

- e12da37: build: Pin the templ version init generates with (@romshark)

- db846b6: fix: Pin datapages in the scaffolded CI (@romshark)

- ec098f5: fix: Escape line breaks in action option values (@romshark)

- f80cbcc: fix: Reject an app package that does not compile (@romshark)

- 696d6d6: fix: Convert a parsed value to its named type (@romshark)

- 6558278: fix: Check stream and asset routes for conflicts (@romshark)

- 240cbb5: fix: Parse a path or query field via UnmarshalText (@romshark)

- 934f8ce: feat!: Write a TextMarshaler through MarshalText (@romshark)

- 972a3a5: fix: Report a page method declared on App (@romshark)

- 9053fd9: fix: Run gen through env in watch mode (@romshark)

- 6ffbfbd: fix: Reject an unexported type in a handler struct (@romshark)

- 55ad93d: fix: Make natscore Close idempotent and concr-safe (@romshark)

- 41a2897: fix: Drop the empty segment from the stream URL (@romshark)

- 491258c: feat: Add WithBodySizeLimit (@romshark)

- 6176a24: fix: Render a marshaling value through MarshalText (@romshark)

- ceb6c9d: fix: Warn about an href the templ sanitizer drops (@romshark)

- de8b1e9: feat!: Recover panics, skip the CSRF store read (@romshark)

- 074e1de: feat: Add DATAPAGES_DEV_MODE env var (@romshark)

- f7e1c2b: fix: Redirect through SSE when action holds stream (@romshark)

- 8c602e7: refactor: Test the server lifecycle in the runtime (@romshark)

- 8486aad: feat!: Add method Addr to Server interface (@romshark)

- 7d1c0a4: fix: End natskv NotifyClosed watcher on lost conn (@romshark)

- 43a001e: fix: Stop storing session cookies in natskv KV (@romshark)

- 3b436e3: fix: Reject a natskv record naming another user (@romshark)

- 47eacb0: feat!: Declare the per-user session interfaces (@romshark)

- 85a199a: fix: Share the session error values across stores (@romshark)

- c501af7: fix: Default the Core logger in NewCore (@romshark)

- bcf2a5d: fix: Keep request context alive across a shutdown (@romshark)

- 759a831: fix: Recover a StreamOpen panic (@romshark)

- 7b14030: fix: Make Shutdown safe before ListenAndServe (@romshark)

- db24490: fix: Stop dropping grouped subject fields (@romshark)

- c596d69: fix: Restore deadlines and oversized-body close (@romshark)

- 1c87d7d: fix: Report missing route variables in route order (@romshark)

- e06dfeb: fix: Validate the declared event subject (@romshark)

- 9dd91ea: fix: Hold Segments to its literals/vars invariant (@romshark)

- 11e1660: fix: Collect the session type after flattening (@romshark)

- 1b1e74e: fix: ResponseBodyWritten false behind middleware (@romshark)

- f63969f: fix: Inherited GET reads path variables wrong (@romshark)

- 550d27c: fix: 404 status sent before PageError404.GET (@romshark)

- 0392e25: refactor: Fix comments and ignored errors in tests (@romshark)

- 771c304: fix: Doubled "v" in the CLI version (@romshark)

- 1adee30: fix: Dev build bumps go.mod to a pseudo-version (@romshark)

- 3eaf1dd: fix: StreamClose runs before the last handlers (@romshark)

- 9631660: fix: Unparsable main.go silently drops the app (@romshark)

- a23edf1: fix: WithHeaders renders in random order (@romshark)

- 2c740a9: feat: Drop and warn-log invalid action options (@romshark)

- 3cbf08b: feat: Refuse and report invalid action options (@romshark)

- 1d55eb9: fix: Non-identifier query tag breaks datapages gen (@romshark)

- 9ec2eaa: fix!: Inline sign-in renders with no CSRF script (@romshark)

- eebb864: fix: Reject the sse parameter on App methods (@romshark)

- 8c0bee2: fix: Anon stream redirect drops ? and # in a path (@romshark)

- 53c1bb8: fix: Empty option value breaks the attribute (@romshark)

- 1434f03: fix: Two actions can generate one identifier (@romshark)

- 954770e: fix: Wildcard named like a generated parameter (@romshark)

- 87dc373: fix: 404 and 500 pages drop the CSRF script (@romshark)

- 0fc415b: fix!: SSE streams skew the request latency metrics (@romshark)

- 45de2d8: fix: hrefcheck disagrees with the templ sanitizer (@romshark)

- 89a2ce8: fix: A //go:build ignore file stops datapages gen (@romshark)

- 969cce8: fix: requests_total labels a status never sent (@romshark)

- 10f0d0c: perf: Asset response allocates a copy buffer (@romshark)

- fde9a7c: fix: Empty cause when a command cannot start (@romshark)

- fbbc61b: fix: A nil buffer drops the closed session tokens (@romshark)

- 8a07851: fix: Validate the Datastar JS bundle URL (@romshark)

- 98ee139: fix: A derived subject type broadcasts an event (@romshark)

- ec8f90a: fix: A status lands in an open event stream (@romshark)

- 2261a07: fix: Refuse an action head without a body (@romshark)

- 532b480: fix: An unmatched path tag panics datapages gen (@romshark)

- 394176c: fix: An unterminated route brace panics gen (@romshark)

- 321546c: fix!: Name collisions in generated code (@romshark)

- 8778a7a: fix: More name collisions in generated code (@romshark)

- 4c1c2df: refactor!: Name parser error types the Go way (@romshark)

- 17bec5b: fix: init scaffolds a project that doesn't build (@romshark)

- aaadc9c: fix: Pass the session to the app-wide Head (@romshark)

- 3753ccc: feat: Allow event type decl outside app package (@romshark)

- 9fbfcad: fix: Separate generated HTML attributes (@romshark)

- 94f3ac9: fix: Keep a route wildcard from shadowing len (@romshark)

- 1e734c9: fix: Refuse a page embed that breaks the build (@romshark)

- 31b074f: fix: Correct signal name handling (@romshark)

- b1ba253: fix: Reject a cmd path that leaves the module root (@romshark)

- adabc5c: fix!: Stop making asset dirs browsable by default (@romshark)

- 36fb7a0: fix: Reject cross-origin action requests (@romshark)

- 3319092: fix: Update IsDatastarRequest call sites (@romshark)

- c86b673: fix: Reject non-finite float URL values (@romshark)

- 816053a: feat: Stateful Pages (#32) (@romshark)

- 8c15cd1: fix: Check stream routes for action state (@romshark)

- 32a1b9b: fix: Reject SSE parameters in GET handlers (@romshark)

- 138f95c: fix: Correct fix hints and multi-app init (@romshark)

- fd65a10: fix: Restrict RecoverError to Datastar requests (@romshark)

- 87c85cc: fix: Preserve page GET sentinel status codes (@romshark)

- ba2bded: fix: Preserve unreflected query parameters (@romshark)

- c95cf22: refactor: Run go fix (@romshark)

- 34ed8ce: fix: Escape path values in URL sync scripts (@romshark)

- 2917ad8: fix: Quote reflected text values (@romshark)

- 7a6d41f: fix: Reject duplicate reflectsignal targets (@romshark)

- f567bbc: fix: Reject reflected signal type mismatches (@romshark)

- 545cff8: fix: Export action option type (@romshark)

- 101b088: feat: Add option WithShutdownTimeout (@romshark)

- b378af4: feat: Add asset cache validation (@romshark)

- 33e05dd: fix: Correct dev mode, install path, route doc (@romshark)

- 673faf7: fix!: Set session cookie expiry from ExpiresAt (@romshark)

- 8e7cd31: fix(example): Use hyphenated keys for signals (@romshark)

- 23aebe5: feat: Generate AI Coding Agent Instructions (#34) (@romshark)

- 16601c1: fix: Drop CSRF_SECRET from the scaffolded .env (@romshark)

- 9a2f1d6: feat: Add datapages-architecture skill (@romshark)

- cc17eb5: fix: Stop PageError500 panic recursion (@romshark)

- 76b546c: fix: Generate session outputs on a page GET (@romshark)

- 685c1f4: fix: Reload only pages that have a stream (@romshark)

- 362a6c7: fix: Write the 500 status after the session read (@romshark)

- 1084b8d: fix: Keep URL fragment on reflected signal sync (@romshark)

- c66224c: perf: Read the session once on an unclaimed path (@romshark)

- b594533: fix: Close the previous session when signing in (@romshark)

- cf04306: fix(example): App-internal URLs (@romshark)

- 8934502: fix: Correct defects in the classifieds example (@romshark)

- 6124589: refactor: Run go fix (@romshark)

- 363b853: feat: Add WithCSPNonce (@romshark)

- 271cd4e: fix: Reject newSession/closeSession on error pages (@romshark)

- e858dc1: fix: Render the head an App-level action returns (@romshark)

- fc19b11: fix: Limit init version lookup time (@romshark)

- bbba2fc: fix: Reject subject fields tagged json:"-" (@romshark)

- c2617e6: fix: Pin the CLI in the scaffolded CI workflow (@romshark)

- c51bc96: fix: Use one Prometheus default in the scaffold (@romshark)

- f3174a8: fix: Report the owner of a conflicting route (@romshark)

- b921109: fix: Require text form on reflected query fields (@romshark)

- 5004427: fix: Scope the action logger per application (@romshark)

- 3b560ba: feat: Service-worker Page Cache (#33) (@romshark)

- 4dcff8d: fix: Confine templ dev mode to the module (@romshark)

- ce7acda: fix: Guard templ generation during watch (@romshark)