Changelog

Every release

Changelog

What changed in each release, sorted the way Keep a Changelog sorts it. Versions follow semver, with one caveat: until 1.0, breaking changes ride the minor version.

  1. nextUnreleased, on develop
    • Changed1
  2. v0.3.1Patch release
    • Added2
    • Fixed7
    • Security4
  3. v0.3.0Minor release - may break, under 0.x
    • Added6
    • Changed18
    • Removed8
    • Fixed47
  4. v0.2.3Patch release
    • Added8
    • Changed11
    • Fixed25
    • Security6
  5. v0.2.2Patch release
    • Security3
  6. v0.2.1Patch release
    • Added1
    • Changed1
    • Fixed2
  7. v0.2.0First in this changelog
    • Added9

Unreleased

On develop, not in a release yet

next

Waiting since v0.3.1

Entries
1
  • Changed1

Changed1

  • A new shop takes the storefront packages from npm. skelet add channel and skelet setup pin the copy’s @skelet/storefront-* to the checkout’s release on npm, where they used to link packages/, so merging a later release no longer changes a shop until you bump them. skelet doctor says when a shop is behind the backend, with the command that bumps it. A checkout ahead of its last release still links packages/, as does --workspace. pnpm takes a @skelet/* release the day it’s published.

    filed as shops-on-npm

v0.3.1

2026-11-01

v0.3.1

Patch release

Released
Entries
13
  • Added2
  • Fixed7
  • Security4

Added2

  • AI agents can read the store over MCP. /api/admin/mcp serves the dashboard chat’s tools to any MCP client that can send an integration token in X-Api-Token, Claude Code included. It’s read-only, reads one channel per connection, and offers only the tools the token’s permissions cover - sales, catalog, orders, stock, promotions, reviews and store setup. Each token can run 4 calls at once and, with RATE_LIMIT_ENABLED on, MCP_RATE_LIMIT_PER_MIN a minute. MCP_ENABLED=false turns it off. Setup is in docs/mcp.md.

  • The storefront packages publish to npm. @skelet/storefront-core, -next, -astro and -react go out with every release at its version, compiled to JavaScript with their types and licensed MIT, so a shop in a repo of its own can install them. A Skelet checkout keeps reading the source. The release workflow publishes through npm’s trusted publishing, with no token stored - docs/releasing.md has the one-time setup.

Fixed7

  • The AI chat’s product search reaches the whole catalog. It used to scan only the first thousand products, so on a bigger store it answered “no products match” for things you sell. It now searches in the database, by name, SKU or URL, like the Products list does, newest first.

  • The AI chat’s tools answer for the chat’s channel. A product’s review counts and stock by location covered every channel, and review counts with the dashboard set to “All channels” did too.

  • A failed read in the AI chat says so. “Could not load …” came back to the model as an ordinary answer, and the cause was never logged. It’s flagged as an error now, and logged.

  • The AI chat’s overview tools say when a read failed. Sales, web traffic, customer behaviour and experiments answered “no data” when the database or the Signals service couldn’t be read, and the chat’s opening snapshot left the section out the same way. They flag the failure now, and the snapshot says the section couldn’t be read.

  • Money in the AI chat uses each currency’s own decimals. Every amount was printed with two, so yen came out a hundred times too small and dinars ten times too large. Promotion totals now name their currency too.

  • The AI chat’s low-stock list covers the channel’s own warehouses. It filtered a store-wide page of 500 items down to the channel’s, so a busy store could show “no low-stock items” for a channel that had some, and in flat inventory mode it never showed any. The insights card’s count is the channel’s now too.

  • The AI chat’s window tops out at a year. A larger window reached the sales queries unclamped.

Security4

  • The AI chat can only run the tools it was offered. With customer data turned off in the AI settings, the chat hid the customer-record tools, but the model could still run one by asking for it by name. Now any tool it wasn’t offered is refused.

  • Turning off customer data in the AI settings hides emails on orders too. The order tools kept showing each customer’s email after the switch was off. They don’t now.

  • The AI chat quotes what shoppers and visitors wrote. Customer names, review summaries, site searches, page addresses, referrers, rage-clicked element text, JS errors and custom event names reach the model in quotes, with line breaks escaped, so a crafted name or search can’t pass itself off as part of the tool’s answer.

  • The AI chat no longer confirms an id belongs to another channel. Looking up an order or customer from another channel answered differently from one that doesn’t exist; both read the same now.

v0.3.0

2026-11-01

v0.3.0

Minor release - may break, under 0.x

Released
Entries
79
  • Added6
  • Changed18
  • Removed8
  • Fixed47

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.

Added6

  • 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.

Changed18

  • 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.

Removed8

  • 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.

Fixed47

  • 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.

v0.2.3

2026-10-27

v0.2.3

Patch release

Released
Entries
50
  • Added8
  • Changed11
  • Fixed25
  • Security6

The Configurator lands - options with rules, templates, pictures and a step-by-step picker on the product page - along with a full media picker and a faster dashboard. One thing to do when you upgrade: migrations now run as the backend starts, so the migrate, signals-migrate and clickhouse-migrate services are gone from the compose files. Drop any docker compose run --rm migrate from your deploy scripts and any override that names those services; the server brings the schema up to date on its own before it serves.

Added8

  • Configurator templates: a set of options and rules kept once and used on many products. Templates live in a product’s Configurator section - “Use a template” adds one, “Manage templates” lists them all to create, use or open one. A template gets its options, rules and map in the same editor a product has, a currency its flat charges are in, and a “Used by” list to add products to. A product can use several, in an order you pick; their options show first on the product page and in the cart, before the product’s own, and an edit to a template reaches every product using it while orders keep what was picked. On the product, template options show read-only under the template’s name, the product’s own options and rules can read their answers, and “Detach” copies a template onto the product as its own when one product needs it changed. A template still in use can’t be deleted, and one the product’s own options still read can’t be taken off.

  • Product types can hand templates to all of their products. A product type’s new Configurator templates section puts its templates on every simple and configurable product of the type, ahead of the ones a product picks itself, and a product shows them read-only with the way to the type. A template on both the type and the product shows once, and a configurable and its variants are asked and charged for it once. The type’s list can’t drop a template one of its products still reads unless that product picked it itself too.

  • Paged gift card and subscription queries. The admin API gains giftCardsPaged(search) and subscriptionsPaged(status), each returning one page with its totalCount, and PromotionRule.codes takes search and isActive.

  • ⌘S / Ctrl+S saves a dashboard page. It opens the same review dialog as the Save button, and does nothing while there’s nothing to save.

  • Options and their choices can have pictures. In the option editor, each choice gets a small picture slot and the option one of its own - pick from the media library or upload. On the product page, an option whose choices have pictures shows them as tiles to pick from, and an option’s own picture sits under its name, like where an engraving goes. Pictures load with the option rules in a query of their own, so the main product query stays under its complexity cap; deleting the picture from the media library just removes it from the option.

  • Products get a Configurator: options plus rules, “when these answers, then these effects”. The product options section is now the Configurator, and its options can carry rules that react to what the customer picked - make another option required or optional, fix and lock the cart quantity, or move the price by an amount or a percentage (per store, like option charges). The cart applies them on add, a price change lands on the line as its own row named by the rule (“Gift set discount”), the storefront previews required options, quantity and price live, and the cart console follows them too. Rules are edited in a dialog next to the options, and a new map view lays the options out top to bottom the way customers meet them, with conditional options branching off the answers that show them and rules pointing at what they change.

  • Step-by-step configurator on the product page. A product’s Configurator has an “On the product page” setting: all options at once, as before, or step by step. Step by step walks customers through the visible options one at a time with a progress bar, keeps their earlier answers above the current step with a link to change each, holds them on a required option until it’s answered, lets them skip an optional one, and ends on a review before adding to the cart. Steps follow the conditions, so an answer that shows or hides a later option changes the count as they go, and the add button’s “missing option” jump brings that step up.

  • PaymentMethodOption.isOffline on the storefront API says whether a payment method has no third party behind it (cash on delivery, bank transfer, pickup, purchase order).

Changed11

  • The dashboard starts faster and idles quieter. The sign-in page downloads less than half as much (576 KB to 260 KB gzipped), and a signed-in page 381 KB instead of 591 KB. Hidden workspace tabs stop polling, and a mutation refetches only the queries on screen.

  • A refused delete in the dashboard now says why, in its confirmation. Deleting a collection, a collection translation or a CMS page used to close the dialog and drop the server’s reason; it now stays open and shows it. Rotating or deleting a webhook, removing a product download, image or URL redirect, and clearing a segment show their failure in the dialog instead of behind it or in a toast.

  • The dashboard settles into place instead of popping in. A reload paints the shell’s frame in its own colours straight away rather than a white page. The sidebar and the home page hold skeletons until your permissions and saved layout are in, then appear once, so sections no longer reorder under you. Stat tiles keep their place while loading instead of showing 0, and page loaders too brief to read no longer flash.

  • The header is a little shorter, 48px instead of 56.

  • Dashboard confirmations use the dashboard’s own dialog. Deleting a workspace, a media asset or directory, a shipping method, an email or email group, or a promotion code, and changing a promotion condition’s type, no longer pop up the browser’s native confirm box.

  • A full media picker everywhere the dashboard picks from the library. CMS image fields and attributes, promotion badges and the email builder now open one large modal with the library’s folder tree, search across names, titles, alt text and captions, sorting (newest, oldest, name, largest), grid or list view with two tile sizes, and a details panel to fix alt text (or write it with AI), title, caption and the crop focal point without leaving. Files upload by drag and drop, paste or the Upload button into the open folder, several at once with progress, and land already picked. Fields that take several assets pick several at a time, and the arrows and Enter work the grid. It keeps loading as you scroll, however big the library is. Media search takes a sort order to back it, with a stable order so paging never skips or repeats a file.

  • Migrations run as the backend boots, with no migrate containers. The server applies the commerce schema, the plugins’ migrations and the data patches before it serves; the signals and ingest processes do the same for the Signals database and the ClickHouse spine. Processes booting together take turns on a lock, so each migration runs once, and a boot refuses to start if a migration it applied has since been edited. skelet migrate runs the commerce step by hand for a deploy that wants it first, and the prod images no longer ship dbmate.

  • Configurator edits no longer re-index products in search. Changing a product’s options, a template or a product type’s templates used to re-index every product it reached, though search holds none of it - a template on a product type with thousands of products meant thousands of re-indexes. Caches and webhooks still hear about each product, now from batched reads.

  • Product options are edited in one dialog. Each option opens a wide editor with its name, how customers answer (picked from described cards), its choices or charge, and when it shows - instead of separate inline forms for the option and each value. The list reads as a summary: choices with their charges, character and file limits, and conditions in words (“Only when Wrapping is Gift”). Charges are typed in money (“3.50 USD”, per-store amounts in each store’s currency) rather than cents, file limits in MB, options reorder with arrows instead of sort numbers, and a choice can be linked to a product - or several products added as choices at once - through the shared product picker. A staged delete stays visible with an Undo.

  • Product recommendations get the full width. Related, upsells and cross-sells are tabs over one sortable table (image, name, SKU, reorder and remove), each tab shows its count and flags unsaved changes, and products are added through the shared product picker. The inline search box is gone - the picker searches too. A list past ten products scrolls inside its own frame and gets a filter box; reordering pauses while a filter is on.

  • Product and category pages make about 40% fewer database queries. An uncached product page went from 32 queries to 19 and a category page from around 35 to 22. Stock is read once per product, the per-question review breakdown is only built when a query asks for it, and attribute, product type and translation metadata is shared across requests for up to 30 seconds. Edits to translations, product type names and attribute groups can take up to 30 seconds longer to reach the storefront.

Fixed25

  • Every shopper cart, gift card, subscription and promotion code can be reached in the dashboard. These lists used to load the first 100 (1000 for codes) and page or search only those; they now page and search on the server.

  • The sidebar’s resize line sits on the content’s edge and stays on top all the way down, instead of running 8px into the page and disappearing under the blurred page title.

  • ⌘K finds every dashboard page, not only the ones in the sidebar, still limited to what the admin may open. Signup forms, web forms and the two Segments pages no longer share titles.

  • The category and media folder trees work from the keyboard, icon-only buttons have names, and form labels point at their inputs, so screen readers announce what each control is.

  • The nightly sweep of abandoned customer option uploads no longer removes an asset that a product option or choice uses as its picture, and option pictures upload to their own option-pictures library folder.

  • Saving product options writes in an order the backend accepts: a reorder together with a new condition, or removing a condition and the choice it named, no longer fails partway through. Moves that would put a condition above the option it reads, and deletes of an option or choice a condition still uses, are refused up front instead of at save.

  • productsByIds returned every product without a name, and the admin cache spread that blank over names other queries had loaded - recommendations showed SKUs where names belonged.

  • Opening a product picker on a product page no longer reloads the page and closes the picker. The picker’s type filter replaced the page’s cached type attributes with a narrower copy.

  • Recommendations past the first 100 linked products (across all three lists) showed as “Unavailable product” - productsByIds answers at most 100 ids, and the editor asked for all of them at once. It now asks in batches.

  • A struggling OpenSearch no longer takes checkout down with it. When OpenSearch keeps failing, the storefront stops asking it for a few seconds and lets a single request check whether it’s back. Postgres still answers in its place, but for at most a quarter of DB_MAX_CONNS at a time, so carts and checkout keep their connections. Past that, a search comes back empty instead of queueing.

  • A browse that fails on every search tier is no longer cached as an empty catalogue for a minute.

  • The section index on long detail pages no longer covers buttons. While you scroll it shows only its ticks, which fit in the page’s right margin, and its labels open over the page only when you point at it or tab to it - before, they spread over buttons at the right edge, like a section’s “Use a template”, and could catch the click meant for them.

  • The backend’s response cache now actually serves the storefront. The storefront never sent an operation name and the cache only recognises queries by name, so no storefront read was ever cached there. It sends one now, and a few more public queries joined the cacheable list: load-more on listings, product compare, blog authors and taxonomy, the store product lookup, the free-shipping threshold and channel locales.

  • A storefront purge can’t be refilled with the old answer any more. Each copy the storefront caches names its purge surface, and a purge gives that surface a fresh epoch in the backend’s response cache before the storefront hears about it, so the refetch always reaches current data.

  • Settings → Cache now controls the storefront. cache/<surface>/enabled and ttl_seconds showed in the admin but were never read. A disabled surface now refreshes every second while staying purgeable, and an enabled one keeps its data for the TTL you set.

  • Cached responses are stored as they were sent rather than base64 inside JSON, so the response cache needs 15-30% less memory per entry.

  • Vetrina edits the shopper’s own cart, and no other. Signed-in shoppers can change, remove, save and estimate cart lines without a cart cookie, because every cart action now resolves through the customer session or the guest cookie. Only adding to the cart creates a guest cart, so a coupon link without a cart waits for the first addition. A backend failure fails the action instead of switching to another cart. The cart in the browser also reloads after sign-in merges the guest cart, and after an edit fails.

  • Vetrina shows and reports money at each currency’s own precision. Prices for a currency with zero or three decimals (JPY, BHD) were divided by a hundred everywhere. Product structured data, view_item, begin_checkout and purchase events and code-only price formatting now use the store’s configured decimals, or ISO 4217’s when only a currency code is known. Order pages convert with the order’s own currency.

  • Vetrina account actions keep the shopper’s language. Signing in or registering, password resets, verification emails, addresses, subscriptions, saved cards, loyalty redemptions, reviews and logout redirected to unprefixed paths, which sent a German shopper back to English. They now go to the localized path. Return destinations keep their query, and a path that already carries the locale, such as /de?welcome=1, no longer gets a second prefix.

  • Vetrina only takes orders it can actually get paid for. The demo storefront ships no payment gateway SDK, so checkout now offers only offline methods, and it re-checks before placing the order that the chosen method is still eligible. If a gateway still asks for a payment action, the shopper lands on the order marked “created, payment not complete”. Before, the storefront tried to finalize the payment itself and redirected as if it had succeeded. Orders fully covered by gift cards or store credit still need no method.

  • A Vetrina coupon link applies whichever way the shopper adds to the cart. A code held from a promotion link was applied only after simple and configurable-product additions. Bundles, grouped products and quick order now apply it too. It applies exactly once, and a grouped or quick-order batch where only some items were added still gets it.

  • Vetrina’s search suggestions always match what was typed. A slow response for an earlier query could replace the results for the current one, and a response that arrived after the search dialog closed showed up when it reopened. Only the response for the current query in the open dialog is shown now.

  • Sorting Vetrina search results keeps the search. Choosing a sort order on /search?q=shirt dropped the search term and any query-string filters. Sort links now keep them and go back to the first page.

  • Videos play in Vetrina CMS pages and product content. The storefront’s own CSP blocked YouTube and Vimeo players (there was no frame-src) and video files (there was no media-src). Embeds now always use YouTube’s privacy-enhanced youtube-nocookie.com host or Vimeo’s player, and the CSP allows exactly those two players plus same-origin, upload-preview and HTTPS video (HTTP too in development). Embed links for any other host are no longer rendered. Who may frame the storefront is unchanged.

  • An item moved from the Vetrina wishlist to the cart stays out of the wishlist. The browser’s own copy of the wishlist kept the moved item, and the next guest-wishlist import added it back. The local entry is now removed before the page refreshes.

Security6

  • golang.org/x/net v0.62.0. Fixes quadratic-time text coalescing in its HTML parser (GO-2026-6860), which the SEO link crawler runs on every page it fetches, so a crafted page could stall a crawl. The other golang.org/x modules move with it.

  • No pnpm audit findings in either app. body-parser 2.4.0 (a denial of service on compressed request bodies) and fast-uri 3.1.9 (four URI parsing advisories), both already inside their parents’ ranges. Only dev tooling reaches them - the shadcn CLI and Sentry’s webpack plugin - so nothing that ships was exposed.

  • Frontend lockfiles refresh weekly. Renovate’s lockfile maintenance re-resolves the dashboard’s and each storefront’s transitive dependencies within their ranges, so a patched release reaches them without an override. Both lockfiles are refreshed now, which clears the markdown-it advisory and drops every audit override but @graphql-tools/utils, whose parent still excludes the fix.

  • Go 1.27.3. Distrusts the compromised intermediate certificates in crypto/x509 (GO-2026-6809).

  • Next.js 16.4.2 in Vetrina. Clears the high-severity advisory against Next.js 16.0.0 through 16.4.0 that pnpm audit reports.

  • The demo storefront no longer records shoppers’ private pages. Session replay and heatmap snapshots skip account, cart, checkout, order, order-tracking, gift-card, email-verification, shared-wishlist and preview pages, with or without a locale prefix, and the signed-in name in the header, the masquerade banner and the sign-in drawer are blanked everywhere. Page views and click heat still count those pages. Replay pauses on a private page and resumes with a fresh snapshot on the next public one. A storefront test fails when a component renders a customer’s name, email or phone outside those pages without blanking it.

v0.2.2

2026-10-18

v0.2.2

Patch release

Released
Entries
3
  • Security3

Security3

  • Go 1.27.2 and golang.org/x/net v0.60.0. Fixes the standard library’s HTTP/2 and HTTP/1 desync issues, the MIME header memory limit bypass, ECH validation in crypto/tls, two html/template escaping bugs and os.Root following junctions on Windows, plus the matching HTTP/2 fixes in x/net. golangci-lint moves to v2.14.0 and golang.org/x/tools to v0.50.0, the first releases that read 1.27.2’s export data, which gqlgen needs too.

  • Next.js 16.3.8, fixing a server-side request forgery in image optimization, with sharp 0.35.5 for its bundled librsvg.

  • No high or critical pnpm audit findings in either app. Patched floors pin the flagged dependencies (shell-quote, fast-uri, undici, brace-expansion, busboy, source-map-js, proxy-addr, the MCP SDK and two GraphQL Tools packages) within their current major lines. braces has no fixed release and only build tooling reaches it, so it’s recorded as a reviewed exception.

v0.2.1

2026-10-18

v0.2.1

Patch release

Released
Entries
4
  • Added1
  • Changed1
  • Fixed2

Added1

  • Secret scanning in CI. A gitleaks job scans the whole history on every push and pull request to main and develop; reviewed test fixtures are listed in .gitleaksignore.

Changed1

  • Core requires the SDK at v0.1.0, the version it declares, instead of v0.0.0, so a plugin in its own repository can fetch core once the module tags (apps/backend/core/vX.Y.Z, apps/backend/sdk/vX.Y.Z) are pushed. The release guide covers tagging them.

Fixed2

  • Browser uploads work behind the shipped CSPs. The dashboard, storefront and Caddy policies only allowed connections to their own origin, so every upload to object storage was refused. UPLOAD_ORIGINS in the root .env names the hosts uploads go to and joins connect-src in all three; dev allows its local hosts on its own. ./skelet release now passes a Dockerfile’s build arguments through from your shell or the root .env, which it never did.

  • The demo storefront’s tab icon is Skelet’s, not the Vercel triangle create-next-app left behind.

v0.2.0

2026-10-18

v0.2.0

First in this changelog

Released
Entries
9
  • Added9

First public release, and where this changelog starts. The v0.1.0 and v0.1.1 tags came before anyone outside the project ran Skelet, so their history lives in git rather than here.

A fresh clone comes up as a working shop, so trying it is cheap. The plugin SDK, the remote-provider contract and the product each carry their own version, so a plugin states its own compatibility range instead of tracking the platform.

Added9

  • Commerce core in Go and PostgreSQL: catalog with simple, configurable, bundle, kit, grouped, digital and subscription products, custom options, carts, checkout, orders with partial fulfillment, a payment ledger, refunds and credit memos, inventory with reservations, promotions, tax, gift cards, store credit, loyalty, referrals, wishlists and reviews.

  • Two GraphQL surfaces, admin and storefront, with separate schemas, so storefront introspection can’t see an admin operation.

  • Multi-channel and multi-locale throughout: one catalog serving several storefronts in different currencies and languages, with per-channel listings, permissions and settings.

  • Content: a block-based CMS, a blog, URL rewrites and redirects, web forms and a product feed for merchant platforms.

  • Admin dashboard in React, with staged saves, saved views, split panes and language packs, and a reference storefront in Next.js.

  • Signals, an optional analytics suite in its own process and database: Lens for web analytics and session replay, Synapse for customer profiles and segments, Pulse for email campaigns and flows, Prism for experiments. Its event spine lives in ClickHouse, and SIGNALS_ENABLED=false runs the commerce side alone.

  • Plugins compiled into the binary from their own Go modules (./skelet cli plugin add), with an SDK at 0.1.0, plus remote providers over a versioned wire contract for shipping and promotions. Offline payment methods are built in; card gateways are plugins.

  • Platform: a runtime config registry with encrypted secrets, integration tokens, HMAC-signed webhooks, a transactional outbox, scheduled jobs with retries, search on Postgres or OpenSearch, bulk import and export, image transforms, and object storage on S3 or a local directory.

  • Operations: everything in containers, a small single-container deployment shape and a split production one, health and readiness endpoints, and opt-in OpenTelemetry and Sentry.

Versions

Which number means what

Version numbers that move on their own

A release tag versions the whole product, and core's Go module takes the same number. The plugin SDK and the remote-provider contract carry numbers of their own and only move when they change - the product reaching 0.4 doesn't mean the SDK did. No compatibility promise holds until 1.0.0.

Release tag
vX.Y.Z - the whole product
Core module
apps/backend/core/vX.Y.Z - the same number, for plugins fetching core
SDK
sdk.Version - the plugin API surface
Provider contract
contract.Version - the remote-provider wire format
Plugins
Manifest.Version - each plugin's own