Starbase
Community Components for Rocket. A community gallery of Rocket web components for Datastar, in the spirit of shoelace.style.
Go · templ · SQLite · Datastar + Rocket · CQRS · plain modern CSS. No Node, no bundler.

Features
- 51 components (forms, navigation, feedback, layout, media, data, utilities, drag-and-drop surfaces from derekr's PD rockets, and a multiplayer pixel board), each with live demos, API tables generated from its Rocket manifest, and an auto-generated props Playground. The seven from PD rockets keep its Beer-Ware licence; everything else is MIT.
- Code playground: edit a component's JS (or TypeScript) and HTML in a sandboxed live preview, type-check it against Datastar's types, save and share immutable links, submit the result as a component.
- Submissions without tools: a GitHub issue form (paste code, link a repo or a playground link); a bot validates it and opens a pull request.
- Autoloader: one
<script>loads each<sb-*>component on first use, on any site. - Showcase: a live Mission Control dashboard and a multiplayer pixel board, both server-driven.
- Realtime everything: every tab keeps one compressed SSE render stream (zstd or Brotli); state changes are CQRS commands.
- Themes, pixel-art design system, zero Node, single static binary with embedded assets and SQLite.
Run a release
Binaries for Linux, macOS and Windows are on the releases page, and a multi-arch image on GHCR:
docker run -p 7331:7331 -v starbase-data:/data ghcr.io/zweiundeins/starbase:latest
The binary is self-contained (assets, components and migrations are embedded; SQLite is pure Go). starbase --version prints the version, /healthz answers ok <version>.
Develop
cp .env.example .env # optional: add GitHub OAuth credentials go tool task live # dev server with live reload on http://localhost:7331 go tool task test # vet + tests (including component validation) go tool task build # production binary in bin/starbase
live, test and build first fetch the pinned TypeScript compiler (go run ./cmd/fetchtsc, once) into internal/tscheck/dist, where the binary embeds it for the playground's type check. A plain go build without it works too, without the type check.
Without GitHub credentials, dev builds sign you in as a fake user at /auth/dev?login=you.
Using the components on your site
<script type="importmap"> { "imports": { "datastar": "https://<starbase>/c/datastar@<build>/datastar-rocket.js" } } </script> <script type="module" src="https://<starbase>/c/autoloader.js"></script> <sb-button variant="pixel" caret>Blast off</sb-button>
To avoid a flash of undefined elements, add class="sb-cloak" to <html> and .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the components present at startup are defined, or after 3 seconds at most. It also exports ready, a promise for the same moment.
The autoloader (generated from the catalog) imports each <sb-*> component the first time its tag appears, as one file (its bundle, see below). It also watches for tags added later, and loads the components a component renders itself. For shadow roots of your own components, call discover(shadowRoot), which it exports.
Pinning, for production
/c/autoloader.js always loads the latest version of each component. In production, pin a snapshot of the whole catalog instead, and let the browser check every file against its hash:
<script type="importmap"> { "imports": { "datastar": "https://<starbase>/c/datastar@<build>/datastar-rocket.js" }, "integrity": { "…": "…the map from https://<starbase>/c/@<catalog>/importmap.json" } } </script> <script type="module" src="https://<starbase>/c/@<catalog>/autoloader.js" integrity="sha384-…"></script>
Every component page's Installation section has this snippet with the current catalog hash and real integrity values filled in. The URLs are immutable and keep working after later releases:
| URL | |
|---|---|
/c/<slug>@<hash>/<file>.js |
one version of a component's file (its module and vendored files) |
/c/<slug>@<hash>/<slug>.bundle.min.js |
one version of a component as one file (see below) |
/c/@<catalog>/autoloader.js |
the autoloader of one catalog snapshot, loading exactly its component versions (their bundles; a snapshot from before the bundles keeps loading the module files) |
/c/@<catalog>/importmap.json |
integrity (SHA-384) for every file that snapshot can load, Datastar's included |
/c/datastar@<build>/datastar-rocket.js |
one build of Datastar + Rocket: the v1.0.4 release with Starbase's fixes (patches/rocket), until they are released upstream |
The components also run on the official Datastar release; the fixes are listed in patches/rocket/README.md.
With integrity in place, a changed file is refused instead of run. The autoloader reports it ([starbase] could not load <sb-…>), and the other components keep working. Every version the site has ever served is kept in its database. To withdraw one (a component that turned out to be malicious), delete its rows from component_files, then the bodies no row refers to any more: DELETE FROM file_bodies WHERE integrity NOT IN (SELECT integrity FROM component_files) (each distinct body is stored once, shared by the versions that have it).
One file per component
A component's modules also come as one minified module, /c/<slug>@<hash>/<slug>.bundle.min.js, which is what the autoloader loads and what a component's size counts. The browser fetches a component's module files in as many rounds as its imports are deep: sb-kanban-board has thirteen, in three rounds. Its bundle is one request. A bundle imports datastar from your import map and nothing else, except a library the component loads on first use, which stays a file next to it (vendor/prism.min.js for sb-code-editor). A component that renders another (sb-code-playground renders sb-code-editor) loads that one's bundle too. The "This component" and "Self-host" tabs of a component page have the snippet, with integrity hashes.
To vendor the same files from a checkout of this repository:
go run ./cmd/dist -out js kanban-board modal # or: go tool task dist -- -out js kanban-board modalThis writes js/kanban-board.js, js/modal.js, the components they render and the files they load on first use, and prints each file's integrity. Each <slug>.js is byte for byte the bundle the site serves for that version; its first line names it, for example /*! sb-kanban-board, version kanban-board@<hash>: its modules in one file */.
Adding a component
No tools needed: use the submission form (a GitHub issue form). Paste the code, or link the GitHub repo the component lives in, and a bot validates it, generates its manifest and opens a pull request. See .github/workflows/component-from-issue.yml.
By hand
go tool task new -- my-widget --category forms --author your-handle
This creates components/my-widget/README.md (front matter + docs, where ```html preview blocks become live demos) and my-widget.js (the rocket('sb-my-widget', …) definition). To write it in TypeScript, rename that to my-widget.ts and run go tool task ts, which type-checks it strictly, with any helper .ts modules and .d.ts declarations in its folder, and writes the .js the site serves next to each module (commit both). Open /components/my-widget in the dev server. The page publishes Rocket's manifest and the server writes manifest.json, which drives the API tables. No Go changes are needed. See /contribute for the house rules.
Every component page also gets a Playground, generated from the manifest: number props become sliders, booleans toggles, oneOf selects and strings inputs. Each control is bound to a local signal that drives the live element through data-attr. Tune it in the README front matter:
playground: props: { yaw: {min: -180, max: 180}, zoom: {min: 0.5, max: 3, step: 0.1} } values: { spin: 30 } # initial values content: Blast off # slotted content of the live element style: "inline-size: 18rem" attrs: { values: "[1,2,3]" } # static attributes, e.g. for props without a control exclude: [href]
Architecture
CQRS in the style of hyperlith, on lith, the runtime of our Go and SQLite apps:
GET /page full server render (SEO), then data-init opens ↓
POST /page the tab's render stream (SSE, zstd or Brotli): the tab's view, rendered again
in the next tick (at most one per 50 ms) after a commit touches it
POST /cmd/... commands: validate → enqueue → 204. Never HTML.
single writer: drain queue → one transaction (savepoint per command)
→ commit → mark the views of the topics it touched (a tab, a session, the board, or all)
-
Write side:
internal/commands(one struct per state change), applied by lith's writer. -
Read side:
internal/queriesruns every render inside one read transaction: a document's own, or a render worker's, which all the pages it renders in a tick share. -
Views:
internal/uiholds templ components, pure functions of the read model. The same function serves the GET and every stream frame. Datastar morphs#app. -
Per-tab UI state (filters, sort, preview theme) is stored in SQLite, in
tab_state. The stream keeps the URL in sync. -
Catalog:
components/is embedded. At startupSyncCatalogmirrors it into SQLite (FTS5 for search). -
Design system:
static/css. Primitive--sb-*tokens feed semantic tokens, which feed components. Layers, container queries, nesting and OKLCH. Themes remap the semantic tokens only. -
Pixel art is generated in Go (
internal/pixelart) and served as cached SVG. -
Multiplayer pixel board (Showcase):
sb-pixel-boardis driven by one server-owned attribute (cells, a hex digit per pixel), which the board's own view renders once for every viewer. Painting is thePaintPixelscommand (POST /cmd/paint, rate-limited per session); it touches only the board, so no one's page renders. The board's view also counts the viewers ("N watching"). -
Code playground:
/playground(and "Open in playground" on every component) edits a component's JS and HTML withsb-code-editorandsb-code-playground. Previews run in asandbox="allow-scripts allow-modals"iframe served by/playground/run, with its own CSP and an opaque origin. A TypeScript switch turnscomponent.jsintocomponent.ts, which the runner has the server transpile (/playground/transpile, esbuild). TypeScript, and JavaScript that starts with// @ts-check, is type-checked on the server against the patched Datastar build's declarations (internal/tscheck: TypeScript's native compiler, embedded in the binary and run as a subprocess) and underlined in the editor. "Save & share" is theSaveSnippetcommand (immutable/playground?s=<id>links). "Submit as component" prefills the issue form with that link, and the bot imports it (set the repository variableSTARBASE_URL). -
Live demo data:
GET /demo/telemetrystreams a simulated mission as signal patches ($_tm). It is a pure function of time, with no state. The Showcase page's Mission Control and the gauge, sparkline and meter docs use it. -
Example dataset: a small universe (galaxies, star systems, planets, moons, bright stars by constellation), seeded into SQLite at startup (
internal/demo,SeedDemo). Two generic, stateless endpoints serve it to any component's docs:GET /demo/data/children?parent=<id>returns the children of a node, or the top level withoutparent.GET /demo/data/search?q=…&kind=star,planet&limit=8returns name matches, prefix matches first, accent-insensitive.
For Datastar requests they patch the signal named by
&into=: a list, or{"<parent>": [...]}for children. WithAccept: application/jsonthey return the list.&delay=<ms>makes loading states visible. Items are{id, value, label, description, icon, kind, lazy}, so they work both as tree items and as select options.
Deployment note: serve over HTTP/2
Every open tab keeps one long-lived SSE connection (its render stream). Over HTTP/1.1, browsers allow only 6 connections per host across all tabs, so a few open tabs can starve new requests. They sit "Stalled" in DevTools. Put the server behind a TLS proxy that speaks HTTP/2 (Caddy, nginx, a CDN), where all streams share one multiplexed connection. Browsers only use HTTP/2 over TLS, so this matters most in production, and when testing with many tabs.
Configuration
| Variable | Default | |
|---|---|---|
ADDR |
:7331 |
listen address, or unix:/path/to.sock for a Unix socket |
BASE_URL |
http://localhost:7331 |
public origin (OAuth callback, CSRF origin, __Host- cookies on https) |
DB_PATH |
data/starbase.db |
SQLite file |
REPO_URL |
https://github.com/zweiundeins/starbase |
"Edit on GitHub" links |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
GitHub OAuth app; callback $BASE_URL/auth/github/callback |
|
GITHUB_TOKEN |
optional read-only token for pull request preview checks (API rate limit) |
Project
- CHANGELOG.md: release notes
- CONTRIBUTING.md: submitting components and working on the site
- SECURITY.md: reporting vulnerabilities
CI runs vet, tests, the templ and manifest checks, govulncheck, cross-platform builds and a container build on every push. Pushing a v* tag publishes a GitHub release (binaries, checksums, notes from the changelog) and the image. Once CI passes on main, it deploys to production, with health checks and automatic rollback: see deploy/.
Licences
Code: MIT, © zwei und eins gmbh and starbase contributors. Fonts: Pixelify Sans and JetBrains Mono (SIL OFL, see static/fonts). Icons: Lucide (ISC), GitHub mark (MIT, Octicons). Datastar + Rocket bundle: MIT, vendored in static/vendor. TypeScript compiler: Apache-2.0, embedded for the playground's type check; release archives carry its licence and notices in typescript/, the container image in /usr/share/doc/typescript.
Maintainer
Maintained by zwei und eins gmbh, Aarberg, Switzerland.