A local home for every app.
Latte runs PostgreSQL, DNS, and HTTPS on your Mac, and Frappé builds, serves, and checks each application at its own named origin.
| Contract | Value |
|---|---|
| Release | 0.15.0 |
| Needs | An Apple Silicon Mac with Apple’s Command Line Tools, and a checkout of the release |
| You will have | PostgreSQL, DNS and HTTPS from Latte, and each application at https://NAME.caramel |
| Proved by | frappe doctor |
Learning it for the first time? Your first application installs the release and sets up local HTTPS in order. This page lists every command, file and setting of the local environment.
Limits at a glance
| Limit | Value |
|---|---|
Log size before Latte’s sweep rolls it (the file moves to .previous) | 1 MiB, checked once a second |
| Development sessions per project | 1 |
frappe logs lines | 200 |
| Wait for related saves before a build | 50 ms |
First crystalline build | up to 20 minutes |
| Latte’s DNS port; Caddy’s ports behind the relay | 15353; 18080 and 18443 |
| Toolchain root directory mode | 0700 |
.env mode | 0600 |
Latte runs three services, not your application
Latte is a per-user service supervisor for macOS. It runs one shared PostgreSQL 18 cluster on a private Unix socket, CoreDNS, and a Caddy HTTPS proxy, and keeps a registry of your sites. Applications run under frappe dev in your terminal, not under Latte. Latte also runs a trace collector; the Crema reference describes it.
| Condition | Outcome |
|---|---|
| A Frappé command needs the services and Latte is not running | Frappé runs latte daemon --detach |
| The daemon | outlives the terminal |
| Stopping the daemon | leaves the services running; the next daemon adopts them |
latte daemon without --detach | runs Latte in the current terminal |
| A log past 1 MiB at Latte’s once-a-second sweep | moves to a .previous file, so a log can pass 1 MiB between sweeps |
| The menu bar app | does not start Latte |
frappe services # postgres, dns and proxy with their states
frappe services start # starts Latte when needed, then its services
frappe services stop # stops the services; Latte keeps running
latte stop # stops Latte; the services keep running
latte service install # start Latte at every login
latte service uninstall
latte versionExpect: frappe services, start and stop print one line each for postgres, dns and proxy with its state, then collector running (127.0.0.1:4318); start starts Latte first when needed, and after stop the services read stopped while Latte keeps running. latte stop prints Latte stopped; its services were left as they were (frappe services stop stops them)., or Latte is not running. latte service install prints Latte now starts when you log in (~/Library/LaunchAgents/dev.caramel.latte.plist), and is running. latte service uninstall prints Latte no longer starts at login; the Latte it started has stopped. and that the services keep running. latte version prints Latte 0.16.0 (control API 1, 2).
The login item is opt-in. latte service install writes ~/Library/LaunchAgents/dev.caramel.latte.plist, takes over from a running daemon, and starts Latte now.
Latte’s logs live in ~/Library/Application Support/Caramel/logs/: latte.log for the daemon, and postgres.log, dns.log, and proxy.log for the services.
The menu bar app shows the services and sites, opens a site, its folder, or its logs, and starts or stops the services. Build it from a Caramel checkout with scripts/build-latte-menu, which writes bin/Latte.app.
Setup runs once per machine
Caramel 0.16.0 runs on Apple Silicon with Apple’s Command Line Tools, and installs from source. From a checkout of the release, these steps install the pinned toolchain, put frappe and latte in ~/.local/bin, and connect .caramel names to Latte. Add ~/.local/bin to your shell’s PATH before the fourth step: from there on the commands run the installed launchers. Each step is safe to run again; prepare refuses an existing bundle folder, so the rm -rf clears it first.
scripts/install-toolchain
scripts/build-release
bin/frappe installations register
frappe services start
rm -rf /private/tmp/caramel-integration-bundle && scripts/install-local-integration prepare /private/tmp/caramel-integration-bundle && sudo scripts/install-local-integration apply /private/tmp/caramel-integration-bundle
latte trust installExpect: install-toolchain installs the pinned Crystal and PostgreSQL tools into a private 0700 root and writes its path to .caramel-toolchain. build-release installs the locked dependencies and builds bin/frappe, bin/latte and the linter. installations register prints Registered Caramel 0.16.0: PATH and writes the launchers to ~/.local/bin. frappe services start prints the three services and the collector with their states. prepare prints a JSON review with bundle, installs, runs_as and sha256; apply asks for your password and prints Installed Caramel resolver and unprivileged standard-port relay. latte trust install prints Latte certificate trust installed.
| Condition | Outcome |
|---|---|
prepare | builds the integration bundle as you; as root it refuses with Prepare the installation as the ordinary installing user |
prepare with an existing bundle folder | fails with File exists |
apply | installs it as an administrator; refuses unless run through sudo |
apply while another release’s integration is installed | refuses with A different Caramel integration is installed; uninstall it explicitly first; frappe doctor then prints the prepare, uninstall and apply sequence that replaces it |
scripts/install-local-integration status | prints a JSON object with current, stale or absent for relay, plist and resolver, without sudo |
The resolver /etc/resolver/caramel | sends .caramel lookups to Latte’s DNS on 127.0.0.1 port 15353 |
The launchd relay /Library/PrivilegedHelperTools/dev.caramel.ports, loaded by /Library/LaunchDaemons/dev.caramel.ports.plist | forwards ports 80 and 443 to Caddy on 18080 and 18443 |
The receipt /Library/Application Support/Caramel/local-integration.json | records what apply installed, and uninstall removes only files that still match it |
latte trust install | adds Latte’s certificate authority, named Caramel Local Authority in Keychain Access, to the keychain security default-keychain -d user names |
latte trust install after the authority was rotated | refuses with Remove the previously installed Latte certificate before rotating its authority |
sudo scripts/install-local-integration uninstall and latte trust remove | undo these steps; uninstall leaves certificate trust alone |
Every site gets its own origin
A site’s origin is https://NAME.caramel.
| Condition | Outcome |
|---|---|
frappe new NAME | registers the application with Latte as a site, writes its private .env, and applies its migrations |
frappe setup in a cloned project | installs the locked dependencies and takes the same steps, keeping existing files |
A site’s state in frappe sites | is running, building, build-error, stopped, unavailable or unknown; (terminal) after it marks a site that a frappe dev session serves |
frappe sites remove NAME | unregisters the site and keeps its folder, databases, credentials, logs, and backups; frappe setup registers it again |
frappe sites remove NAME for a name Latte does not know | exits 1 with No registered site is named NAME |
domain_suffix in config/environment.yml | caramel, test, or localhost |
A .localhost name | resolves to loopback without a resolver entry, but frappe dev, frappe open, and frappe doctor still need the port relay and latte trust install, because they require the origin to reach loopback with trusted HTTPS on the standard port |
| Switching the suffix | change it, delete .env, and run frappe setup, which registers a new site with its own databases |
frappe sites
frappe sites remove demo
frappe openExpect: frappe sites prints one line per site: its name, state, origin and folder. frappe sites remove demo prints Removed demo (https://demo.caramel) from Latte., that its folder, databases, credentials, logs and backups were kept, and the frappe setup command that registers it again. frappe open opens the origin in your browser.
Source changes rebuild the application
frappe dev builds the application, serves it at its origin, and opens the browser when the first build is ready. It watches src, config, db, app, public, shard.yml, shard.lock, and .env with kqueue. Ctrl-C stops the project.
frappe dev
frappe dev --no-open
frappe dev --branch experimentExpect: frappe dev builds the application, serves it at its origin and opens the browser when the first build is ready; --no-open leaves the browser alone. The terminal prints Type check passed in N ms or Type check failed in N ms when the compiler finishes its semantic stages, then Building NAME…. --branch experiment runs against the database branch that frappe db branch create experiment made.
| Condition | Outcome |
|---|---|
| A source change | waits 50 ms for related saves, then runs one compiler build with -D caramel_development |
| A type-check failure | prints Type check failed in N ms and skips code generation |
| A newer change | stops an obsolete build |
| Asset changes alone | refresh the page without a rebuild |
| The application is building or has failed | answers 503 at its origin with a diagnostics page showing the compiler’s output, with secrets and database URLs redacted; the page reloads when the next build is ready |
| Pending migrations | frappe dev says so once and serves as soon as frappe migrate applies them |
A changed .env | needs a restart |
A second frappe dev for the same project | refuses |
frappe logs
frappe logs compiler --followExpect: frappe logs prints the last 200 lines of the application log through tail; compiler --follow keeps printing. A missing log exits 1.
frappe logs access prints the proxy’s access log. These logs live in ~/Library/Application Support/Caramel/logs/sites/.
Every site gets two databases
Latte gives each site a development database and a separate spec database, each with a runtime URL and a migration URL. Setup writes them to .env with APP_ORIGIN and a fresh APP_SECRET.
| File | Holds |
|---|---|
.env | Private: the origin, the secret, and the development and spec database URLs |
.env.test | Committed test-only settings for every spec worker |
config/environment.yml | Site name, PostgreSQL major, extensions, and domain suffix |
db/seeds.cr | Seed data for frappe seed |
Specs never touch development data. frappe corretto migrates the spec database, has Latte clone one for each worker, and gives each worker the values in .env.test. The keys that file cannot set are in the Corretto reference →
| Condition | Outcome |
|---|---|
.env | mode 0600, ignored by Git |
A DATABASE_URL or MIGRATION_DATABASE_URL in .env that differs from Latte’s | refuses with KEY differs from this project’s Latte credentials; command refused |
An APP_ORIGIN in .env that differs from the project’s | refuses with APP_ORIGIN differs from this project; run frappe setup |
frappe seed | runs App.seed from db/seeds.cr against the development database, and never resets it |
frappe db restore FILE | saves the current database as a pre-restore backup first, then restores FILE; refuses while a development session runs |
frappe db branch create NAME that is not a lowercase letter followed by up to 30 lowercase letters, digits or underscores | exits 1 with the usage message |
frappe seed
frappe db dump
frappe db restore backup.dump
frappe db branch create experimentExpect: seed runs App.seed against the development database. db dump saves a backup under ~/Library/Application Support/Caramel/backups/SITE_ID/. db restore saves the current database as a pre-restore backup, then restores the file. db branch create clones the development database into branch experiment and prints its connection URL.
A branch is a copy-on-write clone of the development database.
Branch, diff, and migrate with SugarORM →Projects run the release their lock pins
An application’s shard.lock pins its Caramel release, and Frappé hands each project command to that release’s Frappé.
| Condition | Outcome |
|---|---|
| A missing release | is offered for install on a terminal; agents and pipes get the install command |
frappe sites and other commands that need no project | run in the Frappé you called |
The launchers in ~/.local/bin, on-demand Latte, and the login item | run the newest installed release |
| A resolver or port relay that differs from the newest release | fails frappe doctor’s Port relay check, which prints the prepare, uninstall and apply commands that replace them |
installations install on an installed release | builds what is missing, such as its linter |
frappe installations
frappe installations install 0.16.0
frappe installations register
frappe installations remove 0.16.0Expect: frappe installations prints one line per release, its version then its root, with (this installation) after the one you called; with none it says so and names the two ways to install one. install clones the release’s tag into ~/Library/Application Support/Caramel/releases/VERSION, reuses your toolchain when the release pins the same one, builds it and registers it. register prints Registered Caramel 0.16.0: PATH and needs a built bin/frappe and bin/latte; without them it exits 1 naming the build script. remove forgets a release.
Each installation pins two language servers
Each installation pins two language servers that use its private toolchain and the Crystal compiler the release pins, never a global or Homebrew Crystal: crystalline 0.20.0 for definitions, hover, limited completion, diagnostics on save, formatting, document symbols, and signature help, and ameba-ls 0.2.0 for lint diagnostics with Fix and Ignore actions. liger is disabled. The Current release page states the Crystal pin.
frappe lsp installExpect: The first run needs network access, git and Apple’s Command Line Tools, and prints Building crystalline 0.20.0 with Crystal VERSION (this can take up to 20 minutes)… while it builds crystalline from source. Later runs verify the installed tools.
| Condition | Outcome |
|---|---|
frappe lsp crystalline or frappe lsp ameba-ls | checks the pinned release, then runs a server on stdio for the current project; any editor can run them from the project folder |
Zed with no matching frappe on its login-shell PATH | does not start the servers (see troubleshooting) |
| File | Holds |
|---|---|
.zed/settings.json | Generated: runs both servers through frappe in Zed, which must find frappe on its login-shell PATH |
Older projects copy .zed/settings.json from templates/application in Caramel’s source. frappe installations register puts frappe in ~/.local/bin, where Zed’s login-shell PATH finds it. A one-off PATH=… zed . starts the servers until Zed refreshes the worktree environment, which drops that PATH. CARAMEL_TOOLCHAIN_ROOT is optional, because the installation’s .caramel-toolchain names the toolchain. Before it starts a server, frappe lsp checks the release pinned in shard.lock, as frappe dev does.
Several installations can sit side by side. Each pins its own tools, and installations that share a toolchain root keep their versions apart; different Crystal pins need separate toolchain roots. A project is served by the installation whose frappe Zed finds, and frappe lsp runs under the release the project pins in shard.lock. crystalline navigates into the project’s Caramel dependency in lib/caramel.
To verify, run tail -f ~/Library/Logs/Zed/Zed.log, which shows the lsp crystalline server starting, or run dev: open language server logs in Zed to see the server’s frappe lsp: startup line with its binary, toolchain root and where the root came from.
Editor messages point to their fix
| Message or symptom | What to do |
|---|---|
frappe lsp: no Caramel toolchain is configured | Run scripts/install-toolchain in the Caramel checkout. |
frappe lsp: toolchain root … (ownership, symlink or privacy) | The root must be an owned, private (0700) directory; fix it or point to the right root. |
frappe lsp: <root> is not a completed Caramel toolchain | Rerun scripts/install-toolchain to finish it. |
frappe lsp: <root> does not provide Crystal <version> | This installation needs a toolchain root with its Crystal pin. |
frappe lsp: <name> <version> is not installed in <root> | Run frappe lsp install for this installation. |
frappe lsp: <binary> failed verification | Remove the named directory and run frappe lsp install. |
frappe lsp: another editor tools installation is running | Wait for it to finish. |
frappe lsp: checksum mismatch or download failed | Nothing was installed; retry on a working network. |
frappe lsp: … loaded a library outside Caramel or macOS | The binary was not published; report it. |
frappe lsp: crystalline build directory preserved for inspection | The build output above names the failure; remove the directory afterwards. |
This project uses Caramel X, not Y; use its matching Caramel installation | Install that release with frappe installations install X, or register its checkout. |
Zed failed to spawn command for bin/frappe | Run scripts/build-frappe. For frappe: the matching binary is absent from Zed’s login-shell PATH; a one-off PATH=… zed . works until a worktree-environment refresh. |
Zed Please install crystalline manually and make sure it is on $PATH | The project settings were not applied, because the worktree is untrusted or .zed/settings.json is missing. |
Zed Waiting for worktree … to be trusted | Trust the worktree. |
| After installing | Run editor: restart language server. |
| ameba-ls diagnostics while you edit | ameba-ls intentionally disables Layout/TrailingWhitespace, Layout/TrailingBlankLines and Lint/Formatting while you edit. Try pp! 1 to verify Lint/DebugCalls diagnostics instead. |
Syntax diagnostics in templates/** | Expected: those files contain generator placeholders. |
| Memory use | crystalline compiles the project in memory; its use grows with the project and its dependencies. |
| Format on save | Uses crystalline, which formats with the pinned Crystal formatter. |
Removing editor tools changes nothing else
The root is the first line of the checkout’s .caramel-toolchain.
| Condition | Outcome |
|---|---|
rm -rf "ROOT/editor" | removes every editor tool in that root |
Deleting ROOT/editor/ameba-ls/VERSION or ROOT/editor/crystalline/VERSION-FINGERPRINT | removes one version |
| A launch after deleting | prints frappe lsp: NAME VERSION is not installed in ROOT; nothing else changes |
Prove it
Run the doctor
frappe doctorPassing: Each of the nine checks prints OK and its label, from Project configuration to Port relay, and the command exits 0. A failed check prints CHECK LABEL: reason instead, and the exit is 1. A stale resolver or relay fails Port relay with the prepare, uninstall and apply commands that replace them.