A clean-canvas Polylith Grain app with a UIx + Re-frame frontend. It carries the current Silo seed's event-model, Allium, SQLite, LMDB, code-agent-tools, and verification foundation without its Datastar rendering tier.
The boundary between starter guarantees and cloned-application responsibilities — what initialization changes, what a clone must supply, and how to publish a tagged release — is documented in docs/STARTER-CONTRACT.md.
Run the initializer once from the root of a newly cloned starter:
bb scripts/init_project.bb \
--slug my-app \
--name "My App" \
--port 8080 \
--time-zone America/ChicagoIt generates a development tenant UUID and application-specific cookie name, then updates runtime and
frontend identity, HTML metadata, package identity, local defaults, and README provenance. Optional
--cookie-name, --base-url, --tenant-id, --locale, and --time-zone values override the generated
defaults. It fails if stale starter identity remains in executable configuration.
- Grain CQRS backend: commands → events → read models → queries.
- SQLite event store and LMDB projection cache with memory-first infrastructure adapters.
- Provider-neutral email with logger/test capture, persistent Mailpit SMTP, and AWS SES adapters.
- Provider-neutral files and presigned URLs with memory/test, S3, and LocalStack adapters.
- Authenticated AES-GCM encryption with local-key and AWS KMS envelope-encryption adapters.
- A webhook module for raw-body preservation, HMAC verification, event-ID idempotency, audit receipts, and failed-delivery replay.
- Account foundation: sign-up, required email verification with safe resend, login/logout, password reset, HTTP-only sessions, and session revocation on logout or password change.
- Protected, anonymous-only, and public route policies with safe post-login return paths and explicit 403, 404, session-loading, and application-error outcomes.
- UIx + Re-frame + Reitit frontend styled with Tailwind CSS and shadcn Base UI.
- One wrapped API module (
app.api.interface) and one wrapped auth module (app.auth.interface). - Accessible form fields, structured server-error summaries, and consistent busy/failure/success feedback.
- Keyed request lifecycle state with independent pending, retry, cancellation, and stale-response handling.
- A system/fixed clock interface plus explicit runtime locale and time-zone settings, without domain date rules.
- One validated backend configuration module (
app.config.interface) with secure production checks. - Recursive credential redaction before logging so requests, cookies, headers, passwords, session/JWT secrets, verification/reset tokens, and token-bearing email HTML are not published by the development console adapter.
- Request correlation IDs, in-process transport metrics, diagnostic health, and selectable pretty-console, JSON-console, or file μ/log destinations.
- A real shadcn TypeScript workspace whose compiled React interface is consumable from UIx.
- Allium +
defeventmodelcomposition checks, Polylith tests, Clojure lint, frontend tests, and a production frontend build in one gate. - Playwright browser contracts that exercise real auth, routing, and the shadcn bridge against both development-compiled and release-compiled frontend assets.
Have these on PATH before the first bun run dev or ./scripts/verify-specs.sh:
- Java 21
- Clojure CLI — the project pins Clojure 1.12 in
deps.edn - Bun 1.3.14 — the repository pins the package manager in
.bun-versionandpackage.json - Node.js 20+ — retained only as the compatibility runtime for third-party CLIs such as Vite and Playwright
- Babashka (
bb) — runs the Grain slice and page generator - clj-kondo — the lint stage of the gate
- ripgrep (
rg) — the frontend seam-discipline checks in the gate - Allium CLI (3.5) — the specification stage of the gate
Docker with Compose is optional but recommended. The managed development command uses it for the pinned Mailpit and LocalStack containers. Without Docker, the application still starts with logger email, memory files, and local encryption.
Playwright's Chromium bundle is also required for the browser contract. Install the pinned bundle after
bun install --frozen-lockfile with bunx --no-install playwright install chromium; clean Linux CI uses
--with-deps as well.
On macOS with Homebrew OpenJDK, Java 21 is at /opt/homebrew/opt/openjdk@21/bin. If
java -version does not report 21, put it on PATH for the session:
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"./scripts/dev up
./scripts/dev statusup installs the locked JavaScript dependencies when needed, starts Mailpit and LocalStack when Docker is
available, and claims
https://reframe-template.localhost through Portless when it is installed, builds the
shadcn bridge, starts Vite, Tailwind, Shadow CLJS, and the Clojure backend, then waits for the health check.
It reports the Mailpit inbox as https://reframe-template-mail.localhost with Portless or
http://localhost:8025 directly. Set APP_DEV_INFRA=false to use the in-process adapters instead.
Without Portless it falls back to http://localhost:8080. The lifecycle is designed for developers and
coding agents alike:
./scripts/dev logs --follow
./scripts/dev restart
./scripts/dev downThe backend also starts a loopback-only nREPL in the same JVM, writes its actual port to .nrepl-port,
and reports it from ./scripts/dev status. The default is 127.0.0.1:7888; set APP_NREPL_PORT=0 to
select a free port automatically. Because grain-code-agent-tools is installed during backend boot, an
agent connected through that nREPL can inspect and exercise the exact live event store, registries,
events, projections, commands, and queries serving the browser.
Mailpit stores captured messages in SQLite on its Docker volume, so restart and down preserve the
local inbox. The default keeps the newest 500 messages; override that with APP_MAILPIT_MAX_MESSAGES.
To deliberately empty only the Mailpit inbox and restart that service, use the explicit destructive
command:
./scripts/dev reset-mail --forceUse bun run dev, ./scripts/dev foreground, or the compatibility adapters ./scripts/dev.sh and
./portless.sh when a foreground process is preferable. Those paths use the same backend entry point and
therefore include nREPL. A standalone editor-oriented nREPL with CIDER/refactor middleware remains
available when the full managed stack is not running:
./scripts/nrepl.sh # nREPL, default 7888The backend has safe local defaults. To customize it, copy .env.example to .env, edit it, and export
the values into the shell. scripts/dev loads .env automatically; for the nREPL workflow, export it
before starting the REPL:
set -a
. ./.env
set +aThen start the backend from the REPL:
(require 'app.web-api.core)
(app.web-api.core/start!)The backend serves the compiled frontend and Grain's /command and
/query endpoints from the same origin, so session cookies do not need a development CORS workaround.
Application modules use small provider-neutral interfaces; provider clients and request shapes remain inside their adapters. The managed development command selects SMTP, S3, and KMS when its containers are available. Direct REPL/test starts default to logger email, memory files, and local AES-GCM.
| Capability | Local/test | Production | Configuration |
|---|---|---|---|
| logger, test file capture, or persistent Mailpit SMTP | AWS SES | APP_EMAIL_PROVIDER |
|
| Files | memory | AWS S3 | APP_FILE_STORE_PROVIDER |
| Download/upload URLs | deterministic stub | AWS S3 presigning | follows file-store provider |
| Protected values | local AES-256-GCM | AWS KMS envelope encryption | APP_CRYPTO_PROVIDER |
| Logs | pretty console | JSON console or file | APP_LOG_DESTINATION |
The backend exposes /healthcheck for liveness, /health for safe dependency diagnostics, and /metrics
for process-local request counters and latency summaries. See
docs/PROVIDER-ADAPTERS.md before adding Stripe, Twilio, or another vendor.
bases/web-api/ HTTP entry point, Grain routes, SPA fallback
components/ backend Polylith components
email* logger, local SMTP, and SES adapters
file-store* memory and S3 object storage
url-presigner* deterministic and AWS presigned URLs
crypto* local AES-GCM and KMS envelope encryption
webhooks signature, idempotency, receipt, and replay machinery
observability request metrics, health, correlation, and log adapters
ui/web-app/src/app/
api/ Grain HTTP client seam + remote/stub adapters
auth/ session/account Re-frame module
clock/ system/fixed clocks + configured date presentation
customer/ disposable end-to-end customer tracer bullet
notification/ Re-frame effect over the shadcn toast manager
query_resource/ keyed Grain query cache/freshness/retry module
request/ keyed pending/success/failure/retry/cancellation state
questionnaire/ example Re-frame state module
pages/ UIx pages
re_frame/ local UIx subscription adapter
router/ Reitit/Pushy runtime and outlet
ui/interface.cljs reusable UI primitives + navigation/title/action/content shell slots
ui/shadcn/
components.json shadcn CLI configuration (Base UI + Nova)
src/components/ui/ open-code shadcn components owned by this repo
src/index.tsx small React interface exported to UIx
Add another shadcn component with:
bun run shadcn:add -- dialogThe command uses the lockfile-installed CLI, so a clone does not silently select a different CLI release.
Review the generated source, then export the component—or a small feature-specific bridge—from
ui/shadcn/src/index.tsx. UIx can import those exports from @grain/shadcn. Stateful shadcn widgets
own only ephemeral interaction state; they emit plain values into Re-frame, as the starter questionnaire
demonstrates at /examples/questionnaire.
The protected /examples/customer-workbench route is the complete disposable tracer bullet: authenticated
Grain commands emit customer events, the read model projects them, keyed queries refresh Re-frame state,
and Table, Badge, Combobox, Dropdown Menu, Sheet, Tabs, and Toast components render the lifecycle. Its
record selection, tab, status filter, and sort live in query parameters. Replace its customer terminology
after a clone has equivalent coverage for those seams.
Compose cloned-app navigation and per-page actions through the app.ui.interface/app-shell slots. Keep
session controls and the frame inside that module; application-specific links, filters, and actions belong
in the clone.
Create a complete example slice with:
bb scripts/new_grain.bb service inventoryThe generator prints the backend and frontend wiring still needed. Verify the whole starter with:
./scripts/verify-specs.shExercise real browser behavior against both frontend build modes with:
bun run test:browserTagging a starter release — fresh-clone acceptance, release_starter.sh, the CI/tag rules, and the work
still open before the first tag — is documented under
Releasing the starter.
Reset the configured development store with an explicit confirmation:
bb dev resetAutomation may pass --yes; the command still refuses production environments, repository/home roots,
symlinks, and paths outside storage*, .dev-data/, or the system temporary directory. Use a distinct
path such as .dev-data/app-a for parallel local instances:
APP_STORAGE_DIR=.dev-data/app-a bb dev seed
APP_STORAGE_DIR=.dev-data/app-a bb dev reset --yesThe starter's seed command only creates and marks the safe storage target. A cloned application may add a
committed scripts/app_seed.bb adapter for its own fixtures; application records and other domain seed
data do not belong in the starter.
Set APP_COOKIE_SECURE=true in HTTPS deployments. Override the frontend API origin at compile time
only when the UI genuinely deploys separately; same-origin is the default and preferred topology.
Production startup also requires a non-placeholder APP_JWT_SECRET, a non-development APP_CRYPTO_KEY
when local crypto is selected, AWS SES and S3 configuration, an HTTPS APP_BASE_URL, and secure cookies.
Local SMTP, memory file storage, and AWS endpoint overrides are rejected in production. Invalid settings
fail together at boot with actionable messages.
Set APP_LOCALE to a BCP 47 language tag and APP_TIME_ZONE to an IANA zone (or UTC). The backend
validates both and publishes them to the same-origin browser document for app.clock.interface formatting.
APP_TENANT_ID selects the one tenant served by the default deployment; it is an operating default, not a
single-tenant domain decision. Enabling a second tenant is a cloned-application responsibility — see
docs/STARTER-CONTRACT.md.