CLAUDE.md - Clojure/ClojureScript SaaS Project Guidelines
Build Commands
npm run dev- Start development environment (Shadow-CLJS + Tailwind)bb dev- Start both frontend and backend servers for developmentbb release- Full release build for productionbb build-icons- Generate SVG icon sprite
Lint & Format Commands
npm run standard-clj- Lint Clojure/ClojureScript filesnpm run standard-clj -- fix path/to/file.clj- Auto-fix formatting issuesclj-kondo --lint src/- Run clj-kondo linter on source directoryPrettier is used for JS/CSS formatting
Code is auto-formatted with standard-clj on pre-commit via Lefthook
Test Commands
ALWAYS run
clj-kondo --report-level error --lint your.namespaceafter editing a namespace and immediately fix any issue.Tests located in
/test/clj/saas/and/test/cljs/Run Clojure tests:
clj -X:test-cljRun ClojureScript tests:
clj -M:test-cljsRun single test:
clj -X:test :only namespace/test-name
Coding Style Guidelines
Naming: Use kebab-case for namespaces, functions, variables (Clojure standard)
Organization:
/src/clj(server),/src/cljc(shared),/src/cljs(client)State Management: Re-frame for frontend state, documented in ADRs
UI: UIx for React components, Tailwind CSS with DaisyUI for styling
Error Handling: Use middleware with standard exception types
:system.exception/internal(500),:system.exception/business(400), etc.
Architecture
Full-stack Clojure/ClojureScript app with shadow-cljs, Tailwind + DaisyUI
Single-page application architecture with server-rendered landing page
Authentication uses access token (15m, in-memory) + refresh token (90d) pattern
See
docs/authentication.mdfor detailed token rotation approachUI component documentation in
/portfolio/directory with interactive stories
WebSocket Communication
Uses Sente library for WebSocket connections (
taoensso.sente)Client-side connection setup in
/src/cljs/saas/ws.cljsServer handlers defined in
/src/clj/saas/web/routes/websocket.cljAuthentication via JWT tokens:
Pass tokens in
:paramsmap with Sente connection (not in headers)Server extracts user ID from JWT claims via
req->user-idfunction
Message format:
[:event-id payload](e.g.,[:ai-chat/message {:message "Hello"}])Handler multimethod pattern:
(defmethod ws/handle-message :event-id [{:keys [?data]}] ;; Handle message here )
UI Components
DaisyUI components wrapped in UIx React components at
/src/cljc/saas/common/ui/core.cljcComponent examples in
/portfolio/src/saas/components/directoryKey component properties:
Most components take standard HTML attributes like
:class,:styleSpecial props like
:coloraccept keywords (:primary,:error, etc.)Size modifiers (
:xs,:sm,:md,:lg) for many componentsBoolean flags like
bordered?follow Clojure's convention with?suffix
Page structure pattern:
Define a main page component with
defuiInitialize state with dispatched events in component body
Docs for DaisyUI here
Docs for UIX here
When writing tailwind classes as keywords, when you have more comples classes, such as modifiers, result to using string classes as oposed to keywords. The same if you need to use custom values. Use keyword based class names only for simple classes.
($ :div.md:bg-red-200.h-[100px]) ;; bad ($ :div {:class "md:bg-red-200 h-[100px]"}) ;; good ($ :div#cool-id) ;; bad ($ :div {:id "cool-id"}) ;; good
Portfolio Component Documentation
Always use the
defscenemacro to create component examplesRequired namespace import:
[portfolio.react-18 :as portfolio :refer-macros [defscene]]Include
portfolio/configure-sceneswith a title at the top of each scenes fileBasic example:
(ns saas.components.example-scenes (:require [portfolio.react-18 :as portfolio :refer-macros [defscene]] [saas.common.ui.core :as ui] [saas.common.ui.utils :refer [cn]] [uix.core :refer [$ defui]])) (portfolio/configure-scenes {:title "Example Scenes"}) (defui scene-container [{:keys [children class]}] ($ :div {:class (cn "flex flex-col gap-3" class)} children)) (defscene basic-example :title "Basic Example" ($ ui/button "Click me"))Add component scenes file to
portfolio.cljsto make it availableUse
scene-containercomponent for consistent layout across scenes
Re-frame Conventions
Always define event/subscription parameters as a map, not positional arguments:
;; CORRECT: Use map destructuring for parameters (rf/reg-sub :form/field-state (fn [db [_ {:keys [form-id field-id]}]] ;; Access parameters by name (get-in db [:forms form-id :values field-id]))) ;; INCORRECT: Don't use positional arguments (rf/reg-sub :form/field-state (fn [db [_ form-id field-id]] ;; Positional parameters are harder to maintain (get-in db [:forms form-id :values field-id])))This improves maintainability, readability, and allows adding parameters without breaking existing code
Re-frame with UIx
For dispatching events and other re-frame operations, continue using
re-frame.core:(require [re-frame.core :as rf]) (rf/dispatch [::action-id payload]) ;; Still use rf/dispatch (rf/reg-event-db ::event-id handler-fn) ;; Still use rf for registrationFor subscribing to re-frame state in UIx components, use
use-subscribe:;; INCORRECT in UIx components - won't trigger re-renders properly (let [data @(rf/subscribe [::sub-id])] ...) ;; CORRECT - properly reactive with UIx (require [uix.re-frame :refer [use-subscribe]]) (let [data (use-subscribe [::sub-id])] ...)Only subscriptions need the special
use-subscribehook in UIx componentsThis ensures proper React integration for reactive updates when state changes
Routes and API Structure
Different route files are separated by middleware requirements, not by functionality:
routes/api.clj- For most API routes with standard middlewareroutes/webhooks.clj- For webhook endpoints that need to handle raw payloadsroutes/pages.clj- For server-rendered pages
Always add new API endpoints to
routes/api.cljunless there's a specific middleware conflictGroup related endpoints under a common parent path (e.g.,
/api/payments/...)Webhook endpoints are used for external service callbacks (e.g., Stripe)
Reitit/Malli Integration
Upgraded to Reitit 0.7.2 requires updated Malli schema formats
Schema format changes:
Replace function predicates (like
string?) with keywords (:string)Use vector schemas (like
[:string]) instead of bare predicatesPath parameters should use
[:map [:param-name :type]]formatNever use
nilas a schema value, use empty map[:map]instead
Main routes defined in
/src/cljc/saas/common/routes.cljcRoute names in
/src/cljc/saas/routes.cljcDefine request/response schemas in
common/schema.cljcto be shared across frontend and backend
Last updated