# Cicadel 0.2.7: complete application documentation Build applications using Cicadel. Start with the quick start or AI-agent guide; fetch focused Markdown guides as needed. Source: 2678611d308376ca8045c8741ee9d4d7814a3fec. --- Source: `CHANGELOG.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/releases/changelog/index.md # Changelog ## Unreleased Nothing yet. ## v0.2.7 — 2026-11-13 Production request handling for browser applications: HTML error pages, login redirects, per-route body limits and htmx response helpers. No breaking changes. Upgrade all five modules to v0.2.7 together. - Added `errorpages` (DX-04): `errorpages.Middleware` replaces plain-text 4xx/5xx responses (the router's 404/405, `http.Error`, CSRF failures) with a rendered HTML page for clients that accept HTML, and recovers panics as a 500 page, logging the stack with the request ID and calling an optional `Report` hook. Pages show fixed copy, never the handler's message. htmx partial requests get `HX-Reswap: none` and a `cicadel:error` event instead of a page. Generated projects install it through `project/errors.go` (`GlobalMiddleware`, `ErrorPage`) with a styled `project/errors.templ`. See `docs/error-pages.md`. - Added `bodylimit` (DX-29): `settings.MaxBodyBytes` is now a per-request default that `bodylimit.Limit(n)` or `bodylimit.Set` raise or lower before the body is read. `forms/upload` uses `Config.MaxBytes` as the request limit, so upload routes accept large files while other routes keep the small default. See `docs/request-limits.md`. - `contrib/accounts` (DX-02, DX-03): `Browser.LoginRequired(loginURL)` redirects anonymous visitors to sign in with a safe `?next=` (unsafe methods without it, htmx via `HX-Redirect`), and the sign-in page returns there. `Browser.Middleware` no longer caps every wrapped body at 8 KiB; the account pages keep their own 8 KiB bound. The anonymous account and change-password pages also redirect with `next`. - Added htmx response helpers (DX-08): `htmx.Trigger`, `TriggerDetail`, `Redirect`, `Location`, `Refresh`, `Retarget`, `Reswap`, `Reselect`, `PushURL`, `ReplaceURL` and `CSRFHeaders`. Navigation helpers accept only local paths. - `layout.Props` gains `Lang` and `Dir` (DX-10; ``, validated) and `CSRFToken`, which sends the token as `X-CSRF-Token` on every htmx request. - Added `response.LocalPath`. `response.Redirect` now also rejects targets with whitespace or control characters, such as `/\t/evil.test`, which browsers turn into the protocol-relative `//evil.test`. - Added `security.CSRFFailureMessage`. - `startproject` and `startapp` emit gofmt-formatted Go (DX-25). ## v0.2.6 — 2026-11-12 The accounts app emits typed, transactional events. No breaking changes. Upgrade all five modules to v0.2.6 together. - `contrib/accounts` declares and emits typed events: `UserCreated`, `LoggedIn`, `LoginFailed`, `LoggedOut` and `PasswordChanged` (listed by `accounts.Events()` and included in `Descriptor`/`Service.App`). Payloads carrying `Tx` are emitted inside the account transaction, so handler writes commit with the change, a handler error rolls it back, and `database.OnCommit(e.Tx, fn)` defers side effects. `Service.Create`, `Login`, `Logout`, `ChangePassword` and `ResetPassword` now run through `database.Atomic`; `Create` returns a zero `User` when it fails. Accounts now requires core v0.2.5. See `docs/events.md`. ## v0.2.5 — 2026-11-12 Adds typed application events, the event-hook contract for installable packages. No breaking changes. Upgrade all five modules to v0.2.5 together. - Added typed application events (SVC-08), the package-system event-hook contract. The new `events` package declares an event with `events.New[T](owner, name)`; an app lists the events it emits in `App.Events` and its handlers, created with `Event.Handle`, in `App.Subscriptions`. `cicadel.New` builds an immutable dispatcher and fails when an app declares another app's event, a declaration is duplicated or its name reused, or a subscription names an event no installed app declares. `Event.Emit` runs handlers synchronously in app dependency order, stops at the first error (`*events.HandlerError`), checks cancellation before each handler and bounds nested emission (`events.MaxDepth`, `events.ErrDepth`). Requests, `Start` and cleanup hooks, app commands and every `manage.go` command carry the dispatcher; the new `Application.Context(ctx)` attaches it, the signing keys and the static manifest for other work such as jobs, and `Application.Events()` returns the dispatcher. `manage.go apps` lists `emits` and `handles` lines. See `docs/events.md`. - First-party app manifests (`accounts`, `formsets`, `migrations`, `querygen`) report the module version selected by the build, read from Go build information, instead of a fixed `v0.1.0`. `manage.go apps` now shows the real version; checkout builds without a released version report `v0.0.0`. - Generated projects subscribe in the new `project/events.go`, and the `startpackage` template declares and emits a typed `Greeted` event with a subscriber test. ## v0.2.4 — 2026-11-11 Adds typed per-package settings and completes the readiness checklist. No breaking changes. Upgrade all five modules to v0.2.4 together. - Formset saves (`contrib/formsets`) now run through `database.Atomic`, so model-form hooks registered with `database.OnCommit` during a formset `Save` run after the formset commits instead of failing with `database.ErrNoTransaction`. - Added the typed per-package settings contract (DEV-04). An app declares a struct whose `setting:"NAME"` fields (`,secret` for credentials) may be overridden by `CICADEL__`; it returns `App.Settings: settings.NewPackage(options)` and reads `Package.Value()`. `cicadel.New` resolves settings from `Settings.Env` before `Start`, rejects key collisions with project settings and other apps, and `check`/`check --json` report `:settings:invalid` naming the key, never the value. `manage.go apps` lists keys and values with secrets redacted. New: `settings.NewPackage`, `Package`, `LoadPackage`, `PackageSettings`, `Field`, `PackagePrefix`, `CoreKeys`, `Redacted`, `Settings.Env`, `App.Settings`, `Application.PackageSettings`, `AppSettings`. The `startpackage` scaffold uses it, and `contrib/accounts` `Options` now declares `CICADEL_ACCOUNTS_SESSION_LIFETIME`, `_RECOVERY_LIFETIME`, `_IDLE_TIMEOUT` and `_BCRYPT_COST` with `Options.Validate`; generated projects wire them through `AccountOptions`. No breaking changes. See `docs/package-settings.md`. ## v0.2.3 — 2026-11-11 Adds transaction post-commit hooks and moves to Go 1.26.11 for a standard-library security fix. No breaking changes. Upgrade all five modules to v0.2.3 together and raise your project's `go` line to 1.26.11. - Added transaction post-commit hooks (DB-03): `database.Atomic` runs a function in a PostgreSQL transaction (a savepoint when nested) and `database.OnCommit(tx, fn)` registers a callback that runs after the outermost transaction commits, in registration order. Hooks are discarded on rollback, on failed commit and with a rolled-back savepoint; outside an `Atomic` transaction `OnCommit` returns `database.ErrNoTransaction`. Hook errors and panics are logged and cannot undo the commit; hooks receive a context without the caller's cancellation. Model form (`Bound.Create`/`Update`) and administration saves now run through `database.Atomic`, so `AfterSave` hooks can register post-commit work; formsets adopt it once that module requires this release. See `docs/transactions.md`. - Require Go 1.26.11 (was 1.26.10) in every module and in generated projects, picking up the standard-library `net/http` fix for GO-2026-6859. Projects should raise the `go` line in `go.mod` to 1.26.11; with the default `GOTOOLCHAIN=auto` the toolchain is downloaded automatically. ## v0.2.2 — 2026-11-11 Adds signed, expiring tokens with secret-key rotation. No breaking changes: existing projects keep working, and `check --deploy` warns until `CICADEL_SECRET_KEY` is set. Upgrade all five modules to v0.2.2 together. - Added the `signing` package (SEC-04): purpose-bound, timestamped, URL-safe tokens signed with HMAC-SHA256 under per-purpose derived keys; `Verify` with a maximum age, `VerifyNoExpiry`, typed `SignJSON`/`VerifyJSON[T]`, `ErrInvalid`/`ErrExpired`/`ErrNoKey`, a one-minute clock-skew bound and `WithClock` for tests. New settings `SecretKey` (`CICADEL_SECRET_KEY`) and `SecretKeyFallbacks` (`CICADEL_SECRET_KEY_FALLBACKS`) sign with the current key and verify with previous ones during rotation. Handlers use `signing.For(r.Context(), purpose)`; commands and jobs use `Application.Signer`. `cicadel secretkey` generates a key, `check --deploy` reports `settings:secret_key` when none is set, and `apptest.Settings()` uses the fixed `apptest.TestSecretKey`. See `docs/signing.md`. ## v0.2.1 — 2026-11-10 Adds security headers and HTTPS policy. No breaking changes: existing projects keep their behavior until they opt in. Upgrade all five modules to v0.2.1 together. - Added security-header defaults and HTTPS policy (SEC-03). New settings: `HSTSSeconds` (`CICADEL_HSTS_SECONDS`), `SecureRedirect` (`CICADEL_SECURE_REDIRECT`), `TrustedProxies` (`CICADEL_TRUSTED_PROXIES`) and `ContentSecurityPolicy` (`CICADEL_CONTENT_SECURITY_POLICY`), with `settings.DefaultContentSecurityPolicy` and `settings.HSTSYear`. `security.IsHTTPS` reports HTTPS through TLS or a trusted proxy. Generated projects enable HSTS and the default policy; existing projects keep their behavior and get `check --deploy` warnings until they opt in. See `docs/security-headers.md`. - Added the versioning standard to `docs/releases.md`: patches carry fixes and backward-compatible additions, the minor version moves only when a milestone is complete, and v1.0 requires Django-level completeness. ## v0.2.0 — 2026-11-10 Adds pagination and flash messages, the first two open core gaps, and fixes two v0.1.0 problems. No breaking changes: upgrade all five modules to v0.2.0 together. - Added `paginate`: validated page parsing, ordered GORM pagination in one read-only snapshot, slice pagination, query-preserving relative links, Django-style elided page ranges and the accessible `paginate.Nav` component. - Fixed `management.ExitCode` returning success for errors whose wrapped cause is nil. `cicadel ui check` now exits 1 on type errors instead of 0, and `manage.go` commands report such failures. - The core modules now require each other at v0.1.0, so each builds on its own with `GOWORK=off`; v0.1.0's modules pinned an older framework revision without `App.Static`. CI builds the migrations and query-generation modules standalone. - Added `flash`: session-backed, consume-once messages with info/success/warning/error levels, size limits and the accessible `flash.List` component. See `docs/pagination-and-messages.md`. ## v0.1.0 — 2026-11-10 First release. The core framework is ready for building applications: projects and apps, settings, routing, GORM models with recorded PostgreSQL migrations, typed components with Tailwind and htmx, forms, accounts with sessions and CSRF, static files, email, logging, test helpers and installable packages. From this release, APIs follow the stability guarantee in `docs/releases.md`; the root module and the `contrib/accounts`, `contrib/formsets`, `contrib/migrations` and `contrib/querygen` modules are released together at the same version. Next: pagination and flash messages. Install: `go run github.com/fauzaanu/cicadel/cmd/cicadel@v0.1.0 startproject myapp example.com/myapp`. ### Changes included in v0.1.0 - Added typed static-file contributions: `App.Static` publishes app-owned public files at `/static//` with content-hashed, immutably cached URLs, revalidated authored names, `static.URL` request resolution, `settings.StaticURL`/`CICADEL_STATIC_URL` (local path or https CDN prefix) and an offline `collectstatic` command writing hashed files plus `manifest.json`. Accounts, the starter project, `startapp`, `startpackage` and the component-library example now use it; the `/accounts/style.css` and `/static/app.css` routes are replaced and `ComponentAssets` paths are now relative to `App.Static`. See `docs/static-files.md`. - Made accounts and formsets standard project defaults; added offline `makemigrations` and `generatequeries` management commands from the shared model registry. - Consolidated migration application, planning, verification, adoption and recovery on the recorded-history runner. Removed the separate `database.Migrate`/`Migrator` executor; existing Goose histories require verified adoption. - Preserved first-party handwritten SQL with reviewed historical manifests and verified legacy serial/unique/timestamp-default support. Updated application guides and installable contracts to describe core defaults. - Breaking: renamed Yutqin to Cicadel, including Go modules and packages, CLI executables, environment variables, generated artifacts, cache paths, and framework-owned database identifiers. - Added typed installable runtime model catalogs with explicit app/table/type ownership, offline validation and isolated discovery. Recorded migrations remain authoritative. - Completed application foundation tooling: all operational settings overrides and redacted diagnostics, sorted command help and exit statuses, structured/deployment checks, and safe component/CSS rebuild/restart through `cicadel dev`. Added generated-executable and reload acceptance tests; see `docs/foundation.md`. - Added a green-and-black Tailwind default theme and optional `startproject --accounts` browser starter with compiled login/recovery/reset/account/password-change pages, CSRF protection, PostgreSQL cookie sessions and shared account revocation. - Added SMTP with verified STARTTLS/implicit TLS, batch connection reuse, MIME alternatives and attachments, CC/BCC, reply-to and console/file/capture/discard backends. - Added named logging configuration with levels, filters, propagation, JSON/text/custom handlers, file rotation, optional error email, grouped credential redaction, correlated panic records and private GORM timing. - Reconciled current authentication, component, PostgreSQL migration, mail and logging documentation; administration remains a separate later battery. - Completed standalone typed forms and accessible widgets; added revision-based stale-edit detection, bounded upload fields with compensating cleanup and multipart CSRF integration, and atomic model-form/formset administration. See `docs/forms-completion.md`. - Added core `contrib/formsets`: bounded related-row collections with scoped identities, add/delete limits, parent locking, transactional hooks and atomic saves; added the core `Form.InTransaction` composition API. - Added `forms/model`: explicit GORM field allowlists, typed binding, scoped relationship checks, authorized row-locked updates, safe constraint errors and transactional validation/save hooks. Verified with PostgreSQL concurrency and rollback tests; see `docs/model-forms.md`. - Added the installable-app foundation: manifests, API/dependency compatibility checks, namespaced commands/checks/resources, route-scoped middleware and per-host startup/cleanup. - Added `startpackage`, generated `project/apps.go`, `manage.go apps`, and `apptest.Check`; tested independent Go-module install/upgrade/removal and PostgreSQL migration/data retention. - Implemented React/Cotton-inspired compiled component UI; retained legacy Go templates for compatibility and completed the component-engine acceptance checks. - Added a source-linked Django battery inventory with explicit support decisions and completion criteria. - Reclassified existing integrations as partial batteries; model-driven PostgreSQL migrations now have a documented supported subset, other database backends are excluded, and broader package-system contracts remain planned. - Removed the experimental native `locale` package; i18n/l10n and GIS are third-party-only by design. - Django-familiar project/app scaffolding, typed settings, explicit app registration and management commands. - Standard HTTP routing with namespaced reversal, escaped embedded templates, response helpers and static assets. - PostgreSQL/GORM integration and locked, versioned Goose migrations. - Sessions, password primitives, CSRF protection, bounded forms and explicit authorization. - Working articles and catalog/admin examples with real HTTP/database tests. - API helpers, mail transport/capture, bounded storage/cache, observability hooks and River jobs. - PostgreSQL integration/race tests, fuzz targets, generated-project validation, vulnerability checks and performance baselines. Requires Go 1.26.10+ to incorporate standard-library security fixes. No upgrade migration is required from a prior Cicadel release because none has been published by this implementation. --- Source: `SECURITY.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/sources/SECURITY.md # Security reporting This repository is currently private. Report vulnerabilities to the repository owner through a confidential channel or a security issue within this private repository, accessible only to its authorized collaborators. Do not post exploit details, credentials, or user data to public mirrors or unrelated issue trackers. If repository access is unavailable, contact the owner privately rather than publishing details. Maintainers should reproduce the report, assess affected versions, prepare a fix and regression test, coordinate disclosure with the reporter, and publish upgrade instructions when the fix is released. Avoid copying private reports into public release notes before coordinated disclosure. Before making the repository public, enable GitHub private vulnerability reporting and replace this private-repository process with its security-advisory reporting URL. The GitHub private-reporting endpoint is not available for this private repository; no public reporting address is claimed here. Cicadel is experimental. Local tests and automated scanning do not substitute for independent security review or application-specific authorization, rate limiting, deployment hardening, and account lifecycle controls. Supported security fixes initially target the latest prerelease only. Stable-release support windows will be defined before 1.0. --- Source: `contrib/accounts/README.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/ref/accounts/index.md # Accounts Optional PostgreSQL accounts using installable app API 1. This source module uses `replace github.com/fauzaanu/cicadel => ../..` for repository development; no remote module release is claimed. In a generated application's `project.InstalledApps(db)`, construct `accounts.New(accounts.Options{DB: db, DeliverRecovery: deliver})`, handle its error, and append `service.HTTPApp("/accounts")`. Run `manage.go migrate` before serving. The migration is recorded SQL, never AutoMigrate. Removing registration preserves data. The host owns its database pool. The HTTP endpoints accept POST JSON with `Content-Type: application/json`: | Path | Fields | Authentication | | --- | --- | --- | | login | Username, Password | Credentials; returns token | | logout | empty object | Bearer token | | change-password | OldPassword, Password | Bearer token | | request-recovery | Username | Public; invokes delivery callback | | reset-password | Token, Password | Recovery token | | revoke | empty object | Bearer token; revokes every session | Clients send `Authorization: Bearer `. These routes use no ambient cookies. Install `service.Middleware` explicitly on host routes requiring current-user context; use `UserFromContext` and reject anonymous requests where needed. The middleware does not itself require authentication. For browser applications, construct `service.Browser(sessions)` and wrap routes with `browser.Middleware`. This composes SCS LoadAndSave, CSRF validation and account resolution in order. Use `browser.Login`, `browser.Logout`, `browser.CSRFToken` and `browser.LoginRequired(loginURL)` in HTML handlers (`browser.RequireLogin` answers 401 for JSON routes). The account token is held only in the server-side session; disablement and revocation are checked on every request. Do not additionally wrap these routes with SCS/CSRF middleware. SCS login alone does not create an accounts session. Existing SCS APIs remain compatible. This package does not change forms, uploads or admin. Tokens contain cryptographic randomness and only SHA-256 digests are stored. Password changes and recovery revoke all sessions and recovery tokens in one transaction. Recovery consumption locks the user and rechecks token deletion, so concurrent redemption succeeds once. Session creation serializes with password changes. `SetActive(false)` revokes sessions and recovery tokens. Disabled users cannot log in, resolve current-user context or use permissions. Passwords must contain 12–72 bytes. `Policy` adds application validation; bcrypt cost defaults to 12 (configurable 10–16). Successful password authentication upgrades lower costs without downgrading stronger hashes or overwriting a concurrent password change. `Backend` can verify another credential source but must return an existing local account; central disabled-user checks still apply. `SessionLifetime` defaults to 24 hours and `RecoveryLifetime` to 30 minutes; both are absolute lifetimes of at least one minute. Optional `IdleTimeout` expires sessions after inactivity; successful current-user resolution refreshes activity. PostgreSQL is the persistent store. Schedule `manage.go accounts:cleanup` to remove expired sessions/recovery records. No background worker is started. Transport security, request rate limits, and recovery delivery belong to the host. Delivery must send the raw token only to the account's previously verified recovery destination; never to an arbitrary request-supplied address. Avoid logging tokens or credentials. Groups use `CreateGroup`, `AddMember`, `RemoveMember`, `Grant`, and `Deny`. `HasPermission` checks exact application-defined model/action strings such as `posts.change`. There is no superuser bypass or implicit object access. Apply an explicit host policy for each object in addition to model permissions. OAuth/OIDC, social login and MFA remain outside scope. The `accountui` package supplies compiled HTML account screens. Administration integration remains separate. Run `CICADEL_TEST_DATABASE_URL=postgres://...?... go test -race ./...` with a disposable PostgreSQL database and schema creation privileges. Tests isolate and remove their schema. Parent module tests do not include this nested module. `sh contrib/accounts/verify-host.sh` generates a separate application, registers the package via local module replacements, applies its public migration contract, and verifies login/logout over HTTP. Use a disposable database; the verification applies accounts migrations and removes its test user afterward. ## Events The app declares `UserCreated`, `LoggedIn`, `LoginFailed`, `LoggedOut` and `PasswordChanged` (`accounts.Events()`). Subscribe from any installed app, for example `accounts.LoggedIn.Handle(fn)` in its `App.Subscriptions`. Payloads with `Tx` are emitted inside the account transaction: write through `e.Tx` to commit with the change, return an error to roll it back, and use `database.OnCommit(e.Tx, fn)` for effects after commit. `LoginFailed` carries only the submitted username. Events reach subscribers when the service is called with an application context (requests, commands, `Application.Context`). See the [events guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/events/index.md). ## Browser pages Use `service.Browser(sessions)` and `browser.HTMLApp(Pages{Brand: "My app"})` for the default compiled HTML screens. Register that app once and migrate before serving. It owns its cookie/CSRF middleware and embedded CSS; other protected routes apply `browser.Middleware` followed by `browser.LoginRequired("/accounts/login")`, which redirects anonymous visitors to sign in and returns them to a safe local `next` path afterwards. `UserFromContext` is shared with the bearer adapter. The browser stores the account token only in the server-side SCS store, rotates login cookies and checks revocation/disablement on every request. Migration 002 adds the PostgreSQL `accounts_browser_sessions` table used by the starter. Custom session stores remain supported. Use `cicadel startproject DIRECTORY MODULE` for a wired project, including verified account provisioning and SMTP recovery delivery. Read the [complete authentication guide](https://cicadel.fauzaanu.com/en/0.2.7/topics/authentication/index.md) for the route table, configuration, middleware ownership and testing. There is no public signup or administrator panel in this starter. This module owns `.templ` source and generated Go/CSS. Run `ui build` and `ui check --json` using the matching framework CLI, in addition to its tests. --- Source: `contrib/formsets/README.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/ref/formsets/index.md # Formsets Core FORM-03 battery for bounded collections of model forms and atomic inline related-object editing. PostgreSQL 18 and GORM are supported. This is a separate Go module, using experimental Cicadel app API 1 and the new `forms/model` API. The declared app version is `v0.1.0`; no published module release is claimed. For local development, require `github.com/fauzaanu/cicadel/contrib/formsets v0.0.0` and replace it with this directory; also replace the framework module with the repository root. Go does not inherit a dependency's replace directives. Consumers must pin a real framework/module release once published. The generated consumer test verifies local source integration, not remote versioned installation. Standard projects register `formsets.App()` in `project/apps.go` for inventory and provider validation. Retain this entry when adding local apps. The package owns no routes, schemas, assets, workers or migrations; removing it never deletes application data. The default project includes this module through the public installable contract. ## Define and submit a collection ```go set, err := formsets.New[LineItem](db, formsets.Config[LineItem]{ Model: model.Config[LineItem]{Fields: []model.Field{ {Name: "Description", Required: true, MaxRunes: 200}, {Name: "Quantity", Required: true}, }}, Prefix: "lines", MaxForms: 100, MinItems: 1, MaxItems: 50, AllowDelete: true, }) // Handle configuration errors during setup. collection := formsets.Inline[Order, LineItem](orderID, "OrderID", func(tx *gorm.DB) *gorm.DB { return tx.Where("owner_id = ?", userID) }) posted, err := forms.Parse(r) // Handle parse failure; use body limits, authentication and CSRF middleware. bound := set.Bind(posted.Values) result, err := bound.Save(r.Context(), collection) ``` Use imports `github.com/fauzaanu/cicadel/contrib/formsets`, `github.com/fauzaanu/cicadel/forms`, and `model "github.com/fauzaanu/cicadel/forms/model"`. Define parent/child models and recorded PostgreSQL migrations normally, including foreign keys and unique constraints. Never include the parent foreign key in editable fields. The parent identity is trusted route/application input; parent authorization is mandatory. The URL-encoded wire format is: ```text lines-TOTAL_FORMS=3 lines-0-id=12 lines-0-description=Updated description lines-0-quantity=2 lines-1-id= lines-1-description=New item lines-1-quantity=1 lines-2-id=15 lines-2-DELETE=on ``` Indexes are zero-based and contiguous. `TOTAL_FORMS` is required exactly once, canonical decimal, and limited before allocating rows. MaxForms defaults to 100 and must be between 1 and 1000. MaxItems defaults to MaxForms; it limits stored items, including existing rows omitted from this submission. MinItems defaults to zero. IDs must be canonical positive integers within the model key type; blank means new. Integer primary keys are supported; composite/UUID/string keys are explicitly rejected in this first formset contract. Duplicate identities, repeated control values, malformed/out-of-range indexes, invalid typed input and unauthorized rows fail the whole batch. Unknown model fields cannot cause mass assignment. A deleted row bypasses ordinary required field validation; deleting an unsaved extra row simply discards it. Blank extra rows are otherwise validated normally. Omitting a stored row preserves it. Deletes are disabled unless AllowDelete is true. Boolean delete values are `on`, `1`, `true`, `0`, `false`, or empty. Prefixes are ASCII identifiers; editable input names cannot be `id`, `DELETE`, or contain hyphens. `Bound.Errors()` returns nil or an independent `*formsets.Errors` snapshot. `Rows[index]` contains field messages (empty field key is row-wide); `General` contains collection messages. `Values(index)` and `Len()` support redisplay in your existing typed components. No widget/HTML generation is added. Save-time validation can also return `*Errors`; other errors are operational and must not be shown verbatim. Parent authorization failure is `gorm.ErrRecordNotFound`. ## Atomic persistence Inline locks the authorized parent `FOR UPDATE`, even for empty collections. Existing submitted child identities are reloaded and locked in key order under the parent scope before any writes. Item limits are calculated from the stored collection, not a client-provided initial count. Explicit deletes run first, then creates/updates run in submission order. This permits replacing a deleted unique value. Swapping two surviving unique values is not implicitly staged; PostgreSQL constraints decide whether it is valid. Every save/delete, GORM hook, model-form check/hook, and formset hook uses the same outer transaction. Any failure rolls back the entire batch. Results are returned only after commit: Saved contains persisted rows in submission order; Deleted contains removed IDs. GORM soft-delete behavior and authored database cascade rules apply; this package does not invent cascades. `BeforeDelete` and `AfterDelete` accept `(context.Context, *gorm.DB, *T)`. Changing deletion identity is rejected. `Validate(context.Context, *gorm.DB)` is a final cross-row validation hook: query the transaction and return `*Errors` for safe collection messages. It should be read-only. Model hooks cannot move saved rows outside the collection; persisted ownership fields are checked. Limits are also rechecked after hooks. Use the supplied transaction for audit or join-table writes; defer external side effects until commit or use an outbox. For other collection structures, supply a `Collection[T]` with a trusted Lock, Scope, Seed and Protected field names. Lock must serialize all membership changes (including insertions into an empty collection); Scope filters rows on the supplied transaction; Seed supplies trusted initial values. All writers—including code outside this package—must honor the same collection lock for count guarantees. Normal database deadlock/serialization errors remain operational errors; callers may retry the complete operation. Repeated submissions are separate operations, not implicit idempotent requests. Do not construct sets on an existing transaction; the public Save result assumes ownership of the outer commit. The core's `Form.InTransaction(tx)` is the general composition seam used here. Individual model-form results inside that transaction remain provisional until the outer commit. It accepts PostgreSQL transaction handles only and retains the original model/naming configuration. ## Verification and boundaries Tests cover malformed management data, bounded allocation, identity tampering, scoped parent/child ownership, mixed add/edit/delete, omitted-row preservation, zero values, replacement under uniqueness, limits, concurrent additions, and rollback including deletion/audit hooks. A generated independent project registers the app and exercises the API through HTTP form submissions. Lifecycle checks verify removal does not perform schema/data cleanup. Tests create isolated SQL fixtures, not AutoMigrate-managed schemas. This completes FORM-03 for the documented integer-key collection contract. Nested inline collections, automatic relation/widget inference, client-side row editors, string/composite identities are excluded. FORM-01 widgets/validators are complete; see [forms completion](https://cicadel.fauzaanu.com/en/0.2.7/topics/forms-completion/index.md). See [TESTING.md](https://cicadel.fauzaanu.com/en/0.2.7/sources/contrib/formsets/TESTING.md) for reproducible checks. `Model.VersionField` enables per-row `_version` validation for edits and deletes. `IgnoreEmptyNew` optionally discards wholly empty new rows. `AdminInline[P,T]` provides a typed inline editor and transactional adapter for `admin.Config[P]`. Its parent authorization, ownership and child validation are covered by `admin_test.go`. See the complete forms guide for configuration. --- Source: `contrib/migrations/README.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/ref/migrations/index.md # Model-driven migrations (experimental installable) This independent Go module implements Cicadel's standard automated migration battery. It uses GORM model metadata, compares it with recorded model history, and writes **Goose SQL migration files**. Generation never opens a database. Applying migrations remains an explicit, separate command. For the default project workflow, use [the migration guide](https://cicadel.fauzaanu.com/en/0.2.7/topics/migrations/index.md). The lower-level APIs and standalone tools below operate on the same engine and support existing hosts and history maintenance. ## Try the complete workflow From this repository: ```sh cd contrib/migrations/example go run . migrations:makemigrations notes --dry-run go run . migrations:makemigrations notes --name initial DATABASE_URL='postgres://user:password@localhost/app?sslmode=disable' go run . migrate ``` Edit `models/note.go`, adding `Published bool` with the tag `` `gorm:"not null;default:false"` ``, then: ```sh go run . migrations:makemigrations notes --name published go run . migrations:makemigrations notes --check go run . migrate ``` Keep `DATABASE_URL` in the environment when applying. `--check` exits unsuccessfully if an unrecorded model change exists; it does not check which files the database has applied. `--dry-run` prints the candidate file. Both modes leave files and directories untouched. An unchanged model set writes nothing. ## Install and register The source module lives at `github.com/fauzaanu/cicadel/contrib/migrations` and has its own `go.mod`, tests and dependencies. Framework management imports this module as its canonical migration implementation. There is no published package release yet. For local development, add a project `replace` for this module to the checkout's `contrib/migrations` directory and require it at `v0.0.0`; likewise replace the framework with this checkout. Before publication, pin a released framework version and remove the development replace. The manifest's `v0.1.0` is the experimental contract version, not an existing tag. ```go import migrations "github.com/fauzaanu/cicadel/contrib/migrations" // In your installed-app factory: generator, err := migrations.New(migrations.Config{ Apps: []migrations.Definition{{ Name: "blog", Directory: "apps/blog/migrations", Models: []any{&blog.Author{}, &blog.Post{}}, }}, }) // Check err, then register your blog app and generator with cicadel.New. // Give the blog app Migrations: os.DirFS("apps/blog/migrations") for development, // or embed the committed migrations in the production executable. ``` Run `migrations:makemigrations blog` through `management.Manager`. The app registers this as a `BuildCommands` contribution. **Your project builder must inspect `app.IsBuildCommand(args[0])` before opening the database or acquiring resources.** The manager bypasses runtime hooks, provider checks and global runtime checks for these commands. Constructors must also remain free of database work. See the [runnable host](https://github.com/fauzaanu/cicadel/blob/2678611d308376ca8045c8741ee9d4d7814a3fec/contrib/migrations/example/manage.go) for the full wiring; ordinary existing project builders are not automatically rewritten by installing this module. `models.go` and `models/*.go` both work: these are ordinary imported Go structs. Explicitly register every managed model in `Definition.Models`. Alternatively, generate this registry using the build-time discovery command below. Use the same GORM naming strategy in `Definition.NamingStrategy` and your runtime GORM configuration. Each directory belongs to exactly one app. Cross-app relationships require explicit pinned historical references, described below. ## File format and review `00001_initial.sql` contains a model-state JSON comment followed by `-- +goose Up` and `-- +goose Down` SQL. Later files record a hash of the preceding file. The snapshot and SQL are published together as one file, with an exclusive generation lock and an atomic no-overwrite publish. Every generation revalidates the history and regenerates its expected SQL to detect accidental edits. Commit these files. SQL from old files runs independently of current model definitions. Review both directions before applying. Down statements can drop tables, columns and data; their existence is not a promise of lossless rollback. Use the checksum-aware `Runner` for PostgreSQL transactions, execution history, locking and schema verification. Rollback uses an explicit target with `cicadel-migrate -target APP=VERSION -allow-data-loss apply`, as shown below. Handwritten histories require reviewed per-version snapshots and the explicit import/adoption protocol below. Legacy generated formats require contiguous versions; format 3 supports explicit branches and merges. Do not mix unrecorded SQL into a managed directory. Use recorded data/schema declarations instead. Generation validates immutable history offline; the runner additionally detects applied-history changes and managed-schema drift against the live database. Generation does not prove constraints will fit existing data. ## Supported slice - PostgreSQL only; single or composite primary keys, optional integer identity. - Built-in boolean, integer, float, string, byte and time fields; nullable columns, string lengths, and scalar defaults. Unsigned 64-bit values are limited to the PostgreSQL signed BIGINT range; choose signed types for predictable portability. - Creating tables; adding ordinary nullable columns or non-null columns with an explicit scalar default; adding ordinary/unique indexes. - Single/composite foreign keys to registered primary or unique keys in the same app, including belongs-to/has-one/has-many declarations and reference actions. Explicit join models represent relationship tables. All tables and indexes are created before foreign keys. Named check constraints are supported. - Deterministic output, no-op detection, SQL preview, CI checks and reverse SQL. Undeclared removals, renames and alterations are rejected. Explicit declarations are described below. Remaining unsupported features include implicit many-to-many join tables, native user-defined SQL types, unregistered custom storage mappings, expression/default SQL and advanced indexes. Versioned built-in storage mappings for custom fields and serializers are supported as described below. Explicit `gorm:"-"`/migration-ignore fields remain unmanaged. Unsupported GORM features may still be used with the existing handwritten migration workflow. Other databases are excluded. Dependency graphs, compatible merges, replacement bundles, verified baselines and managed-schema drift checks are supported with the explicit boundaries documented below. ## GORM and GEN GORM is already the runtime ORM. Normal Go models and GORM queries work today; `AutoMigrate` is not called. This package generates schema migration files, and The recorded-history runner applies them. [GORM GEN](https://gorm.io/gen/index.html) can additionally generate type-safe query APIs from existing structs (`ApplyBasic`), or generate structs from live tables. It does not replace historical schema diffing. GEN is not a dependency of this package; the separate [querygen installable](https://cicadel.fauzaanu.com/en/0.2.7/ref/querygen/index.md) generates queries from authored models and tests GORM version compatibility. ## Verification ```sh go test -race ./... go vet ./... CICADEL_TEST_DATABASE_URL='postgres://.../disposable_test_database' go test -race ./... ``` The PostgreSQL integration test creates and drops its `ym_*` fixture tables and `cicadel_modeltest_versions`. Use a disposable database. CI runs this nested module explicitly: root `go test ./...` does not traverse nested modules. ## Build-time model discovery Opt models in with `//cicadel:model` immediately above their exported struct declarations. Helpers without that marker are excluded even in `models.go`. Select the installed app packages explicitly; no runtime directory scanning occurs. From the consumer module (after installing this module): ```sh go run github.com/fauzaanu/cicadel/contrib/migrations/cmd/cicadel-models --out modelregistry/models.gen.go ./apps/blog ./apps/blog/models go run github.com/fauzaanu/cicadel/contrib/migrations/cmd/cicadel-models --check --out modelregistry/models.gen.go ./apps/blog ./apps/blog/models ``` Use `modelregistry.Models()` in `Definition.Models`. The output package must be separate from model packages to avoid import cycles. Go package/type analysis respects imports, GOOS, GOARCH and `--tags`; excludes test files; rejects aliases, generic and unexported model types. Run `--check` for every supported build configuration before compilation. A configuration with a different model set requires a separate registry/build configuration, not silently shared history. `--check` detects a stale or missing registry without writing anything. The runnable example uses generated discovery; its `models.Models()` function also demonstrates the explicit-registration escape hatch. Regenerate it from this module using: ```sh go run ./cmd/cicadel-models --out example/modelregistry/models.gen.go ./example/models ``` ## Declared PostgreSQL changes (format 2) Explicit declarations were introduced in format 2; new files use format 3. Format-1 and format-2 files remain readable and byte-for-byte immutable; the next file links their existing hash. No conversion or rewrite is necessary. Pass `Options.Changes` in Go or `--changes changes.json` to `makemigrations`. Declarations run in order before inferred additions. For example, after renaming `Body` to `Text` in the model (keeping its table name): ```json [{"kind":"rename_column","table":"demo_notes","name":"body","to":"text"}] ``` ```sh go run . migrations:makemigrations notes --name rename_body --changes changes.json ``` Declarations are one migration's intent, not permanent model settings. Omit them on subsequent no-op/check runs. Reusing a stale declaration fails validation. Supported declarations: - `rename_table` (`table`, `to`) and `rename_column` (`table`, `name`, `to`) preserve data and update recorded references. Implicit PK/identity names are renamed with their owning objects. - `drop_table` and `drop_column` require a nonempty `reason` acknowledging data loss. Drop dependent indexes/FKs explicitly first; no implicit CASCADE. - `drop_index`, `drop_check` and `drop_foreign_key` explicitly remove constraints. A changed model declaration then adds its replacement through the ordinary diff. - `alter_column` supplies the complete target `column`. Type/size changes require an authored `using` expression and `reason`; `reverse_using` is optional and is an author's explicit claim that the reverse conversion is appropriate. Making a column non-null requires `backfill`, an SQL expression for NULL rows. Defaults may be changed or removed. Primary/identity alterations still fail. - `data` supplies `up` and optional `down` SQL against historical table/column names, independently of current Go types. It must not change schema. SQL is trusted developer input; never interpolate request input into declarations. A removal, backfill, conversion without `reverse_using`, or data operation without `down` makes the migration irreversible: its Down raises an error. Restore a backup or write a reviewed forward repair. Even an explicit inverse conversion may lose information; review it against real data. A reversible schema change is not a data backup. New constraints may fail against existing rows; PostgreSQL rolls back the transactional migration, permitting correction and retry. Preview/check validate all declarations and historical files before writing. Embedded Goose directives in authored expressions/SQL are rejected. PostgreSQL checks SQL syntax and data-dependent constraints when applying; offline generation does not claim to prove arbitrary SQL expressions safe or valid. ## PostgreSQL catalog, execution and recovery `Load(definitions...)` reads historical files without current models. Its catalog orders migrations by version-level dependencies plus each app's previous version. Set `Options.Dependencies` to refs such as `Ref{App: "accounts", Version: 2}` when creating a dependent migration. Missing dependencies, cycles and inconsistent applied prefixes fail planning. A target cannot orphan another app's dependency. Format-3 histories support explicit branches, merges and replacement bundles as described below; duplicate IDs and missing parent versions are rejected. A standalone command avoids starting application runtime hooks: ```sh # From contrib/migrations, after generating example migrations: go run ./cmd/cicadel-migrate -app notes=example/migrations show go run ./cmd/cicadel-migrate -app notes=example/migrations -offline sql DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations plan DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations apply DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations verify # Roll back to a target after reviewing its Down SQL and taking a backup: DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations -target notes=1 -allow-data-loss apply ``` Repeat `-app APP=DIRECTORY` for **every managed app**, including dependencies. `show` inspects artifacts offline; `status` reports applied/pending state from the database. `migrate`, `showmigrations` and `sqlmigrate` alias `apply`, `status` and `sql`, respectively. `plan` and `sql` compare against applied history. `-offline` explicitly assumes a fresh database. `-timeout` bounds connection, lock and execution time. Plans are rechecked under the shared Goose advisory lock. The new `Runner` maintains `cicadel_migration_checksums` alongside existing `cicadel_APP_versions` Goose history. Every migration's SQL, checksum and Goose record commit atomically. A failure rolls back that migration; previously committed migrations remain applied. Correct the data prerequisite and retry the same immutable SQL. Do not edit an applied migration or rewrite its checksum. Missing applied files, modified checksums, inconsistent dependencies and drift fail explicitly. A commit error is reported as uncertain: inspect before retrying. For an existing **generated** Goose history, use `adopt` once. Adoption verifies the actual managed schema against the snapshot of the applied prefix before recording checksums. It never marks pending migrations applied or overwrites checksums. Unrecorded handwritten histories are not automatically adopted. Existing schemas without history use the explicit initial-baseline workflow below. Schema verification renders expected tables in a randomly named scratch schema inside a transaction that is always rolled back. It requires database CREATE privilege. Managed columns, defaults, identities, keys, constraints, indexes, user-trigger presence and row-level-security enablement are compared; unrelated tables are ignored. This is schema verification, not verification of row contents, permissions, sequence counters, every PostgreSQL extension feature or external processes that change schema without taking the migration lock. Management commands and the standalone tooling use this same checksum-aware runner. The former core Goose executor has been removed. Existing Goose records require verified adoption before further migrations; applied SQL is never silently replayed. ### Authored schema SQL and initial baselines `Record(definition, historicalState, options)` is the model-independent recording API. `RecordData(definition, up, down, options)` uses the latest recorded state without loading models. Both use the same atomic file publication and hash chain. For handwritten schema SQL, a `Change{Kind: "schema", After: &state, Up: sql, Down: reverseSQL, Reason: "..."}` records the complete resulting historical state. Missing Down makes it irreversible. The transactional runner verifies that SQL actually produced the declared state **before committing**; it also checks tables declared removed are gone. Only schema features represented by `State` can be verified. Transaction-control statements and concurrent DDL are rejected. Explicit handwritten SQL is trusted developer code. To adopt an existing database with no migration history, generate schema-only initial files from its intended models, review them, then run: ```sh DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations -reason 'Adopt existing release after schema review and backup' baseline ``` Baseline requires exact managed-schema verification and no existing Goose history for those apps or checksum-managed history in the database. Include all initial apps in the same operation. Only generated initial schema files are eligible; authored data SQL and later versions cannot be skipped this way. Initial SQL is **not executed**. `cicadel_migration_baselines` permanently records the checksums, reason and time of adoption, separately from execution history. Existing rows remain intact. Later migrations execute normally. Existing Goose histories use `adopt` instead; neither workflow can rewrite an existing checksum. ### Branches and replacements (format 3) New files now use format 3, recording explicit parent IDs and hashes. Formats 1 and 2 remain readable without rewriting. Version IDs are unique positive integers; format-3 IDs may have gaps so independently developed branches can allocate separate IDs. Legacy formats retain their original contiguous-history contract. Use `--parent VERSION --version NEW_ID` to generate a deliberate branch. On combining branches, ordinary generation/application rejects multiple heads. Create an explicit merge using `makemigrations --merge` with the combined models, or, independently of models: ```sh go run ./cmd/cicadel-migrate -app notes=example/migrations -name merge_branches merge ``` Compatible disjoint schema changes merge deterministically. Concurrent changes to the same schema object, including duplicate additions, fail rather than inventing a destructive resolution. Resolve unapplied conflicting work explicitly; never edit a migration already applied to any deployment. Authored data SQL still requires human review for semantic conflicts; schema snapshots cannot prove data operations commute. Target versions select their ancestor closure, not every smaller numeric ID. Rollback follows reverse dependency order. Record a cross-app dependency with `--depends accounts:2` (repeat as needed), or `Options.Dependencies`. All dependency apps must be supplied when loading/applying. Data SQL can be recorded offline with `-up-file` and optional `-down-file` followed by the `data` command; it reads historical state without importing current models. Consolidate an app's ancestor history into a replacement: ```sh go run ./cmd/cicadel-migrate -app notes=example/migrations -target notes=3 -name initial_history squash ``` The `.squash.json` bundle is canonical, checksummed and verified against every original source. Fresh installations execute an eligible consecutive bundle in one transaction and audit it in `cicadel_migration_replacements`. Partially applied histories execute remaining originals, preserving data SQL order. Cross-app ordering that interleaves bundle members also falls back to originals. Rollback uses original reverse operations. SQL is consolidated, not optimized away. **Retain the originals and the bundle**: deletion of either executed history is an error, and originals remain necessary for older deployments. This workflow supports replacement without assuming all users have already upgraded. ### PostgreSQL capabilities and nontransactional recovery `Renderer("postgres")` exposes the versioned `SchemaRenderer` contract and its capabilities. Other databases are rejected. `Render` is offline and accepts historical before/after states and explicit declarations. PostgreSQL 18.4 is the locally tested version; CI runs PostgreSQL 18. Ordinary schema operations run transactionally. No support for unrelated database providers is implied. One explicitly supported nontransactional operation is a single concurrent index addition, selected with `Options.ConcurrentIndex` or `makemigrations --concurrent-index INDEX_NAME`. Its reverse drops that index concurrently. Mixing it with other schema changes is rejected before publication; arbitrary nontransactional SQL and transactional replacement of concurrent-index migrations are unsupported. Before concurrent DDL, the runner commits a dirty marker. Failure or interruption blocks ordinary apply/plan until recovery is deliberate. For a failed unique index, inspect PostgreSQL's invalid index and duplicate rows, repair the data and remove the known failed index explicitly. Then: ```sh # Certify that the pre-migration schema is restored; this does not rerun SQL: DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations -target notes=3 -recovery retry recover # Retry the immutable migration: DATABASE_URL='postgres://.../app' go run ./cmd/cicadel-migrate -app notes=example/migrations apply ``` If DDL completed but the process stopped before recording completion, use `-recovery complete` instead. It records completion only after verifying the post-migration schema. Both actions are audited in `cicadel_migration_recoveries`. Neither action blindly executes SQL, discards a dirty marker, drops an index, or claims transactional rollback of nontransactional work. ### Cross-app model relationships Pin the referenced app's recorded schema in the dependent definition: ```go migrations.Definition{ Name: "notes", Directory: "apps/notes/migrations", Models: []any{¬es.Note{}}, References: []migrations.ReferenceSource{{ App: "accounts", Version: 1, Directory: "apps/accounts/migrations", }}, } ``` The referenced model may appear in a GORM relationship field, but do not register its table as owned by both apps. Generation resolves the pinned historical state, records it in the dependent snapshot, and automatically adds the version-level dependency. Catalog loading checks that the recorded referenced snapshot matches the referenced migration. Every dependency app must be supplied to the runner. Cross-app destructive changes require deliberate ordering: for example, first record removal of a dependent FK, then rename the referenced table with a dependency on that removal, then add the FK against the new pinned snapshot. The planner will not guess how to coordinate a destructive change across apps. Explicit join models with composite keys can refer to multiple app snapshots. ## Project initialization and runnable lifecycle The [project workflow guide](https://cicadel.fauzaanu.com/en/0.2.7/topics/migrations/index.md) installs the commands with Go tool directives and initializes an offline entrypoint. Run `go tool cicadel-migrations-init -app blog=./apps/blog` after marking authored models. Existing files are never overwritten. The separate-consumer test exercises actual local module installation, discovery across `models.go` and `models/`, regeneration checks, offline migration generation and PostgreSQL application. From this module, run the complete disposable lifecycle: ```sh DATABASE_URL='postgres://.../disposable_database' go run ./example/workflow ``` It preserves generated artifacts for review and removes its own uniquely named schema. The walkthrough covers initial generation/application, a cross-app FK, rename/rollback/reapply, conversion/backfill, historical data SQL, transactional failure/retry and concurrent-index repair/recovery. Its integration test runs in CI. ### Importing an existing handwritten history `ImportGoose(definition, []LegacySnapshot{...})` requires a reviewed schema state and reason for **every** handwritten SQL version. It preserves all SQL bytes and writes `.cicadel-legacy.json`, binding file hashes, historical states and declared dependencies. `Reversible` defaults to false; a legacy Down is not evidence that lost rows can be recovered. Later generated files link the imported fingerprints. The CLI accepts the corresponding JSON array: ```sh go tool cicadel-migrate -app blog=apps/blog/migrations -states-json reviewed_states.json import DATABASE_URL='postgres://.../existing_app' go tool cicadel-migrate -app blog=apps/blog/migrations adopt ``` Existing applied prefixes are verified against their actual recorded schema before adoption. Fresh installations execute the original SQL and generated upgrades. Tests cover fresh, initial-prefix and fully-applied legacy histories. Editing either imported SQL or its metadata changes the migration fingerprint. Supported import directives are transactional Up/Down and StatementBegin/StatementEnd. NO TRANSACTION, environment substitution, unknown directives, transaction control and CRLF source files fail before publication; convert unsupported histories deliberately rather than silently changing their execution semantics. Snapshots must use supported PostgreSQL state metadata. Adoption verifies schema, not historical row contents: reviewing old data SQL and its provenance remains the operator's responsibility. ### Package-owned embedded histories Runtime catalogs can use `Definition{Name: "packageapp", Source: migrationFS}` with an `fs.FS` rooted at that app's migrations. This supports independently installed packages and executables without source-tree access. Use `//go:embed all:migrations` and `fs.Sub` when importing histories: the `all:` prefix includes `.cicadel-legacy.json`. Generation/import/squash require a writable source checkout and reject an embedded source. They never write into a dependency cache. The separate-consumer integration test applies an embedded package history. ### Custom fields and serializers Register explicit storage metadata in `Definition.CustomTypes`, keyed by the explicit GORM `type` tag when present, otherwise its field data type. For example: ```go CustomTypes: map[string]migrations.CustomType{ "uuid": {Version: 1, SQL: "uuid"}, "jsonb": {Version: 1, SQL: "jsonb"}, "numeric": {Name: "money", Version: 1, SQL: "numeric(12,2)"}, }, ``` `SnapshotWithOptions` accepts the same registry through `SnapshotOptions`. Mappings use supported built-in PostgreSQL storage types and record their version in historical state. Storage changes require explicit alteration declarations; old SQL replays without the current custom Go field implementation. Native enum and domain lifecycle management and arbitrary custom SQL types are unsupported. ## Handwritten schema metadata Reviewed import snapshots can represent serial primary keys, named UNIQUE constraints and the `now()` timestamp default. These are explicit historical metadata, not inferred from the live database during generation. Verification compares their PostgreSQL catalog signatures, including constraints and sequence properties. Other SQL default expressions remain unsupported. Original SQL bytes and applied versions are preserved during verified adoption. --- Source: `contrib/migrations/example/migrations/README.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/sources/contrib/migrations/example/migrations/README.md Generated SQL belongs here. Run commands from the example directory. Commit generated files; review their Up and Down sections before applying. --- Source: `contrib/querygen/README.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/ref/querygen/index.md # Typed query generation This separately installable module generates GORM query APIs from authored Go structs. PostgreSQL is the only supported database. It does not generate models, read database schemas or generate/apply schema migrations. Tested compatibility: GORM **v1.31.2**, GEN **v0.3.29**, dbresolver **v1.6.2**. GEN's default dbresolver v1.5.3 fails to compile against this GORM version (`undefined: gorm.Stmt`); keep the explicit v1.6.2 requirement. GEN transitively depends on other vendor drivers through upstream datatypes; these are not Cicadel database integrations and do not change the PostgreSQL-only contract. From this module: ```sh go run ./example/generate go test -race ./... CICADEL_TEST_DATABASE_URL='postgres://.../disposable_database' go test -race ./... ``` Regeneration tests compare every generated query file byte-for-byte with the committed example and run twice. PostgreSQL tests compile and execute typed create, filter and update queries. A separate temporary consumer module resolves this module through a local replacement, generates its own query package from the runtime catalog, then compiles and executes that exact output against PostgreSQL. It verifies create/filter/delete, nested transaction rollback and canceled contexts on a pinned connection with a transaction-local temporary table. This is local installation verification, not evidence of a published release. For local installation, require `github.com/fauzaanu/cicadel/contrib/querygen` at `v0.0.0`, replacing it with this directory and replacing the framework with the checkout. Run `go mod tidy`. No package release has been published yet. Standard projects run `go run manage.go generatequeries` offline from registered `ModelOf` declarations. The generated `query/` directory is reviewed and committed. Existing custom hosts can register `querygen.New(querygen.Config{Models: registry.Models(), OutputDirectory: "query"})` as an installed app. Run `querygen:generate` through the host's management entrypoint, which must dispatch build commands before opening a database. Explicit models work equally well without discovery. The command writes generated queries to a dedicated output directory. Review and commit generated files; remove obsolete generated files deliberately when removing models. Do not store authored code in that directory. Use `query.Use(db)` for instance-local query objects. The example deliberately avoids GEN's package-global default database. Runtime models remain authoritative. See the upstream [struct-based DAO configuration](https://gorm.io/gen/dao.html). Catalog integration: pass `Config{Apps: installedApps, OutputDirectory: "query"}` to consume app-owned `ModelOf[T]` declarations directly. `Apps` and `Models` are mutually exclusive. Ownership and declared tables are validated before output; tables must match GORM's default naming strategy, including `TableName` methods. The factory snapshots declaration slices. Generation remains offline, does not start apps, and never changes historical migration snapshots. Query model Go names must be unique across selected apps and exported/importable. --- Source: `docs/ai.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/intro/ai/index.md # Build with an AI agent Give your coding agent a task and the documentation index for your application's version. Use this page to pick the right index and copy a starting prompt. ## Choose the documentation version Pick a version with **Documentation version** on this page. The links and prompts below change to match it. | Version | Content | | --- | --- | | `dev` | Follows ongoing development | | Released versions | Frozen; match that release exactly | - [Application documentation index](https://cicadel.fauzaanu.com/en/0.2.7/llms.txt) links to focused Markdown guides. - [Complete application documentation](https://cicadel.fauzaanu.com/en/0.2.7/llms-full.txt) bundles every guide, for when the agent needs broader context. > [!TIP] > The root `/llms.txt` currently points to `dev`. Put the version-specific URL in > saved project instructions. ## Start an application ```text Use https://cicadel.fauzaanu.com/en/0.2.7/llms.txt as your Cicadel documentation index. Create a new application in ./myapp with module example.com/myapp. The application should: [describe the users, screens, data and permissions]. Read the relevant guides before choosing APIs. Show concrete Go and .templ code. Configure PostgreSQL for the standard accounts and recorded-migration workflow. Register apps explicitly, rebuild generated UI, and run checks and tests. Verify the resulting HTTP behavior and report the local URL and any remaining setup. ``` ## Add a feature to an existing application ```text Read https://cicadel.fauzaanu.com/en/0.2.7/llms.txt. First inspect this project's go.mod, registered apps, routes and existing UI. Add [feature] using the project's conventions and matching Cicadel version. Implement its handlers, components, validation and permissions. If the schema changes, generate and review a recorded PostgreSQL migration. Run the affected tests and the project's normal checks. Summarize the result. ``` ## Fix a bug ```text Use https://cicadel.fauzaanu.com/en/0.2.7/llms.txt and the relevant API guide. Reproduce this problem: [steps, expected result, actual result]. Find the failing request or data flow, add a regression test, and fix it. Preserve existing authorization, CSRF checks and migration history. Run the test against the real affected behavior and report what passed. ``` ## Give the agent useful context Tell the agent: - the application module path; - the screen or route to change; - the expected behavior; - who may access the data. > [!WARNING] > Supply secrets through your agent's secure mechanism. Never paste them into a prompt. For specialized work, point the agent at a skill. Skills are optional instructions for building applications and reusable packages. - [Component-authoring skill](https://cicadel.fauzaanu.com/en/0.2.7/sources/skills/cicadel-components/SKILL.md) - [Installable-app skill](https://cicadel.fauzaanu.com/en/0.2.7/sources/skills/cicadel-installables/SKILL.md) ## Framework design is separate Application work does not need the framework roadmap or implementation history. Those documents have a [separate framework index](https://cicadel.fauzaanu.com/en/0.2.7/framework/llms.txt). Read [the philosophy](https://cicadel.fauzaanu.com/en/0.2.7/internals/philosophy/index.md) only for the reasoning behind Cicadel's design. --- Source: `docs/api.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/ref/api/index.md # API index The source packages include Go documentation; inspect them with `go doc github.com/fauzaanu/cicadel/PACKAGE`. | Package | Main entry points | | --- | --- | | `cicadel` | `App`, `Manifest`, `Requirement`, `New`, `Start`/`Close`, `InstalledApps`, `Resources`, `Check`, `SystemChecks`, `SystemCheck`, `CheckMessage`, `Run`/`Serve`, `Health`, `App.Settings`, `Application.PackageSettings`, `AppSettings`, `App.Events`, `App.Subscriptions`, `Application.Context`, `Application.Events` | | `events` | `New[T]`, `Event[T].Emit`/`Handle`/`Name`/`Owner`, `Handler[T]`, `Declaration`, `Subscription`, `NewDispatcher`, `App`, `Dispatcher.Subscribers`, `WithDispatcher`, `FromContext`, `HandlerError`, `MaxDepth`, `ErrDepth`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/events/index.md) | | `settings` | `Defaults`, `Load`, `LoadWith`, `Settings.Validate`, `Settings.Env`; package settings `NewPackage[T]`, `Package.Resolve`/`Value`, `LoadPackage[T]`, `PackageSettings`, `Field`, `PackagePrefix`, `CoreKeys`, `Redacted`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/package-settings/index.md) | | `urls` | `Path`, `Include`, `New`, `Router.Reverse` | | `response` | `HTML`, `JSON`, `Redirect`, `Error`, `LocalPath` | | `errorpages` | `Middleware`, `Options`, `Error`, `New`, `Default`, `Reason`, `ReasonCSRF`, `Event`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/error-pages/index.md) | | `bodylimit` | `Apply`, `Set`, `Limit`, `ErrBodyRead`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/request-limits/index.md) | | `htmx` | `Script`, `Handler`, `HTML`, `IsRequest`, `IsPartial`, `Trigger`, `TriggerDetail`, `Redirect`, `Location`, `Refresh`, `Retarget`, `Reswap`, `Reselect`, `PushURL`, `ReplaceURL`, `CSRFHeaders`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/htmx/index.md) | | `ui` | `Definition[P,S]`, `Content`, `Text`, `Fragment`, `Group`, `Renderer`, `Merge`, `URL`, `Resolve`, `Diagnostic`, `Committed` | | `ui/layout`, `ui/form` | Minimal document (`layout.Props`: `Title`, `Stylesheet`, `Lang`, `Dir`, `CSRFToken`) and accessible field/CSRF components | | `uic` | `Compile`, `Generate`, `GoDiagnostics`; `cicadel ui generate/build/check`, `ui css install/build/check` | | `templates` | Deprecated experimental compatibility: `New`, `Engine.Render`, `Engine.HTML` | | `database` | `Open`, `Migrator`, `Migrate`, `Atomic`, `OnCommit`, `ErrNoTransaction`; [transactions](https://cicadel.fauzaanu.com/en/0.2.7/howto/transactions/index.md) | | `apptest` | `Check`: per-instance lifecycle smoke checks for app authors | | `management` | `Manager.Run`, `Manager.CommandNames`, `UsageError`, `ExitCode`, custom `Command` | | `forms` | `Parse`, legacy `Form.Required`/`Valid`; typed `New`, `Typed[T]`, built-in fields/validators, `Definition.Bind`, `Value[T]` | | `forms/upload` | `Parse`, `Middleware`, `FromRequest`, `Batch.Save`/`Close`, `CleanupError` | | `forms/model` | `New[T]`, `Config[T]`, `Field`, `Bind`, `Bound.Create`/`Update`, `Related[R]`, `InTransaction`, `Errors`; [model-form contract](https://cicadel.fauzaanu.com/en/0.2.7/topics/model-forms/index.md) | | `security` | `NewSessions`, `Login`, `Logout`, `CSRF`, `CSRFFailureMessage`, `RequireLogin`, password helpers | | `signing` | `Keys`, `NewKeyring`, `Keyring.Signer`, `For`, `Signer.Sign`/`Verify`/`VerifyNoExpiry`/`Issued`/`WithClock`, `SignJSON`, `VerifyJSON[T]`, `GenerateKey`, `ErrInvalid`, `ErrExpired`, `ErrNoKey`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/signing/index.md) | | `admin` | `Routes[T]`, explicit `Config[T]`, `Field`, permission `Action` | | `api` | `Decode`, `Pagination`, `Fail`, generic `List[T]` | | `static` | `URL`, `NewManifest`, `Manifest.URL`/`Handler`/`Collect`, `ParseURL`, `Handler` (single directory); [static files](https://cicadel.fauzaanu.com/en/0.2.7/howto/static-files/index.md) | | `paginate` | `Parse`, `Options`, `Request`, `Query[T]`, `Slice[T]`, `Page[T]` (`URL`, `Links`), `Nav`, `Navigation`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/pagination-and-messages/index.md) | | `flash` | `New`, `Messages.Add`/`Success`/`Info`/`Warning`/`Error`/`Pop`/`Peek`, `List`, `ErrNoSession`; [guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/pagination-and-messages/index.md) | | `observability` | `NewLogger`, `NewLogging`, `Logger`, `Middleware`, `RequestID`, `Event`, `OpenRotatingFile`, `NewEmailHandler`, `NewQueryLogger` | | `jobs` | `Migrate`, `NewProducer`, `InsertTx`, `NewWorker` | | `mail` | `Message.Bytes`, `Sender.Send`/`SendMany`, `SMTP`, `Transport`, `RenderComponent`, `Render`, `Capture`, `Console`, `File`, `Discard` | | `storage` | `Store`, `NewLocal`, `HTTP`, `ValidateKey` | | `cache` | `NewMemory`, `Get`, `Set`, `Delete` | The CLI is `go run ./cmd/cicadel startproject DIRECTORY MODULE`, `startapp NAME [DIRECTORY]`, or `startpackage DIRECTORY MODULE NAME`. See [installable apps](https://cicadel.fauzaanu.com/en/0.2.7/topics/installable-apps/index.md). The full workflow is in [the tutorial](https://cicadel.fauzaanu.com/en/0.2.7/intro/tutorial/index.md); integration boundaries and limitations are in [extensions](https://cicadel.fauzaanu.com/en/0.2.7/topics/extensions/index.md). Native i18n/l10n APIs are intentionally absent. See [the battery support policy](https://cicadel.fauzaanu.com/en/0.2.7/sources/batteries.md) for package-extension plans and current capability gaps. The approved UI architecture is [composable components](https://cicadel.fauzaanu.com/en/0.2.7/topics/component-ui/index.md). The compiler/runtime and typed library contracts are implemented; legacy template calls remain a separate compatibility API. Tailwind is the first-party default; see [CSS tooling](https://cicadel.fauzaanu.com/en/0.2.7/howto/tailwind/index.md) and the [tested component authoring guide](https://cicadel.fauzaanu.com/en/0.2.7/howto/component-authoring/index.md). The first-party [formsets module](https://cicadel.fauzaanu.com/en/0.2.7/ref/formsets/index.md) adds `New[T]`, `Inline[P,T]`, `Collection[T]`, `Bound.Save` and `App` for atomic collections. See [complete forms](https://cicadel.fauzaanu.com/en/0.2.7/topics/forms-completion/index.md) for widgets, revision tokens, uploads and atomic administration inlines. The first-party [accounts module](https://cicadel.fauzaanu.com/en/0.2.7/ref/accounts/index.md) exports `Service.Browser`, `Browser.HTMLApp`, `Browser.Middleware`, `Browser.Login`/`Logout`, `Browser.LoginRequired`, `Browser.RequireLogin`, and typed `accountui.Page`. See [authentication](https://cicadel.fauzaanu.com/en/0.2.7/topics/authentication/index.md), [email](https://cicadel.fauzaanu.com/en/0.2.7/topics/email/index.md) and [logging](https://cicadel.fauzaanu.com/en/0.2.7/topics/logging/index.md) for complete integration contracts. The [foundation guide](https://cicadel.fauzaanu.com/en/0.2.7/internals/foundation/index.md) documents `cicadel dev`, command help and structured deployment checks, configuration precedence and lifecycle contracts. --- Source: `docs/authentication.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/topics/authentication/index.md # Browser authentication and the accounts starter The accounts app signs users in with server-side sessions and CSRF checks. Every generated project includes it. ## How it fits together Cicadel has one core accounts service and two HTTP adapters: - **Browser adapter.** Server-rendered HTML, server-side SCS sessions and CSRF checks. - **JSON bearer adapter.** For API clients that send `Authorization: Bearer …`. Both adapters read the same PostgreSQL users, account sessions, recovery tokens, groups and permissions. Disabling an account, resetting its password or revoking its sessions affects both. A browser never receives the account bearer token in HTML or JSON. ## Ask your agent ```text Read https://cicadel.fauzaanu.com/en/0.2.7/topics/authentication/index.md. Add browser sign-in to my application using the accounts starter or adapter. Use PostgreSQL sessions, CSRF protection and verified recovery addresses. Show the registration and route protection code. Test sign-in, sign-out, and rejection of revoked sessions; keep secrets outside source code. ``` ## Create the starter From the framework checkout: ```sh go build -o bin/cicadel ./cmd/cicadel ./bin/cicadel startproject /tmp/myapp example.com/myapp cd /tmp/myapp go mod edit -replace=github.com/fauzaanu/cicadel=/absolute/path/to/Cicadel go mod edit -replace=github.com/fauzaanu/cicadel/contrib/accounts=/absolute/path/to/Cicadel/contrib/accounts go mod edit -replace=github.com/fauzaanu/cicadel/contrib/formsets=/absolute/path/to/Cicadel/contrib/formsets go mod edit -replace=github.com/fauzaanu/cicadel/contrib/migrations=/absolute/path/to/Cicadel/contrib/migrations go mod edit -replace=github.com/fauzaanu/cicadel/contrib/querygen=/absolute/path/to/Cicadel/contrib/querygen go mod tidy export DATABASE_URL='postgres://USER:PASSWORD@localhost/myapp?sslmode=disable' export CICADEL_DEBUG=true export CICADEL_SECURE_COOKIES=false go run manage.go migrate CICADEL_USER_PASSWORD='a local account password' go run manage.go createuser owner@example.com go run manage.go runserver ``` `startproject` always includes accounts, formsets and the standard migration workflow. Runtime commands need PostgreSQL. The accounts module ships with the framework at the same version. Keep all core modules on one version, and commit your resolved Go dependencies. ### Who can have an account - The stored username is the account's verified recovery address. Create accounts only for email addresses whose ownership you have verified. - Requests cannot supply a separate delivery address. - There is no public registration endpoint and no administrator panel. - Use the service's `Create` API for your own provisioning and invitation workflows. ## Screens and actions The default prefix is `/accounts`. Change it with `accounts.Pages.Prefix`. | Route | GET | POST | | --- | --- | --- | | `/accounts` | Signed-in account and session actions | — | | `/accounts/login` | Login form | Rotate session and sign in | | `/accounts/recover` | Recovery form | Deliver a reset link to the verified destination | | `/accounts/reset` | Reset form with token | Consume the token once and replace the password | | `/accounts/change-password` | Current/new password form | Replace password and revoke all sessions | | `/accounts/logout` | — | Revoke current account session and destroy its cookie session | | `/accounts/revoke` | — | Revoke all account sessions and sign out | | `/static/accounts/app..css` | Embedded compiled stylesheet, published through `App.Static` | — | To customize the screens, compose your own pages with `contrib/accounts/accountui`. ## Integrate an existing application Create the service and the browser adapter in your project setup. Then register the returned app: ```go service, err := accounts.New(accounts.Options{ DB: db, DeliverRecovery: DeliverRecovery, }) if err != nil { return err } browser, err := service.Browser(security.NewSessions(store, true)) if err != nil { return err } authApp, err := browser.HTMLApp(accounts.Pages{ Brand: "My application", Logger: logger, }) if err != nil { return err } installed = append(installed, authApp) ``` This fragment uses your PostgreSQL `db`, shared session `store`, logger, recovery callback and existing `[]cicadel.App`. Pass `installed` to the project's `cicadel.New` call. 1. Register the adapter once. 2. Run `migrate` before serving. The generated starter has the full setup in `project/application.go`. ## Protect a view Send anonymous visitors to the sign-in page, like Django's `login_required`. Install `browser.Middleware` and `browser.LoginRequired` on the app that owns the views: ```go notes := cicadel.App{ Name: "notes", Middleware: []cicadel.Middleware{ browser.Middleware, browser.LoginRequired("/accounts/login"), }, Routes: []urls.Route{ urls.Path("GET", "/notes/{id}", "notes:detail", detail), }, } ``` An anonymous `GET /notes/42` is redirected to `/accounts/login?next=%2Fnotes%2F42`. The sign-in page carries `next` in its form and returns there after a successful sign-in. - Unsafe requests (POST, DELETE …) are redirected without `next`, so signing in never repeats an action. - htmx requests get `HX-Redirect`, so the whole page navigates. `next` is the page the request came from (`HX-Current-URL`) when it is on the same host. - `next` is only ever a local path. The sign-in page ignores `//evil.test`, `https://…`, backslashes and control characters, and returns to the account page instead. - `LoginRequired` panics at startup when its login URL is not a local path. For JSON routes, use `browser.RequireLogin` instead. It answers 401. - Apply `browser.Middleware` first. It loads the session, checks CSRF and resolves the user. - Read the user with `accounts.UserFromContext` inside handlers. - Both guards check a freshly resolved account, not only a stored SCS user ID. > [!WARNING] > Login is not authorization. Object permissions still need explicit application > policy and scoped database queries. Check them inside the handler. `HTMLApp` already applies LoadAndSave → CSRF → account resolution to its own routes. Do not wrap those routes with the same middleware again. ## Sessions - The starter stores sessions in the PostgreSQL table `accounts_browser_sessions`, using SCS's configurable table name. You can supply a custom store instead. - Migration 002 adds that table without rewriting existing accounts history. - Sessions are HttpOnly, SameSite=Lax and secure by default. - Sessions rotate on login and clear at logout. - Account expiry and revocation are checked on every browser request. - SCS expiry can end a session before its account token expires. `accounts:cleanup` removes expired tokens. - Every unsafe method requires CSRF. - Bodies under `browser.Middleware` keep the application's limit, `settings.MaxBodyBytes`, or a route's own limit (see [Limit request bodies](https://cicadel.fauzaanu.com/en/0.2.7/howto/request-limits/index.md)). The account pages bound their own forms to 8 KiB. - htmx requests send the CSRF token as a header when the page sets `layout.Props.CSRFToken` (see [htmx](https://cicadel.fauzaanu.com/en/0.2.7/howto/htmx/index.md#csrf-for-htmx-requests)). ## API clients For API-only clients, use `service.HTTPApp(prefix)` and `service.Middleware` with an explicit `Authorization: Bearer …` header. Register either adapter once, under the `accounts` app name. If a host needs both, combine their routes into that one app. Scope the cookie middleware to the HTML routes. > [!WARNING] > Never apply cookie CSRF middleware to a bearer-only API by accident. ## React to account changes The accounts service emits typed events such as `accounts.UserCreated`, `accounts.LoggedIn` and `accounts.PasswordChanged`. See [accounts events](https://cicadel.fauzaanu.com/en/0.2.7/howto/events/index.md#accounts-events). ## Recovery delivery `project.DeliverRecovery` is the application-owned callback that sends reset links. It: - sends to the stored verified address; - builds a local reset route from the configured origin; - passes the token only to the mail transport. Recovery reveals nothing about which usernames exist: - Known and unknown users get the same recovery response. - Missing delivery configuration is handled without revealing whether a username exists. - Delivery errors are logged without tokens, addresses or server response text. Passwords must be 12–72 bytes. Applications may add their own password policy. Configure these starter environment variables: | Variable | Purpose | | --- | --- | | `CICADEL_PUBLIC_URL` | HTTPS origin used to construct reset links | | `CICADEL_MAIL_FROM` | Sender mailbox | | `CICADEL_SMTP_ADDRESS` | SMTP `host:port` with STARTTLS | | `CICADEL_SMTP_USERNAME`, `CICADEL_SMTP_PASSWORD` | Optional SMTP credentials | | `CICADEL_LOG_FORMAT`, `CICADEL_LOG_LEVEL` | JSON/text format and log threshold | ## Deploy 1. Use HTTPS and secure cookies. 2. Configure allowed hosts. 3. Apply gateway rate limits to the login and recovery endpoints. 4. Schedule `manage.go accounts:cleanup`. 5. Do not log query strings on reset routes at the proxy. Reset pages send `no-referrer`, `no-store` and a restrictive content security policy. See [email](https://cicadel.fauzaanu.com/en/0.2.7/topics/email/index.md), [logging](https://cicadel.fauzaanu.com/en/0.2.7/topics/logging/index.md) and [operations](https://cicadel.fauzaanu.com/en/0.2.7/howto/operations/index.md) for configuration details. --- Source: `docs/cache.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/topics/cache/index.md # Cache application data The `cache` package stores expensive results as bytes for a limited time. Like Django's `CACHES`, you name each cache and pick a backend for it. `cache.Backend` is the context-aware interface that backends implement. You can replace the backend without changing the code that uses it. ## Cache a value ```go backend, err := cache.NewMemoryBackend(1000, 64*1024) if err != nil { return err } registry, err := cache.NewRegistry(map[string]cache.Alias{ "articles": {Backend: backend, Namespace: "production:blog", Version: "v1"}, }) if err != nil { return err } articles, err := registry.Lookup("articles") if err != nil { return err } err = articles.Set(ctx, "public:42", []byte("cached payload"), time.Minute) ``` Build the registry at startup. Then inject the clients into your apps. ## Misses and errors | Result | Meaning | | --- | --- | | `(nil, false, nil)` | A miss. | | An error | An outage. Check it with `errors.Is`. | On an error, you choose whether to fall back to the authoritative data source. Cicadel never decides that for you. > [!WARNING] > Never use cache contents for session revocation, authorization decisions or > distributed locking. ## The memory backend `NewMemoryBackend(capacity, maxValueBytes)` is a bounded, concurrent backend that lives in one process. - It copies values in and out. - Entries expire at the TTL boundary. - When full, it evicts the oldest written entry. - The existing `Memory` and `Store` APIs remain compatible. Each instance is local to one application process. It does not coordinate with other processes. Use a suitable shared backend when several instances must share entries. ## Keys The registry copies the alias map. Each alias hashes its name, namespace and version together with your key into a length-framed SHA-256 key. - Raw keys never reach the backend. - Keys must be 1–1024 bytes. - TTLs must be positive. - Hashing does not encrypt values. Put every variation into the logical key: tenant, locale, permission and representation, where they apply. ## Invalidate entries - `Delete` removes one logical key. Deleting a missing key succeeds. - Changing `Version` selects a fresh key space. Old entries expire on their own. Every process must adopt the new version to agree on the active generation. - There is deliberately no global flush. Readers and writers in flight can race with a delete. Invalidation is not transactional. ## Write a backend A backend must: - be safe for concurrent use; - preserve byte ownership; - implement positive TTLs; - report operational errors. Cancellation can race with a completed write. An error does not prove the change was rolled back. The built-in backend checks cancellation before its short in-memory operation. Network backends should pass the context to their transport. ## Ask your agent ```text Read https://cicadel.fauzaanu.com/en/0.2.7/topics/cache/index.md. Cache [expensive read] in this application. Include tenant and permission variation in the key, set a TTL, define invalidation after writes, and keep the database authoritative. Test hits, misses and backend failures. ``` ## Coalescing expensive loads Create one `cache.NewLoader(client)` and reuse it. Then call `loader.Get(ctx, key, ttl, load)`. The `load` function takes a context and returns `([]byte, error)`. - Hits skip the function. - Concurrent misses for the same key share the first caller's function and TTL. Different keys load independently. - The loader rechecks the cache before loading, which closes the miss/join race. - Successful loads fill the cache. Load and backend failures propagate, and callers can retry. - Every caller gets its own copy of the result. ### Cancellation Cancelling a caller stops only that caller's wait. The load keeps the first caller's context values. It ends when every waiter cancels or the work finishes. ### Rules for load functions - Honor cancellation. - Never call back into the same loader and key. - Use keys that include every authorization and tenant variation. - Keep one load policy per loader. Coordination is local to one loader instance, not across processes. Deletes and concurrent writes keep the best-effort consistency described above. ## Bounded load execution For request-serving apps, cap concurrent loads and give each one a deadline: ```go loader := cache.NewBoundedLoader(client, 32, 5*time.Second) ``` Both limits must be positive. - Hits still work at capacity. - Callers that join an existing load share its slot. - New misses at capacity get `cache.ErrLoaderBusy` immediately. Check it with `errors.Is` and choose your own retry or fallback. The deadline covers the backend recheck, the load and the write. Functions and backends must cooperate with cancellation. An abandoned load keeps its slot until it actually returns. This stops repeated cancelled requests from starting unlimited replacement goroutines. > [!WARNING] > A load function that ignores cancellation can use up every slot. Go cannot force > it to stop. `NewLoader` stays compatible and unbounded. Neither constructor provides distributed admission control or transactional invalidation. Race-tested evidence: - `TestBoundedLoaderAdmissionAndAbandonedExecution` checks capacity rejection, abandoned-write prevention and slot reuse. - `TestBoundedLoaderDeadline` checks deadline propagation and limit validation. --- Source: `docs/component-authoring.md` Markdown: https://cicadel.fauzaanu.com/en/0.2.7/howto/component-authoring/index.md # Build a page with components Components are Cicadel's replacement for Django templates. This walkthrough builds a card component and a list page, then wires up the view, URL and tests. Every snippet below is real code. `TestComponentAuthoringGuide` copies each file into a generated project, then compiles and renders it. See also: - [Component UI](https://cicadel.fauzaanu.com/en/0.2.7/topics/component-ui/index.md): the full API contract. - [Tailwind CSS](https://cicadel.fauzaanu.com/en/0.2.7/howto/tailwind/index.md): styling. - [`$cicadel-components`](https://cicadel.fauzaanu.com/en/0.2.7/sources/skills/cicadel-components/SKILL.md): the reusable agent skill. ## Ask your agent ```text Read https://cicadel.fauzaanu.com/en/0.2.7/howto/component-authoring/index.md. Add [screen] to my application using typed Cicadel components. Inspect the current app structure, compose the page from explicit props and slots, and connect its handler and route. Build the UI and test its rendered output. ``` ## Translate what you know Think "props down, markup composed, HTML returned." Cicadel borrows React's composition model, not its JavaScript runtime. | React habit | Cicadel equivalent | | --- | --- | | Function component with typed props | Private `.templ` function; public `ui.Definition[Props, Slots]` contract | | `…` | `@Card.Call(CardProps{Title: "Hello"}, CardSlots{Children: ...})` | | `children` / a ReactNode prop | `ui.Content` fields named `Children`, `Footer`, etc. | | Fragment `<>…` | Private markup helper via `ui.Fragment(helper(...))`, or `ui.Group(...)` | | `{title}` | `{ p.Title }` (text escapes automatically) | | `{items.map(...)}` | A multiline Go `for` block | | Conditional JSX | A multiline Go `if` or `switch` block | | Default/destructured props | Go zero values plus a pure `Defaults` function | | `className`, `htmlFor` | HTML `class`, `for` | | Boolean HTML prop | `disabled?={ p.Disabled }` | | Utility classes | Tailwind v4, complete class strings in source | | Context provider / hook state | Explicit typed inputs; handlers load data and process POSTs | | `onClick` / hydration | Not part of this server engine; use links/forms for server interactions | | `key` | No reconciliation runtime, so no key prop; keep DOM IDs unique for labels | Do not add JSX, hooks, npm or a browser CDN script to write components. Tailwind runs from a pinned standalone build tool. ## Set up the project 1. Generate a project with the module path `example.com/componentguide`. 2. Run `go mod tidy`. 3. Install the CSS compiler once with `go tool cicadel ui css install`. You add the files below to an ordinary local Go app. You do not need a new module. ## 1. Define the contract The contract declares the props, slots, defaults and validation. ### apps/dashboard/card.go ```go package dashboard import ( "fmt" "github.com/fauzaanu/cicadel/ui" ) type CardProps struct { Title string Tone string } type CardSlots struct { Children, Footer ui.Content } var Card = ui.Definition[CardProps, CardSlots]{ Name: "dashboard:card", Source: "apps/dashboard/card.templ", Defaults: func(p CardProps) CardProps { if p.Tone == "" { p.Tone = "neutral" } return p }, Validate: func(p CardProps, s CardSlots) error { if err := ui.Required("Title", p.Title); err != nil { return err } if s.Children.Empty() { return fmt.Errorf("Children slot is required") } if p.Tone != "neutral" && p.Tone != "success" { return fmt.Errorf("unknown Tone %q", p.Tone) } return nil }, Body: card, } func cardClasses(tone string) string { if tone == "success" { return "rounded-xl border border-emerald-300 bg-emerald-50 p-6" } return "rounded-xl border border-slate-200 bg-white p-6" } ``` The rules this contract sets: - An empty `Tone` defaults to `neutral`. - An empty `Title` or a missing `Children` slot is an error. - `Footer` is optional. Zero and `false` are not always "missing". Use pointer props only when an omitted value must differ from a supplied zero or `false`. > [!WARNING] > `Defaults` must not mutate objects referenced by caller-owned maps, slices or pointers. ## 2. Write the markup ### apps/dashboard/card.templ ```templ package dashboard templ card(p CardProps, s CardSlots) {

{ p.Title }

@s.Children
if !s.Footer.Empty() {
@s.Footer
}
} ``` Two kinds of insertion, on purpose: | Syntax | Inserts | | --- | --- | | `{ p.Title }` | Escaped text. | | `@s.Children` | A typed HTML fragment. | Write complete utility class names in Go variants, as `cardClasses` does. Tailwind cannot evaluate `"bg-" + color + "-50"`. ## 3. Compose the page ### apps/dashboard/page.templ ```templ package dashboard import "github.com/fauzaanu/cicadel/ui" import "github.com/fauzaanu/cicadel/ui/layout" templ itemFooter() { Back to home } templ dashboardBody(rows []Item) {

Dashboard

if len(rows) == 0 {

No items yet.

} for _, row := range rows { @Card.Call(CardProps{Title: row.Title, Tone: row.Tone}, CardSlots{ Children: ui.Text(row.Description), Footer: ui.Fragment(itemFooter()), }) }
} templ dashboardPage(rows []Item, stylesheet string) { @layout.Page.Call(layout.Props{Title: "Dashboard", Stylesheet: stylesheet}, layout.Slots{Children: ui.Fragment(dashboardBody(rows))}) } ``` - Private helpers such as `itemFooter` need no public definition. Add one only for a reusable, validated contract. - `ui.Text` wraps request text and escapes it. - `ui.Fragment` adapts compiled markup. Never pass it strings. - `ui.Group` combines fragments that are already typed. ## 4. Add the view and URL ### apps/dashboard/app.go ```go package dashboard import ( "context" "log/slog" "net/http" "github.com/fauzaanu/cicadel" "github.com/fauzaanu/cicadel/response" "github.com/fauzaanu/cicadel/static" "github.com/fauzaanu/cicadel/ui" "github.com/fauzaanu/cicadel/urls" ) type Item struct { Title, Description, Tone string } type LoadItems func(context.Context) ([]Item, error) func New(load LoadItems) cicadel.App { return cicadel.App{ Name: "dashboard", Components: []ui.Export{Card.Export(1)}, Routes: []urls.Route{urls.Path("GET", "/dashboard", "dashboard:index", func(w http.ResponseWriter, r *http.Request) { rows, err := load(r.Context()) if err != nil { response.Error(w, 500); return } stylesheet, err := static.URL(r.Context(), "project", "app.css") if err != nil { response.Error(w, 500); return } err = (ui.Renderer{}).HTML(r.Context(), w, 200, ui.Fragment(dashboardPage(rows, stylesheet))) if err != nil { slog.ErrorContext(r.Context(), "render dashboard", "error", err) if !ui.Committed(err) { response.Error(w, 500) } } }), }, } } ``` - The loader is explicit. It can query through your application's database. - The component never discovers a request-wide map. - `static.URL` resolves the project's compiled stylesheet to its content-hashed URL. The handler passes it in as a prop. See [static files](https://cicadel.fauzaanu.com/en/0.2.7/howto/static-files/index.md). - Keep permissions in the handler and its query scope. ### Register the app: project/apps.go For the runnable demo, replace the starter's empty app list with this loader: ```go package project import ( "context" "example.com/componentguide/apps/dashboard" "github.com/fauzaanu/cicadel" "gorm.io/gorm" ) func InstalledApps(_ *gorm.DB) ([]cicadel.App, error) { return []cicadel.App{dashboard.New(func(ctx context.Context) ([]dashboard.Item, error) { return []dashboard.Item{{Title: "Ready", Description: "Typed props and slots work.", Tone: "success"}}, ctx.Err() })}, nil } ``` In your own project, use your module's import path. A direct call to `Card` uses that definition. For an overridable library, resolve it through `ComponentRegistry` and inject the result into the page factory. See [the override recipe](https://cicadel.fauzaanu.com/en/0.2.7/sources/skills/cicadel-components/references/patterns.md#libraries-and-project-overrides). ## 5. Test the behavior ### apps/dashboard/app_test.go ```go package dashboard import ( "context" "strings" "testing" "testing/fstest" "github.com/fauzaanu/cicadel" "github.com/fauzaanu/cicadel/apptest" "github.com/fauzaanu/cicadel/ui" ) func TestDashboard(t *testing.T) { attack := `` project := cicadel.App{Name: "project", Static: fstest.MapFS{"app.css": {Data: []byte("body{}")}}} app := apptest.New(t, apptest.Settings(), []cicadel.App{project, New(func(context.Context) ([]Item, error) { return []Item{{Title: attack, Description: attack}}, nil }), }) stylesheet, err := app.Static().URL("project", "app.css") if err != nil { t.Fatal(err) } res := apptest.NewClient(app).Get(t, "/dashboard").Status(t, 200) res.Contains(t, `rel="stylesheet"`).Contains(t, `href="`+stylesheet+`"`) res.Contains(t, "Back to home").Contains(t, "border-slate-200") if strings.Contains(string(res.Body), "