Caramel today.
What to do when you upgrade an application to Caramel 0.16.0, release by release since 0.8.0, and what the release supports and still lacks.
| Contract | Value |
|---|---|
| Release | 0.16.0 |
| Covers | Upgrade notes from 0.8.0 to 0.16.0, what the release supports and its current limits |
| Needs | An application whose shard.lock pins an earlier release, and the machine setup of your first application |
| Proved by | frappe check · frappe corretto · frappe lint |
The guide in three stages
21 sections · read in order the first time
- 1 · Upgrade notes
- 2 · Supported today
- 3 · Limits
Each upgrade section is written from the release’s entry in CHANGELOG.md ↗: what to run, what to edit, and what changes without an edit. Upgrade one release at a time and run the proof at the end after each.
Upgrade notes
Upgrading to 0.16.0
Upgrade Caramel and Frappé together. There are no framework migrations and nothing is removed; the toolchain changes, and these changes can need an edit.
frappe db diffkeeps an index or foreign key whose name SugarORM did not generate and prints a note, where it dropped every undeclared one. A changed index is built under a temporary_swapname, then the old index is dropped and the new one renamed, so a unique index is never missing.belongs_totakeson_delete: :cascade,:restrictor:set_null.frappe migratecommits each transactional migration with its own journal row instead of one shared transaction, so a failure no longer rolls back the migrations before it. A migration that hitslock_timeoutrolls back and runs again after 1, 2 and 4 s; a fourth timeout stopsmigratewithMigrator::LockTimedOut, which names the migration.CARAMEL_POOL_SIZE(1 to 32) andCARAMEL_POOL_TIMEOUT(1 to 300 seconds) set the connection pool of every command that opens one, which was fixed at 4 connections and a 5 s checkout wait. An invalid value exits 1 with one line naming it.- Applications can use
BigDecimal,BigIntandBigRationalin every managed build: the toolchain now bundles GMP 6.3.0, andfrappe buildlinks it statically. Remove any@[Link(ldflags: …)]onLibGMPand any vendoredlibgmp.aan application added for this. - The toolchain changed, so installing this release sets up a new toolchain directory and its first builds start with an empty compiler cache. If you use the editor tools, run
frappe lsp installagain after installing.
Upgrading to 0.15.0
Upgrade Caramel and Frappé together. Run frappe migrate once for a framework migration; these changes can need an edit.
- Every database connection sets
statement_timeoutandlock_timeoutwhen it opens, and the client’s read timeout follows the statement timeout, 5 seconds behind it.serve,work,seed,lintanddriftdefault to 30 s and 5 s;migratedefaults to no statement timeout and 5 s. A statement that ran longer than 30 s now fails withcanceling statement due to statement timeout. RaiseCARAMEL_STATEMENT_TIMEOUTorCARAMEL_LOCK_TIMEOUTfor a process that needs longer, andCARAMEL_MIGRATION_STATEMENT_TIMEOUTorCARAMEL_MIGRATION_LOCK_TIMEOUTformigrate: whole seconds, 0 to 86400, 0 for none. The Crema reference lists the variables → - A job whose process dies or loses its connection is failed on its third release instead of being redelivered forever: maintenance records
Caramel::ColdBrew::AbandonedJob, calls theon_failedhooks, andjobs retryruns it again. Released runs no longer count againstretry_onattempts. - The framework migration
20260927000006addscaramel_jobs.releasesand takes a lock on everycaramel_jobspartition. Runfrappe migratewhile no job is running, ormigrate’s 5 s lock timeout may refuse it;CARAMEL_MIGRATION_LOCK_TIMEOUTraises it.
Upgrading to 0.14.0
Upgrade Caramel and Frappé together. There are no framework migrations; an application with a SweetTalk domain adds one of its own, and these changes can need an edit.
- With
caramel/sweet_talk, aSweetTalk::Refusalthat an action’shandledoes not rescue answers 422 with its message under_base, shaped like a failed contract:{"errors": {"_base": [message]}}for JSON clients, the contract failure page for browsers and htmx, and text startingERR INVALID:422for other clients. ASweetTalk::Missinganswers 404. Neither is reported to Crema as an error. An action that rescues them itself is unchanged, andSweetTalk::Boundarystill answers 500. Asking an aggregate for its id beforecreate, or an event for its subject before it is emitted, raisesNilAssertionErrorinstead ofSweetTalk::Missing. - A contract field may take a SweetTalk value or an aggregate’s id:
field quantity : Ordering::Quantitybinds as anInt32, and the value’s refusal is the field’s error.CARAMEL_FIELD_<NAME>constants gain a sixth element, the declared type, so a macro of your own that destructures exactly five must take six. - A lifecycle’s
statecolumn has aCHECKconstraint namedcheck_<table>_state_<8 hex digits>, and a state of two or more words is stored asmovesspells it. 0.13.0 storedawaiting_paymentasawaitingpayment: first add a migration that respells such rows, such asUPDATE ordering_payments SET state = 'awaiting_payment' WHERE state = 'awaitingpayment', and migrate, then runfrappe db diff, which adds each constraint asNOT VALIDand validates it in the_concurrentlymigration.frappe migratestops, naming the constraint, while a row holds any other text; the constraint staysNOT VALIDuntil you fix those rows and migrate again. A lifecycle whose constraint name would pass PostgreSQL’s 63 bytes no longer compiles; give the context or the aggregate a shorter name, sincerenamed_from:keeps the old table and its long name. - A
findsthat names an unknown attribute or state, or a list of entities, fails to compile even when nothing calls it. Code outside an aggregate can no longer call__sweettalk_rows; give the aggregate afindswith a block instead. - Calling another context’s service, as in
Catalogue.price_of(tea), counts as namingCatalogue::: outsideborder Catalogueit compiles only where the context follows Catalogue. Move the call into the border, or declarefollows: Catalogue. In a border the call draws a dependency edge, so two contexts whose borders call each other no longer compile: let one of them react to the other’s events instead. A context namesCaramel::OutboundorHTTP::Clientonly inside a border with an outside system,border Cards, outside: "…" do … end. - Additive, nothing to edit:
specificationnames a rule once,findsandcountssort, limit and count, a context names itssubdomain:and what itpublishes. The SweetTalk guide lists them → frappe agent-manifestprints aGUIDES:line afterDOCS:, so a tool that reads the manifest by line number finds the commands one line later.- For applications made earlier: to give agents the guides in Markdown, add
Every page of it in Markdown: https://caramelize.dev/docs/VERSION/llms.txtafter the documentation link inAGENTS.md. An application with a domain gets the SweetTalk skill with its nextfrappe make domain, or now withmkdir -p .claude/skills/sweettalk && cp lib/caramel/templates/skills/sweettalk/SKILL.md .claude/skills/sweettalk/, which also refreshes it after a later upgrade; addDomain rules: .claude/skills/sweettalk/SKILL.md, which frappe make domain writesafterDomain:inAGENTS.md.
Upgrading to 0.13.0
Upgrade Caramel and Frappé together. There are no framework migrations and nothing is removed; one call stops compiling, and these changes can need an edit.
- Keyword
whereno longer takes a value that may be nil, such as aString?variable, because nil matched the rows whose column IS NULL. The call fails to compile with…MayBeNilin the error. Narrow the value first (if token = contract.token), or use the newwhere_null(:column); the literalwhere(column: nil)still works. - A request may send at most 4,096 values and 128 files, and a multipart body’s boundaries and part headers count toward its size; more answers 413. Multipart files are stored only for a route whose contract has an
UploadedFilefield. - A Crema sink or exporter that matches
Caramel::Crema::SpanKindexhaustively must handle the newdomainkind. sudo scripts/install-local-integration applyinstalls only a bundle that the user who ransudoprepared, and refuses to run as root withoutsudo. ACARAMEL_LATTE_SWIFT_CACHEdirectory must be yours alone.- SweetTalk arrives, experimental, and nothing changes until an application requires
caramel/sweet_talk.frappe make domain NAME CONTEXT --means=TEXTstarts a domain; a multi-tenant application names its tenant withtenant App::Accountbefore its first context. Applications generated before this release add theLint/UnusedArgumentexclusion forapp/domain/**to their.ameba.ymlby hand, aslib/caramel/templates/application/.ameba.ymlhas it.
Upgrading to 0.12.0
Upgrade Caramel and Frappé together. There are no framework migrations and nothing is removed; one change can need an edit.
- On SIGTERM,
servenow finishes in-flight requests for up to 25 s before it exits, and an open server-sent events stream holds it for the whole 25 s. Give a supervisor that stopsserveat least 30 s. - Applications can run in production on Linux:
frappe buildwrites a static linux/amd64 artifact,servereads public files fromCARAMEL_ROOTand runs without workers with--no-workers,APP jobs unknownlists queued jobs a binary cannot run andAPP db rolesprints the role script. Nothing to edit. The Production guide puts them in order →
Upgrading to 0.11.0
Upgrade Caramel and Frappé together. There are no framework migrations and nothing is removed; these changes can need an edit.
- The schema document
frappe db diffreads is now version 3, because it carries CHECK constraints. - SugarORM owns the CHECK constraints named
check_….frappe db diffdrops acheck_…constraint that no schema declares; any other CHECK constraint stays and is noted. Declare a hand-writtencheck_…constraint, or rename it, before you diff. - Two schemas that name one table no longer compile. Remove the duplicate or give one a different table.
- Run
shards updateso your application takes crystal-db 0.15.0 and crystal-pg 0.31.0. - After a job is renamed or moved, name its old class with
renamed_from "Old::Name"in the job.workandmigratenow log a warning naming queued class names that no job claims.
Upgrading to 0.10.0
Upgrade Caramel and Frappé together. There are no migrations and no breaking changes; the change is in the development tools.
- The inspector’s request page shows each query as a card instead of a table row: the statement with one clause per line and coloured, and each bind listed beside its
$n. Pointing at a placeholder or a bind lights up both. Copy SQL and Copy with values still write the statement as it ran.
Upgrading to 0.9.0
Upgrade Caramel and Frappé together. There are no migrations and no breaking changes; the changes are in the development tools.
- The development toolbar is redesigned: its route, status, time, query count and flags link to their place on the request’s page in the inspector,
`,jandkwork from the keyboard, and Copy for an agent copies the request as Markdown. - Each query on the inspector’s request page has Copy SQL and Copy with values. A query keeps its first 20 binds, and a literal over 2,000 bytes is not kept for copying: those placeholders stay as
$nand the copy starts with a comment saying so. Anilbind is now recorded asNULLand shown bare in the Binds column; the textNULLstays quoted. - The toolbar, the inspector and the production console follow the system’s light or dark appearance, with a Theme button to choose. The console keeps its own choice.
- The application binary’s
opscommands now refuse a flag they would have ignored instead of dropping it silently.
Upgrading to 0.8.0
Upgrade Caramel and Frappé together. The schema document frappe db diff reads is now version 2, and SugarORM::Catalog::ForeignKey holds columns and references_columns so a key can span several columns. Two framework migrations and these edits apply.
- Run
frappe migrate. Two framework migrations addcaramel_jobs.contextandcaramel_metrics. - Run
latte stoponce if Latte is running, so the next command starts the new one. Frappé, Corretto and Latte.app now use Latte’s control API 2. The managed PostgreSQL restarts once to preloadpg_stat_statementsandauto_explain. - To keep per-minute aggregates, add
require "caramel/crema/recorder"afterrequire "caramel"inconfig/application.cr. New applications already have it. - Add
Caramel/Dumpto an existing application’s.ameba.yml, as new applications have it, sofrappe lintreports a leftoverdump. - Logging now sets itself up: JSON in production, text in development and test. The old
error_typelog line is gone; theerrorline carrieserror_classandfingerprint.Caramel::DevelopmentError.responsenow takes aCaramel::Crema::ErrorReport;Caramel::Database::Configgainsapplication_name, andSugarORM::Repothe private hooksobserveandobserve_checkout, whichcaramel/crema/sqlredefines. - To adopt the agent files, copy
AGENTS.mdandCLAUDE.mdfromlib/caramel/templates/application; existing applications keep their own. - Opt in as you need:
require "caramel/crema/otlp"exports traces to the endpointOTEL_EXPORTER_OTLP_ENDPOINTnames; the ops socket opens besideserve, and forworkwhenCARAMEL_OPS_SOCKETnames a path;frappe logs accessprints the site’s access log;frappe make tenancy MODELmakes the application multi-tenant.
Earlier releases
The notes start at 0.8.0, the release that first put its upgrade steps on this page. Each release’s guides stay published at /docs/VERSION/ from 0.4.0 on, and CHANGELOG.md holds every release’s notes. The ones before 0.8.0 that still need an edit: 0.7.0 changes html lang: "en" in app/views/layouts/application.cr to html lang: Caramel.language and replaces Caramel::Application::EXPIRED_FORM with Caramel::Wording.expired_form; 0.6.0 adds Lexbor 3.6.4 under development_dependencies; 0.5.0 binds JSON bodies through the contract and gives specs a committed .env.test; 0.4.2 pins Crystal 1.21.1.
Supported today
Runtime and local tools
Caramel is a source-built Crystal framework with a managed toolchain; the versions it pins and the machine it develops on are stated once, in Your first application. Frappé creates projects, generates resources and supervises builds; Latte supplies managed services and named HTTPS. An application’s shard.lock selects its framework release, and Frappé dispatches to the matching installation; frappe installations install VERSION installs another release beside this one.
Caramel 0.16.0 source release ↗
Requests and responses
Typed action contracts bind route and query values, URL-encoded forms, multipart uploads, and JSON objects. The default policy checks CSRF for writes and limits ordinary request input to 2 MiB; multipart uploads have a separate 64 MiB budget. Use per-action ingress for a custom limit, authenticated APIs, or exact raw webhook bytes.
Actions return HTML or JSON according to the request. render_errors handles errors found after contract binding. Blueprint views write typed HTML with escaped text and attributes; htmx partial responses update selected page regions.
Data and resource generation
SugarORM provides typed schemas, codecs for any field type, changesets, queries, explicit preloads, row locks, upserts, version columns and CHECK constraints. Derive a migration with frappe db diff, review it, and apply it with frappe migrate; frappe seed runs db/seeds.cr. frappe make resource generates application-owned models, changesets, actions, views, routes, migrations, and request specs.
Use :server for fields populated by the action, :unique for unique indexes and constraint checks, and :url for URL validation and inputs. --only=create,show generates a smaller action set; create and show are required, and edit needs new and update.
Background work
Cold Brew stores jobs in PostgreSQL and supports transactional enqueueing, retry policies, durable status, retry and failure hooks, renamed jobs, recurring schedules, PubSub, and cache. A job whose process died on three of its runs fails as Caramel::ColdBrew::AbandonedJob and can be retried. The application binary runs queues through serve or a worker-only work process with queue and concurrency options. External calls can repeat, so use a stable identifier for deduplication.
Read the Cold Brew reference →
Development and testing
Corretto drives in-process application requests against isolated test databases. Its client sends forms, multipart uploads, JSON, or exact raw bodies. Blueprint-shaped have_html expectations inspect decoded rendered HTML; render_partial can check an envelope’s target, swap, and content together.
Generated applications pin Lexbor 3.6.4 under development dependencies. Development installs need cc, ar, and network access for the verified native parser source. Production installs omit the parser. Put application-owned test settings in committed .env.test; development .env does not reach specs.
Every page of these guides has a Markdown edition at its address followed by index.md, listed in https://caramelize.dev/docs/0.16.0/llms.txt, and frappe agent-manifest prints that address on its GUIDES: line.
Internationalization
Caramel has opt-in, typed translations. Run frappe make locale CODE to add Crystal catalogs and enable translated resource generation. Views and actions use t for messages and l for number and time formatting. Language selection supports a switch, optional URL prefixes, cookies, and browser preferences.
Contract and changeset errors, JSON error messages, and framework pages use the request’s locale. Missing translations fall back, and frappe translations reports what remains. Background jobs carry their language explicitly.
Multi-tenancy
An application can be multi-tenant. frappe make tenancy MODEL adds the tenant schema, sign-up and home pages, and a tenant block in the routes; resources generated afterwards belong to the tenant, and --central makes one every tenant shares.
Domain-Driven Design
An application can model its domain in SweetTalk, an opt-in language whose contexts, values, aggregates, events, policies, projections and processes expand into SugarORM and Cold Brew. frappe make domain NAME CONTEXT --means=TEXT starts one in app/domain/ and writes a skill that gives agents its rules. Since 0.14.0 a refusal answers 422 from an action, values and ids bind as contract fields, and a lifecycle’s states are held in a PostgreSQL check. The guide maps each Domain-Driven Design pattern to its word and states its experimental status; the Box Office path builds one domain end to end.
Speak the domain’s language → Build a box office →
Observability
Every routed request, job and schedule run gets a request id, a trace and one canonical log line. In development, frappe dev adds an inspector, a toolbar, richer error pages and the frappe traces, trace and errors commands. In production, an owner-only ops socket serves status, Prometheus text, recent errors and a console; the recorder and the OTLP exporter are opt-in. Latte adds statement statistics, menu-bar error counts and notifications, per-site access logs and a local trace collector on 127.0.0.1:4318.
Read the observability guide →
Production
An application runs in production on Linux: frappe build writes a static linux/amd64 artifact, serve --no-workers and work are the process roles, GET /health answers readiness, SIGTERM drains for up to 25 s, APP db roles prints the role script and APP jobs unknown checks the queue before cutover. Every connection carries the statement and lock timeouts of 0.15.0.
Limits
Current limits
- Caramel ships no command that provisions servers or deploys;
frappe buildends at the artifact. - No generator for authentication: the sessions recipe shows the tables and actions to write by hand.
- Multipart files are parsed and bounded, but no recipe yet stores a file and removes it;
storage/is generated empty. - Frappé has no extension point for commands of its own; an application adds commands to its binary with
Caramel::Crema.command. - Installing a release still starts from a Git checkout of the framework:
frappe installations install VERSIONexists once one release is registered, and there is no installer for a machine without one. - Recipes labelled Draft are written against 0.16.0 and not run end to end; the cookbook marks each.
- SweetTalk is experimental until an application has run it in production.
Prove it
Check an upgraded application
frappe migrate
frappe check
frappe corretto
frappe lintPassing: frappe migrate ends with The database matches the declared schema., frappe check exits 0, frappe corretto ends with Corretto: N of N workers passed and frappe lint exits 0.