| name | c3kit |
| description | Use this skill when working in projects that depend on c3kit libraries (apron, bucket, wire, scaffold). Covers the schema system, database API, web layer, app lifecycle, and conventions specific to the c3kit ecosystem. |
c3kit (Clean Coders Clojure Kit)
When This Skill Applies
Use this skill whenever you work in a project that depends on any c3kit library. Check deps.edn or project.clj for com.cleancoders.c3kit/apron, c3kit/bucket, c3kit/wire, or c3kit/scaffold.
Library Overview
| Library | Purpose | Key Namespaces |
|---|
| apron | Foundation — schemas, time, logging, app lifecycle, utilities | schema, legend, corec, time, log, app, env |
| bucket | Database abstraction — uniform API over multiple backends | api, memory, datomic, jdbc, seed, bg |
| wire | Web layer — AJAX, WebSocket, REST, JWT, assets, flash, Redis | apic, ajax, websocket, flash, jwt, routes |
| scaffold | Build tooling — CLJS compilation, CSS compilation | cljs, css |
Apron: Foundation
Schema System
Schemas are plain maps. Each field is a spec with :type, optional :validate, :coerce, :present, and :message keys. Four operations: coerce, validate, conform (coerce + validate), and present. Each has a bang variant that throws on failure; non-bang returns the entity with error objects embedded in failed fields.
When working with c3kit.apron.schema, load the c3kit-schema skill for full API coverage and critical guidance on avoiding silent failures.
Legend
A legend is a registry of schemas indexed by :kind. Build it once, then use kind-dispatched operations.
(def legend (legend/build [user order product]))
;; Dispatch by :kind
(legend/coerce! legend {:kind :user :name 123})
(legend/conform! legend {:kind :order :total "42.50"})
Corec (ccc)
Always alias as ccc. Provides cross-platform utilities designed for :refer :all in some contexts but typically aliased.
Key functions:
- Collections:
conjv, dissocv, assocv, removev — vector operations
- Filtering:
ffilter, find-by, ffind-by — first-match filtering
- Logic:
nand, nor, xor
- Options:
->options — normalizes variadic key-value or map args
- Identity:
new-uuid — cross-platform UUID generation
- No-op:
noop — useful for stubbing println in tests
;; ffilter — like (first (filter ...))
(ccc/ffilter #(= :admin (:role %)) users)
;; ->options — normalize args
(defn configure! [& options]
(swap! config merge (ccc/->options options)))
Time
Starting point for dealing with time.
Creation:
(time/now)
(time/utc 2026 3 31)
(time/local 2026 3 31 14 13)
Human-readable duration constructors. Designed for threading:
(-> 5 time/minutes time/ago) ;; 5 minutes ago
(-> 1 time/days time/from-now) ;; tomorrow
(time/before (time/now) (-> 30 time/seconds)) ;; 30 seconds before now
Parsing/unparsing with named formats:
(time/parse :dense "20240115")
(time/unparse :http-date some-date)
App Lifecycle
The app namespace manages application state and service lifecycle.
;; Define services as start/stop symbol pairs
(def my-service (app/service 'myapp.db/start 'myapp.db/stop))
;; Start services
(app/start! [my-service other-service])
;; Access app state
(app/resolution! :db) ;; throws if missing
(app/resolution :db) ;; returns nil if missing
Services are started/stopped via symbols resolved at runtime (requiring-resolve), enabling decoupled initialization.
Environment
Priority chain for environment values: overrides -> system properties -> env vars -> .env file.
(env/env "DATABASE_URL") ;; resolve from chain
(env/override! "PORT" "3000") ;; set override for testing
Logging
Always alias as log. Wraps Timbre.
(log/info "Starting server on port" port)
(log/warn "Deprecated: use foo instead")
(log/error e "Request failed")
;; Capture logs in tests
(log/capture-logs
(do-something)
(should-contain "Starting" (log/captured-logs-str)))
Bucket: Database
The api Namespace
All database operations go through c3kit.bucket.api. It provides a uniform API across Datomic, JDBC (Postgres, SQLite3, MSSQL, H2), in-memory, and IndexedDB backends.
Global Impl
A global atom holds the current DB implementation. Most functions use it implicitly. Functions suffixed with - take an explicit db parameter.
;; Implicit (uses global impl)
(api/entity :user 123)
(api/find :user :name "alice")
(api/tx {:kind :user :name "alice"})
;; Explicit (takes db as first arg)
(api/entity- my-db :user 123)
(api/find- my-db :user :name "alice")
Core Operations
;; Create/update — tx handles both
(api/tx {:kind :user :name "Alice" :email "alice@example.com"})
;; Read
(api/entity :user 123) ;; by id
(api/find :user :name "Alice") ;; returns seq
(api/ffind :user :name "Alice") ;; returns first match
;; Delete
(api/delete {:kind :user :id 123})
;; Count
(api/count :user :role "admin")
;; Reduce (streaming, doesn't load all into memory)
(api/reduce :user conj [] :active true)
Find Options
The find API accepts data-driven query options:
;; Filtering
(api/find :user :role "admin" :active true)
;; With options map
(api/find :user {:where {:role "admin"}
:order-by [:name :asc]
:take 10
:drop 20})
;; Operators in filters
(api/find :order :total ['> 100])
(api/find :user :name ['like "Ali%"])
(api/find :product :category ['not= "archived"])
Safety Guard
Destructive operations require safety to be off:
;; This will throw in production
(api/clear)
;; Disable safety for tests/migrations
(api/with-safety-off
(api/clear))
Soft Delete
(api/soft-delete entity) ;; returns {:kind k :id id :db/delete? true}
(api/delete? entity) ;; checks if entity is soft-deleted
Compare-and-Swap (CAS)
Optimistic locking via metadata:
(api/tx (api/cas {:version 3} updated-entity))
Seed Entities
Deref-able entities that auto-create on first access:
(def admin-user (seed/entity :user {:name "Admin" :role "admin"}))
;; First deref creates, subsequent derefs return cached
@admin-user ;; => {:kind :user :id 1 :name "Admin" :role "admin"}
Testing with Bucket
Use spec-helperc/with-schemas to set up a fresh in-memory DB per test context:
(describe "User Service"
(helperc/with-schemas [user order])
(it "creates a user"
(let [result (api/tx {:kind :user :name "Alice"})]
(should= "Alice" (:name result)))))
This sets up an in-memory DB, turns safety off, and clears between tests.
Background Tasks
Schedule recurring tasks with bg:
(bg/start! {:task 'myapp.jobs/cleanup
:schedule "0 0 * * *"
:name "nightly-cleanup"})
Supported Backends
| Backend | Config :impl | Platform |
|---|
| In-memory | :memory | clj, cljs |
| Datomic On-Prem | :datomic | clj |
| Datomic Cloud | :datomic-cloud | clj |
| PostgreSQL | :jdbc + :dialect :postgres | clj |
| SQLite3 | :jdbc + :dialect :sqlite3 | clj |
| H2 | :jdbc + :dialect :h2 | clj |
| MSSQL | :jdbc + :dialect :mssql | clj |
| Reagent Memory | via re-memory | cljs |
| IndexedDB | via indexeddb | cljs |
Wire: Web Layer
API Responses (apic)
All communication channels (AJAX, WebSocket, REST) share the same response shape:
(apic/ok) ;; {:status :ok}
(apic/ok payload) ;; {:status :ok :payload payload}
(apic/ok payload "Success!") ;; with flash message
(apic/fail "Validation failed") ;; {:status :fail :flash [...]}
(apic/error "Server error") ;; {:status :error :flash [...]}
(apic/redirect "/login") ;; {:status :redirect :uri "/login"}
Check responses: apic/ok?, apic/fail?, apic/error?
AJAX
Server-side — middleware stack for Transit serialization:
(-> handler
ajax/wrap-ajax)
Client-side — request functions:
(ajax/post! "/api/users" {:name "Alice"}
:on-ok #(handle-success %)
:on-fail #(handle-failure %))
(ajax/get! "/api/users"
:on-ok #(reset! users (:payload %)))
WebSocket
Server-side:
;; Configure handlers
(websocket/configure!
:handler-key 'myapp.ws/handle-message)
;; In handlers, return apic responses
(defn handle-message [request]
(apic/ok {:result "processed"}))
Client-side:
(websocket/call! :my/action {:data "value"}
:on-ok #(process-response %))
REST Client
Sync and async HTTP methods:
(rest/get! "https://api.example.com/users")
(rest/post! "https://api.example.com/users" {:name "Alice"})
(rest/put! "https://api.example.com/users/1" {:name "Bob"})
(rest/delete! "https://api.example.com/users/1")
REST response constructors in restc:
(restc/ok body)
(restc/created body)
(restc/bad-request body)
(restc/not-found)
(restc/unauthorized)
Flash Messages
Server-side — attach to Ring responses. Client-side — Reagent atom with auto-timeout:
;; Create flash messages
(flashc/success "Saved!")
(flashc/warn "Are you sure?")
(flashc/error "Something went wrong")
;; Client: add to UI state
(flash/add! (flashc/success "Done!"))
JWT Authentication
;; Sign
(jwt/sign {:user-id 123} secret)
;; Middleware
(-> handler
(jwt/wrap-jwt {:secret secret}))
Asset Fingerprinting
MD5-based cache-busting for static assets:
;; Middleware strips fingerprints from incoming requests
(-> handler
assets/wrap-asset-fingerprint)
;; In templates, add fingerprints
(assets/add-fingerprint "/css/app.css")
;; => "/css/app-a1b2c3d4.css"
Routes
;; Lazy routes for development (reloads on each request)
(routes/lazy-routes 'myapp.routes/app-routes)
;; Redirect routes
(routes/redirect-routes
{"/" "/dashboard"
"/old-page" "/new-page"})
Lock and MessageQueue
Pluggable infrastructure with in-memory (testing) and Redis (production) backends:
;; Distributed locking
(lock/with-lock "my-resource"
(do-exclusive-work))
;; Message queue
(mq/enqueue :my-topic {:data "value"})
(mq/on-message :my-topic
(fn [msg] (process msg)))
Testing with Wire
Wire provides rich test helpers in spec_helper and spec_helperc:
;; Assert AJAX responses
(should-be-ajax-ok response)
(should-be-ajax-fail response "Expected message")
(should-redirect-to response "/login")
;; Mock AJAX in CLJS tests
(should-have-invoked-ajax-post "/api/users")
;; Mock WebSocket
(should-have-invoked-ws :my/action)
Scaffold: Build Tooling
ClojureScript Compilation
Configure in config/cljs.edn:
{:development {:output-dir "resources/public/cljs/dev"
:output-to "resources/public/cljs/dev/main.js"
:main myapp.main}
:production {:output-dir "resources/public/cljs/prod"
:output-to "resources/public/cljs/prod/main.js"
:main myapp.main
:optimizations :advanced}}
Run: clj -M:cljs once development or clj -M:cljs auto development
CSS from Garden
Configure in config/css.edn:
{:styles-var myapp.styles/styles
:output-to "resources/public/css/app.css"}
Run: clj -M:css once or clj -M:css auto
Common Mistakes
- Using
api/find without knowing the return type — find returns a seq, ffind returns a single entity, entity looks up by id
- Forgetting safety guard —
api/clear and api/delete-all throw unless wrapped in api/with-safety-off
- Skipping
conform! — always conform entities through their schema before persisting
- Returning values from
defthen without asserting — produces 0 assertions (see the gherclj repo for step-definition guidance)
- Using
clojure.test assertions — the ecosystem uses speclj (should=, should-throw, etc.)
- Wrong alias for
corec — it's ccc, not corec or core
- Calling protocol methods directly — use the public API functions (
api/entity, not (-entity impl ...)), protocol methods are prefixed with - to signal they're internal
- Ignoring the
- suffix convention — api/find- takes an explicit db; api/find uses the global impl