# [0.3.0] - 2026-11-01

Storefronts become templates you copy and own, built on shared packages that hold everything that isn't design. The Next.js demo is now the `nextjs-vetrina` template, `astro-shop` joins it, and both run on one framework-free core, so a fix to cart, checkout or routing reaches every shop through a merge. Every dev command is now the `skelet` CLI, which you install once and start projects with through `skelet new`. Upgrading takes a few renames: install the CLI the README's way, since `./skelet` and `bin/` are gone; `vetrina-demo` is `nextjs-vetrina` in your `env/` files, `.skelet-apps`, `docker-compose.override.yml` and any `--apps` list; and on small or prod each storefront runs under a compose profile of its own name, so add it to `COMPOSE_PROFILES` in `.env`, or re-run `skelet setup`, which does it for you.

### Added

- **A second template, `astro-shop`, built for speed.** An Astro 7 storefront under `templates/` on the
  same front API as nextjs-vetrina, for small catalogs. It has a home page, product pages, a cart and a
  one-page checkout. Pages are rendered once and served from Astro's route cache until the backend purges
  them through `/api/revalidate`, the same contract nextjs-vetrina implements. Every page works without
  JavaScript, and the scripts it does ship come to about 5 KB gzipped. Product JSON-LD, canonical URLs,
  `robots.txt` and `sitemap.xml` are built in. It has a light and a dark theme that follow the system
  until the shopper picks one, and self-hosted Poppins and Montserrat fonts - `?font=montserrat` on any
  URL switches the font for testing. It runs on the same storefront core as nextjs-vetrina, through
  `@skelet/storefront-astro`. Run it with the `astro-shop` profile on port 3001, and test it with
  `skelet test storefront astro-shop`.

- **The product picker and the Products list filter by category, collection, price, stock and
  attributes.** A category includes its subcategories, a price is read in each product's own
  currency, and stock follows the storefront's rule - a product with no stock record counts as in
  stock. The admin API's `productsPaged` takes the same filters as `categoryIds`, `collectionIds`,
  `priceMin` / `priceMax` and `inStock`.

- **`skelet doctor` checks more of what breaks a clone.** git new enough for setup's sparse-checkout,
  Go on the host against `go.mod` (lint and parts of codegen run it there), Node against `.nvmrc` for
  the frontends you run on the host, `plugins.json` against the plugin directories by the rules codegen
  enforces, whether the pre-commit hook is installed, and a WSL clone sitting on the Windows drive.
  Outside a project, the installed `skelet doctor` checks the machine alone, before `skelet new`.

- **Install the CLI, then start a project with `skelet new`.**
  `curl -fsSL https://github.com/skeletg/skelet/releases/latest/download/install.sh | sh` puts
  `skelet` on your PATH after checking it against the release's checksums, and
  `skelet new my-shop` fetches a release into `my-shop` and runs its setup. The project is a git
  checkout with Skelet as its `upstream` remote and your own `main` branch, so a later release is
  a merge. Inside a project the installed `skelet` hands every command to that project's own CLI,
  so it never runs one with a different release's code. Each release now carries the CLI for
  macOS and Linux, built by the tag push with a provenance attestation.

- **`skelet setup` draws its questions in the terminal.** Arrow keys pick the target, the template and the search
  engine, checkbox lists take the apps, dev tools and payment gateways, and each answer folds into one line under its
  section. Before anything is written a review lists every answer - enter goes ahead, `e` asks again from the answers
  given, `q` leaves with nothing written. The slow steps run behind a spinner, their output in `.skelet/setup.log`,
  shown when one fails. Without a terminal, or with `--yes`, `--profile` or `NO_COLOR`, setup asks in plain lines as
  before.

- **`skelet test --changed` runs only what a change reaches.** Go keeps its test cache, keyed on the code and on the
  migration files, so only the packages a change touches rerun; each frontend runs its typecheck and only the vitest
  files that import a changed file, and an app nothing touched is skipped. "Changed" is everything since the branch
  left develop plus what isn't committed yet, or `--changed=REF`. Every test run also starts faster: leftover test
  database clones are dropped in one go, and the database flush they cause runs in the background after the Go tests
  instead of stalling the start of the next run for a minute or more.

### Changed

- **The CMS page editor is down to five sections: Layout, Content, SEO, Page settings and Channels.**
  Publishing, scheduling, review and the cache rebuild live in one menu in the header, which also
  says where the page is live. Version history and comments open in their own dialogs, and the
  history says what each version changed and what restoring one would undo. The SEO previews and settings fold into one line that sums up the
  on-page checks, and each block keeps its tracking and visibility settings under Advanced. Blocks
  open and close without jumping, and the one you click stays on screen while another closes. In
  global scope, one notice explains why the layout and content are read-only and offers a scope
  that can edit them. The live preview shrinks to leave the editor room, and on a screen too narrow
  for both it folds to a rail that opens over the editor - it used to squeeze the editor out of
  sight. It can be hidden on wide screens too.

- **The docs, messages and help say `skelet`, the installed CLI, instead of `./skelet`.** In a
  project it runs that project's own code; with only Go, `go -C cmd/skelet run . <verb>` does the
  same.
- **`skelet doctor` warns when the `skelet` on your PATH is a link to a project's `./skelet`,**
  which projects no longer have. The installer replaces such a link with `--force`.

- **`skelet check structure` looks in every tracked file for a `bin/` script that no longer exists**,
  not only in Markdown and YAML, so a Go or TypeScript message pointing at a retired script fails the
  gate instead of reaching someone.

- **Every dev command is part of the skelet CLI.** In a project, the installed `skelet` builds
  that project's own `cmd/skelet` on first use - with host Go when there is one, otherwise inside
  the `golang` image, so Docker alone is still enough - and runs it, so a command always matches the
  code you're on. Verbs, flags and exit codes are as they were.
- **Storefronts are scaffolded with `skelet add channel <name>`**, the pair to `rm channel`.
  `new channel` keeps working and says it's `add channel` now.

- **`skelet doctor` is part of the Go CLI.** The same checks, columns and tally, and it still fails only
  on a FAIL.

- **The pre-commit hook finds the skelet CLI on its own.** `skelet hooks` writes a hook that runs
  `skelet check` through the CLI on your PATH, `~/.local/bin/skelet`, the one it was installed with,
  or `go run` from the checkout - git GUIs often run hooks without your shell's PATH. The hook an
  earlier `skelet hooks` wrote is updated the next time you run any `skelet` command, and a hook of
  your own is no longer overwritten unless you pass `--force`. It also installs into worktrees.

- **`skelet setup` is part of the Go CLI.** Same questions, flags and files, but it no longer needs
  OpenSSL - which stock macOS doesn't have - or anything else on the host, and the env files it
  creates are readable by you alone.

- **Storefronts are made from templates.** The demo storefront is now the `nextjs-vetrina` template under
  `templates/`, and `apps/channels/` holds a project's own shops. `skelet add channel <name>` copies
  a template there (`--template <name>`, or `--from <shop>` for one of your own), points its paths at
  its new home and adds it to the lockfile. The templates and the packages they build on stay
  upstream's, so merging a release brings their fixes without touching your shop. `skelet rm channel`
  leaves the templates alone.
- **`skelet setup` leaves the templates out unless you ask for one.** Setup is for working on your own
  store: your storefronts under `apps/channels/` default to yes, a template to no, and `--yes` takes
  that default (`--all` still means everything). A template left out of the working tree is still
  forked by `skelet add channel`, straight from git.
- **Setup offers your first storefront.** With no shop of your own yet, `skelet setup` asks which
  template to copy and what to call it, and makes it the way `skelet add channel` would - selected,
  and running where your other frontends run. `--template <name>` and `--shop <name>` do it without
  asking, and `skelet new` hands them on: `skelet new my-store --template astro-shop`.
- **The storefronts are one pnpm workspace.** `pnpm-workspace.yaml` and `pnpm-lock.yaml` at the root
  cover `apps/channels/*`, `templates/*` and the new `packages/*`, and the workspace's catalog keeps
  the versions they share in step, so a package and the app using it resolve the same React. The
  dashboard stays a project of its own.
- **A storefront's logic is `@skelet/storefront-core`, with no framework in it.** Everything the demo
  kept in `app/lib` that isn't design - the GraphQL client and its operations, auth, cart, checkout,
  channel and locale, the actions - now lives in `packages/storefront-core`, which asks the request it
  runs in for cookies, headers, locale and channel instead of calling Next. `@skelet/storefront-next`
  answers those the Next way and wraps the actions as server actions, `@skelet/storefront-astro`
  answers them the Astro way, and `@skelet/storefront-react` holds the client hooks any React
  framework can use. A Next storefront's routing is the adapter's as well - channel and locale, the
  session refresh, the visitor id - so its `proxy.ts` is a call to `storefrontProxy` and a matcher. A shop forked from a template keeps up with fixes
  to that code by merging a release, without touching its own pages. Codegen is the core's, run by
  `skelet gen`, and `skelet check` fails on a framework import in the core and reads the shared lists
  from it.
- **A storefront's dev container mounts the whole repo** at `/var/www/skelet`, read-only but for its own directory
  and `packages/`, with `env/` hidden behind a tmpfs, and its prod image builds from the repo root with a `.dockerignore` of its own
  beside the Dockerfile. `skelet doctor` notes an override still mounting a storefront at
  `/var/www/storefront`, and a `.skelet-apps` naming a storefront that isn't there.
- **`vetrina-demo` is now `nextjs-vetrina`** everywhere it's named, since a template carries its framework
  in its name: the compose service and profile, the `skelet-nextjs-vetrina` image and its
  `NEXTJS_VETRINA_IMAGE` variable, and `env/{prod,small}-nextjs-vetrina.env`. Rename your `env/` files
  and update `.skelet-apps`, `docker-compose.override.yml` and any `--apps` list that names the old one.

- **Storefront prod images** install only what their template depends on, run as root-owned files with no npm, yarn or
  corepack, and carry their own `HEALTHCHECK`; the prod and small compose files run them with `init`. CI checks the
  whole storefront workspace in one job and smoke-runs each storefront image.
- **`formatPrice` takes a locale**, and writes a currency with no symbol configured the locale's way rather than as its
  code. astro-shop formats every price through it, in the channel's language.

### Removed

- **The scripts behind `skelet check`, its gates and `skelet changelog` are gone** - `skelet check`,
  `check pins`, `check structure`, `check contracts` and `changelog` are part of the CLI now.

- **The scripts behind `skelet gen` and `skelet lint` are gone** - both are part of the CLI now, and
  `skelet check codegen` is `skelet gen --check`. The golangci-lint version they pin lives in the CLI's
  source, where `skelet check pins` and Renovate find it.

- **The scripts in `bin/` behind `down`, `stop`, `restart`, `ps`, `logs`, `serve`, `sh`, `cli`,
  `psql`, `db`, `search`, `release`, `provider` and `fixperms` are gone** - those commands are part of
  the skelet CLI now. Run them as `skelet <command>`: the old `psql` script is `skelet psql`,
  `opensearch` is `skelet search`, and `push-images` is `skelet release`.

- **The scripts behind `skelet add channel` and `skelet rm channel` are gone** - every verb is part
  of the CLI now.

- **`./skelet` and `bin/` are gone.** Install the CLI the README's way - inside a project it runs
  that project's own code, and one from an earlier release still gets its `./skelet`. With only Go,
  `go -C cmd/skelet run . <verb>` does the same. A pre-commit hook from an older `skelet hooks`,
  which ran a script that's gone, is rewritten by any `skelet` command, and `skelet doctor` fails
  on one that's left.

- **The scripts behind `skelet test`, `skelet test e2e` and `skelet test storefront` are gone** - all
  three are part of the CLI now, along with the disposable-database helpers they shared.

- **The scripts behind `skelet go`, `skelet pnpm` and `skelet gen sqlc`, `gqlgen` and `buf` are gone** -
  they're part of the CLI now.

- **The script behind `skelet up` is gone** - it's part of the CLI now, port check and env syncs
  included. `bin/` holds only what `skelet add channel` and `skelet rm channel` still run.

### Fixed

- **A CMS page's search-indexing switch sticks.** The editor had two of them that both saved on
  every Save, so changing the slug in the same save could put indexing back the way it was. The
  switch now lives in the SEO panel only.

- **Creating a record no longer leaves a stale draft behind.** On a slower machine, the language,
  currency, country and other create pages could store their own emptied form as a draft just after
  the create, so the next visit to the page offered to restore a blank form. The form now keeps what
  was created until the page moves on.

- **A production dashboard no longer prints raw message syntax.** Any text with a number or a plural
  in it - the table pager's row count and "of N" pages among them - showed up as
  `{totalRows, plural, one {# row} other {# rows}}` instead of the formatted words, in English and in
  installed language packs alike. Development builds were never affected.

- **A project's own `v*` tag no longer tries to publish a Skelet release.** The release workflow runs only in
  Skelet's repository, so a project made with `skelet new` can tag its versions freely. The docs also say how a
  project keeps Renovate to its own shops, so its bumps don't collide with the next release's.

- **A small or prod setup serves the storefront you picked.** The demo storefront always started and the edge always
  proxied to it, whatever setup was told. It now runs under its own compose profile like astro-shop, and setup writes
  env files only for the storefronts you picked, turns their profiles on in `COMPOSE_PROFILES`, and points
  `STOREFRONT_REVALIDATE_URL` and the edge's new `STOREFRONT_UPSTREAM` at the one it serves. A value you set by hand
  stays.
- **The dashboard previews your first shop.** `skelet add channel` on a dev clone sets `VITE_STOREFRONT_ORIGIN` to the
  new shop's port when `.env` names none, instead of leaving previews on the template's `:3000`.

- **Messages that named retired scripts say which `skelet` command to run.** The ops CLI's plugin,
  job and data-patch help, the plugin scaffold's README and migration templates, the generated
  plugin list, the remote-provider errors and the dashboard's dev admin switcher pointed at scripts
  that are now `skelet cli`, `skelet psql`, `skelet db migrate`, `skelet logs` and `skelet provider`.

- **`skelet gen --check` catches a generated file that's new, and lists only what it changed.** It
  compared the diff of tracked files, so a newly generated file that wasn't committed passed, and
  files that were already modified before the run showed up as stale.

- **`skelet sh`, `cli` and `provider` find a running app container by its service name.** They
  looked for `skelet-app` in the container list, so with `COMPOSE_PROJECT_NAME` set they never saw it
  and started a second app container every time.
- **`skelet search` needs neither `jq` nor `curl`**, prints its table everywhere, and rejects a
  `--limit` that isn't a number instead of sending OpenSearch broken JSON.
- **A failed `skelet db export` leaves nothing behind**, and `skelet db import` without a terminal says
  to re-run with `-y` instead of failing on `/dev/tty`.
- **`skelet serve` reads the root `.env` the way compose does** rather than running it as a shell
  script.

- **`skelet add channel` refuses an override with no `services:` section.** It added the
  storefront's volumes there, dropped the service itself, and reported it written.
- **`skelet add channel` handles an override that lists `volumes:` above `services:`.** The new
  volumes landed under `services:`, compose rejected the file, and nothing could be added.
- **`skelet add channel` leaves the source's `node_modules` and `.next` behind** instead of copying
  them into the fork and deleting them afterwards - a host install alone can run to a gigabyte.
- **`skelet add channel` refuses a name the override already uses, and a `--from` that isn't a
  storefront name,** before anything is copied.
- **`skelet rm channel` says when a tracked storefront holds untracked files.** It called the
  directory recoverable with git checkout, which brings none of them back.

- **`skelet test e2e` says it needs ClickHouse before it touches anything.** Under `skelet up --lean`
  it dropped and recreated the e2e Postgres databases, then stopped on a compose error about a
  service that wasn't running.
- **`skelet help <command> <sub>` describes the subcommand.** `skelet help test e2e` and
  `skelet help check pins` printed the parent command's help.

- **`skelet gen` writes sqlc's files as you.** sqlc ran as its image's root, so on a Linux Docker
  engine every regenerated file under `internal/db/sqlc` belonged to root.
- **`skelet gen buf` hands its arguments to buf as they are.** They were spliced into a shell
  command, so one with a space or a quote in it was split or misread.

- **`skelet up`'s port check stops when its input ends.** Ctrl-D at its prompt asked the same
  question again, forever.
- **Two services moved off busy ports in one go can't land on the same port.** The second was
  offered, or allowed, the port the first had just moved to, and compose then failed to bind it.
- **env/signals.env gets the value core runs with when env/go.env names a key twice.** It copied
  the first line, while compose reads the last.
- **Re-running `skelet up` leaves an unchanged stack alone.** Every `--build` gave the images a new
  ID through buildx's default provenance attestation, so the app, Signals and Docker-run frontends
  were recreated on every run. The dev images are built without it now; set
  `BUILDX_NO_DEFAULT_ATTESTATIONS` yourself to choose otherwise.

- **Doctor's `*.localhost` check can fail.** Its fallback passed on any output from curl, including the
  `000` curl prints when the name doesn't resolve, so the check never warned. Now only a failed lookup
  warns, and any answer from the server - a 404 included - passes.

- **A prod setup starts its datastores.** Setup created the root `.env` empty, without the template's
  `COMPOSE_PROFILES=bundled`, so compose left the bundled Postgres, pgbouncer, Valkey and ClickHouse
  out of the deployment. A new `.env` for small or prod now starts from `.env.example`.
- **Small and prod pass the storefront revalidation secret.** Compose hands the app and the storefront
  `STOREFRONT_REVALIDATE_SECRET` from the root `.env`, which setup never filled, so both got an empty
  one over the value generated into the backend's env file. Setup now copies it across like the
  other shared secrets.
- **The dev storefront signs revalidations with the secret core checks.** It had a fixed value while
  setup generated core's; it now reads core's from the root `.env`, which setup fills.
- **Setup no longer rewrites `plugins.json` in the old format.** It only understood bare `0`/`1`
  entries, so a run dropped module plugins from the file and could write the name of a parked
  `_plugin` directory, which the build then refused. It now merges its gateway picks in the form
  `skelet cli plugin` writes, removes parked names an older run left, and leaves the rest alone.
- **`skelet setup --profile FILE` leaves FILE alone.** It ran the file as a shell script to read it and
  wrote the answers back into it; it now only reads it, and saves the answers to `.skelet-profile`.
- **A `skelet setup --yes` re-run keeps the frontends where they run.** With a profile from before the
  Docker-or-host question, it emptied `.skelet-apps`, moving every containerized app to the host.
- **`skelet add channel` suggests a setup command that keeps your other apps.** It built the `--apps`
  list from `.skelet-apps`, which only names the apps running in Docker.

- **The Next.js storefront's production image serves its pages again.** Every page answered 404: in production
  Next runs the proxy a second time, on the path its own rewrite pointed at, and the rule refusing a
  direct `/<channel>/` URL refused that pass too. The rewrite now carries a mark only the running
  server knows, and its second pass goes straight through - without refreshing the session or
  reporting a crawl again.

- **A storefront's dev container can no longer change the checkout.** It mounts the repo read-only, writing only its own
  directory and `packages/`, with the root `.env` and the rest of `apps/` hidden as well as `env/`. It installs frozen,
  `skelet pnpm <name> add|remove|update` runs over the workspace's manifests in a throwaway container, and the new
  `skelet pnpm --lockfile` refreshes the lockfile by itself. `skelet doctor` notes a shop in the override still
  mounting the checkout writable - recreate it from the template's block. Run `skelet up` to pick it up.
- **Open redirects.** A `returnTo` with a tab, a newline or a backslash in it is refused, a posted `locale` no longer
  decides where an action lands, and a locale is only ever a locale code in front of a path.
- **Masquerade links install only masquerade sessions.** The backend marks the tokens it mints for an operator, and the
  storefront's `/masquerade` landing refuses any other - a customer's own token in a crafted link would have signed
  whoever clicked it into the sender's account. It redirects to a relative `/`.
- **The backend proxy keeps to its routes.** `/t/%2e%2e/api/...` matched a proxied prefix and resolved outside it.
- **A purge drops everything it names.** Both templates' revalidate routes kept only the first 64 tags and paths; the
  body is bounded by size now. A warm request stays on the storefront's origin and follows no redirect, since it carries
  the revalidate secret.
- **The session refresh logs a shopper out only when the backend says the token is no good** - not on a rate limit or
  an outage - names the shopper's address to the backend, and hands the rotated refresh token to the render too.
- **astro-shop's route cache kept one channel's page for another host** when hosts map to channels; pages now vary by
  host there. Its pages are dropped by menu and channel purges as well, kept for the admin's cache time, and render
  catalog reads without a shopper's session.
- **A cart the backend couldn't read is kept.** astro-shop deleted the cart cookie whenever reading the cart failed.
- **Shopper actions no longer purge every cached page.** Signing in or out, consent and placing an order re-render the
  shopper's own page instead of revalidating the whole site.
- **`skelet add channel`** copies into place last, so a failure leaves nothing half-made, renames the source only
  where its name stands whole, defaults to `nextjs-vetrina`, rewrites the ports of a template on its own (astro-shop's
  3001), and fails loudly when the lockfile can't be refreshed. `skelet rm channel` refuses a template setup left out of
  the working tree, which it used to strip from the tracked compose files.
- **`skelet setup`** offers a first shop only to a plain interactive dev setup, asks before where the frontends run so
  the shop goes with them, and refuses a shop name a compose service already has.
- **`skelet release`** builds only the frontends the clone runs when setup recorded a selection.

- **A monthly subscription started on the 29th-31st no longer skips a month.** Each renewal date was the previous one
  plus a month, so the first short month knocked it off its day for good - started on Jan 31, it next billed on Mar 3,
  with no February invoice, and stayed on the 3rd. Renewals now count from the subscription's start (or the end of its
  trial, or when it was resumed), and a month too short for the day bills on its last day instead: Jan 31, Feb 28,
  Mar 31. Yearly plans started on Feb 29 bill on Feb 28 in other years. Existing subscriptions keep the date they're
  on now.

[0.3.0]: https://github.com/skeletg/skelet/compare/v0.2.3...v0.3.0

---

From the Skelet changelog: https://skelet.dev/changelog/0.3.0/
