A ClojureScript + React (via Reagent + Re-frame) starter template designed for cloning. Build new SPAs by cloning this repo, renaming the namespace, and deleting the demo pages.
- Overview
- Tech Stack
- Project Structure
- Getting Started
- Architecture
- Routing
- Feature Flags
- GitHub Pages Deployment
- How to Turn Clojkstra into a New App
- Extension Points
- Linting and Formatting
- Nix Dev Shell
Clojkstra is not a one-off app. It is a reusable starter template, engine, and vehicle for future projects. Every architectural decision is documented and every file is clearly labelled as either a framework file (keep it) or a demo file (safe to delete and replace).
The included SPA demonstrates:
- Hash-based client-side routing (works on GitHub Pages with no server config)
- re-frame event dispatch and subscriptions wired end-to-end
- A reusable Reagent UI component library
- Global notification toasts, loading overlays, and error banners driven by app-db
- Feature flags, config tokens, and a runtime-updatable theme surface
- A
reg-event-fxeffect handler library (navigate, set-title, localStorage, log, setTimeout) - Local ratom state patterns alongside app-db patterns
| Package | Role | Version |
|---|---|---|
| ClojureScript | Language | 1.11 |
| re-frame | State management + event bus | 1.4 |
| Reagent | React wrapper | 1.2 |
| shadow-cljs | Build tool | 2.28 |
| Bun | JS runtime + package manager | 1.1 |
| Tailwind CSS | Utility CSS (CDN in dev) | 3.x |
All dependencies are FOSS-licensed. npm and node are never used — everything goes through bun / bunx.
Clojkstra/
├── src/
│ └── clojkstra/
│ └── app/
│ ├── core.cljs # Entry point — init, hot-reload, mount
│ ├── db.cljs # App-db schema and default state
│ ├── events.cljs # All re-frame event handlers
│ ├── subs.cljs # All re-frame subscriptions
│ ├── views.cljs # App shell, layout, page dispatch
│ ├── routes.cljs # hash router (no external deps)
│ ├── effects.cljs # Custom re-frame effect handlers
│ ├── utils.cljs # Pure utility functions
│ ├── components/
│ │ └── ui.cljs # Reusable Reagent UI component library
│ └── pages/
│ ├── home.cljs # Home page (demo)
│ ├── about.cljs # About page (demo)
│ └── example.cljs # Example page template (demo)
├── docs/
│ ├── index.html # GitHub Pages entry point
│ ├── 404.html # SPA redirect for deep links
│ └── cljs-out/ # Compiled JS (git-ignored, generated)
├── shadow-cljs.edn # shadow-cljs build config
├── deps.edn # Clojure/ClojureScript dependency manifest
├── package.json # Bun scripts + JS devDependencies
├── flake.nix # Nix devShell
├── .envrc # direnv: use flake
├── .clj-kondo/config.edn # Linter config
└── .cljfmt.edn # Formatter config
**Prerequisites**
- Option A — Nix (recommended)
nix develop # or: direnv allow (if you have direnv installed)This drops you into a shell with Java, Clojure CLI, Bun, clj-kondo, and cljfmt pre-installed. No global installs needed.
- Option B — manual
Install the following globally:
- JDK 21+
- Clojure CLI tools
- Bun (
curl -fsSL https://bun.sh/install | bash)
Then install JS dependencies:
bun install
**Dev Commands**
| Command | What it does |
|---|---|
bun run dev | Start shadow-cljs watch + dev HTTP server on http://localhost:8080 |
bun run release | Production build → docs/cljs-out/ (dead-code eliminated, minified) |
bun run clean | Remove docs/cljs-out/ and .shadow-cljs/ cache |
bun run report | Generate a build size report → report.html |
clj-kondo --lint src/ | Lint all ClojureScript sources |
cljfmt check src/ | Check formatting without modifying files |
cljfmt fix src/ | Auto-fix formatting in place |
nix fmt | Format flake.nix with nixpkgs-fmt |
The dev server serves from docs/ and hot-reloads on every save via shadow-cljs.
**Data Flow**
User interaction
│
▼
(rf/dispatch [::events/some-event payload])
│
▼
reg-event-db / reg-event-fx ← reads :db, returns new :db + optional :effects
│
▼
app-db (single immutable map — the only source of truth)
│
▼
reg-sub (layer 2: extract raw slice → layer 3: derive/transform)
│
▼
@(rf/subscribe [::subs/some-value])
│
▼
Reagent component re-renders → DOM update
Effects (HTTP, routing, localStorage, etc.) are handled by registered reg-fx handlers in effects.cljs — they are never called directly from view components.
**File Map**
| File | Kind | Purpose |
|---|---|---|
| core.cljs | framework | Entry point — init, hot-reload, mount |
| db.cljs | framework | App-db schema and default state |
| events.cljs | framework | All re-frame event handlers |
| subs.cljs | framework | All re-frame subscriptions |
| routes.cljs | framework | hash router (no external deps) |
| effects.cljs | framework | Custom re-frame effect handlers |
| utils.cljs | framework | Pure utility functions |
| views.cljs | framework | App shell, layout, and page dispatch |
| components/ui.cljs | framework | Reusable Reagent UI component library |
| pages/home.cljs | demo | Home page — counter + notification demo |
| pages/about.cljs | demo | About page — stack info, file map, data flow |
| pages/example.cljs | demo | Example page template for new features |
**Framework vs. Demo Files**
- Framework files form the reusable base architecture. They contain extension points and should be kept (and modified) when building a new app.
- Demo files are example content that demonstrate how the framework is used. They are safe to delete entirely — removing them will not break the base architecture.
Clojkstra uses hash-based routing (/#/about, /#/example) so the app works on GitHub Pages without any server-side URL rewriting.
Routes are defined as a pure data structure in routes.cljs:
(def app-routes
["/" {"" :home
"about" :about
"example" :example
true :not-found}])To add a new route:
- Add an entry to
app-routesinroutes.cljs - Create
src/clojkstra/app/pages/my_page.cljswith a(defn page [] ...)component - Require it in
views.cljsand add acasebranch inpage-for-route - Add a
{ :handler :my-page :label "My Page" }entry tonav-linksinviews.cljs
Navigate programmatically from anywhere:
(routes/navigate! :about)
;; or from an event handler via the :navigate effect:
{:navigate {:handler :about}}Feature flags live in db.cljs under :config :features:
:features
{:example-feature true
:debug-panel true}Check a flag in a view via subscription:
@(rf/subscribe [::subs/feature-enabled? :my-flag])Toggle a flag at runtime from an event:
(rf/dispatch [::events/toggle-feature :my-flag])Set a flag programmatically:
(rf/dispatch [::events/set-config [:features :my-flag] false])The app is built directly into docs/ so GitHub Pages can serve it from that directory.
**One-time setup:**
- The repo is already at https://github.com/ArikRahman/Clojkstra
- Go to Settings → Pages on that repo
- Set Source to
Deploy from a branch, branchmain, folder/docs - Click Save
**Deploy a new release:**
bun run release
git add docs/
git commit -m "release: <version>"
git pushGitHub Pages will serve the updated build within a minute.
The docs/404.html handles the rare case where someone hits a non-root URL directly — it redirects them to index.html so the SPA router can take over.
This is the core workflow the template is designed for.
**Step 1 — Clone and rename**
git clone https://github.com/ArikRahman/Clojkstra my-new-app
cd my-new-app**Step 2 — Replace the namespace root**
Rename src/clojkstra/ to src/my_app/ (underscores in paths, hyphens in ns names):
mv src/clojkstra src/my_appDo a project-wide find-and-replace:
clojkstra.app→my-app.appclojkstra/app→my_app/app
Most editors support this with a single multi-file search-and-replace.
**Step 3 — Update config**
In db.cljs, set your app’s name and version:
:config
{:app-name "My New App"
:version "0.1.0"
...}In shadow-cljs.edn, update :init-fn:
:init-fn my-app.app.core/init
:after-load my-app.app.core/on-reloadIn package.json, update the "name" field.
**Step 4 — Delete demo files**
Remove the three demo pages:
rm src/my_app/app/pages/home.cljs
rm src/my_app/app/pages/about.cljs
rm src/my_app/app/pages/example.cljsIn views.cljs, remove the requires for those pages and add your own.
In events.cljs and subs.cljs, delete all sections marked [DEMO].
In db.cljs, remove the :counter and :notifications keys.
**Step 5 — Seed your domain state**
Add your app’s initial data structure to db.cljs:
(def default-db
{:current-route {:handler :home :route-params {}}
:loading? false
:error nil
:config { ... }
;; Your domain state:
:current-user nil
:my-feature {:items [] :loading? false}})**Step 6 — Add your first real page**
Copy pages/example.cljs as a starting scaffold:
cp src/my_app/app/pages/example.cljs src/my_app/app/pages/my_feature.cljsUpdate the namespace, follow the checklist at the top of the file, and add the route.
**Step 7 — Wire API clients and auth (optional)**
Add custom effect handlers in effects.cljs for HTTP, WebSocket, or analytics.
Add :auth and :api keys to db.cljs.
Gate in-progress work behind feature flags.
Each file has a clearly marked Extension point comment block.
Here is a quick reference:
| Where | What to add |
|---|---|
| db.cljs | New top-level keys for domain state; feature flags; theme tokens |
| events.cljs | Domain event handlers; reg-event-fx for side-effectful events |
| subs.cljs | Domain subscriptions; derived/computed layer-3 subs |
| routes.cljs | New route entries in app-routes |
| effects.cljs | HTTP, WebSocket, analytics, native API effect handlers |
| utils.cljs | Pure helper functions with no re-frame dependencies |
| components/ui.cljs | New reusable Reagent components; or split into sub-namespaces |
| views.cljs | New page requires + case in page-for-route; new nav links |
| pages/ | One file per top-level page/feature |
**Lint:**
clj-kondo --lint src/Configuration is in .clj-kondo/config.edn. re-frame registration macros are taught to clj-kondo so it understands reg-event-db / reg-sub / reg-fx without false positives.
**Format (check only):**
cljfmt check src/**Format (auto-fix):**
cljfmt fix src/Configuration is in .cljfmt.edn. Namespace :require blocks are intentionally not auto-sorted — manual grouping with framework/local comments is preferred.
flake.nix provides a fully reproducible development environment:
nix developOr with direnv:
echo "use flake" > .envrc
direnv allowThe shell includes: jdk21, clojure, bun, git, curl, jq, clj-kondo, cljfmt.
On entry, bun install runs automatically if node_modules/ is missing.
Format flake.nix itself with:
nix fmtMIT — do whatever you want with it. The whole point is that you clone it and make it yours.