Upgrading
A site installed with 0.3.39 (March 2026) or later upgrades in place to any later version, and can skip versions: migrations run in order on deploy. They only go forward, so the way back from an upgrade is a backup taken before it. Compatibility lists what each kind of release can change.
Before you upgrade
- Back up the database and media. Backups and recovery has the steps for each deployment.
- On a 0.x release, work through Upgrading from 0.x.
Upgrade
Cloudflare
npm install @jant/core@latest
Commit package.json and the lockfile and push to main. The deploy workflow, or Cloudflare itself for a one-click deploy, deploys the new version and runs its migrations. Without either, deploy with npm run deploy.
Install with @latest. npm update stays inside the range in package.json, such as ^0.11.0, and never reaches the next major version.
jant deploy runs the migrations before it uploads the new Worker, and the old Worker keeps serving in between. When the release notes say a migration replaces a table, don't write to the site until the deploy has finished: writes in that window aren't safe.
Docker Compose
docker compose pull
docker compose up -d
up runs the migrations in jant-migrate before it starts the site. If .env pins IMAGE to a version, change it first.
Docker without Compose
Pull the image and run the migrations with the same data directory:
docker pull owenyoung/jant:latest
docker run --rm \
-e AUTH_SECRET=<auth-secret> \
-v "$(pwd)/data:/var/lib/jant" \
owenyoung/jant:latest \
jant migrate
Then stop and remove the old container, and start one from the new image with the command in Deploy with Docker. The container doesn't run migrations itself.
Node
Install the new @jant/core, run jant migrate, then restart jant start.
Hosted Jant
The service upgrades your site.
Upgrading from 0.x
Each item says what a site looks like on the current version, so it holds whichever 0.x release you start from. Skip the groups that aren't about you.
Do these first
- Back up with a method your old version supports. Before 0.11,
jant db export --remotefails on D1, so use D1 Time Travel. On Postgres, usepg_dump: no version restores asite snapshottaken there before 0.11. On SQLite, copy everyjant.sqlite*file, not onlyjant.sqlite. - Convert old text attachments on 0.7.1. A site with text attachments added before 0.3.42 (April 2026) that never ran
jant migrate-text-attachmentsruns it before going further, since 0.8.0 removed the command. Install@jant/[email protected]and deploy it, setINTERNAL_ADMIN_TOKENon the site and in your shell, runnpx jant migrate-text-attachments --url https://<your-site>, then upgrade to the latest version. - Check your environment against Everyone. From 0.11, a value a setting doesn't take stops the site at startup.
Everyone
| Variable | On the current version |
|---|---|
AUTH_SECRET |
At least 32 characters, without replace-me |
Switches, such as NOINDEX, PUBLIC_API_ENABLED, RSS_FEEDS_ENABLED, TRUST_PROXY |
true or false. 1, 0, yes, and on stop the site |
RATE_LIMIT_DISABLED |
No longer read. Set RATE_LIMIT_ENABLED=false instead |
RATE_LIMIT_SEARCH_PER_MIN |
0 means no limit. It used to mean 30 |
CORS_ORIGINS |
Empty turns cross-origin access off; unset or * allows every origin. A cross-origin write that carries the session cookie needs its origin listed by name |
STORAGE_DRIVER |
r2, s3, or local, in lowercase |
DISCOVER |
latest or off. A site that offered Discover its featured posts is now off; turn Discover on again under Settings → General to stay listed |
SITE_ORIGIN |
Scheme, host, and port only, such as https://example.com. A subpath goes in SITE_PATH_PREFIX |
SITE_LANGUAGE |
A language tag such as zh-CN, not zh_CN |
PAGE_SIZE, ARCHIVE_PAGE_SIZE, RSS_FEED_LIMIT, SUMMARY_MAX_CHARS, UPLOAD_MAX_FILE_SIZE_MB, SLUG_ID_LENGTH |
Within the ranges in Configuration. PAGE_SIZE defaults to 25; set 50 for the old page size |
DEV_API_TOKEN |
Not set in production. It's a local debugging aid |
Data backfills run with the migrations, in jant deploy and in Docker Compose. If your deploy calls wrangler deploy or wrangler d1 migrations apply itself, run jant migrate once (npx jant migrate --remote on Cloudflare).
Cloudflare sites
Make these changes in the same commit as the new @jant/core.
.github/workflows/deploy.yml: replace the whole file with the current one. Older copies skip every deploy, or deploy without the backfills:curl -fsSL -o .github/workflows/deploy.yml https://raw.githubusercontent.com/jant-me/jant-starter/main/.github/workflows/deploy.ymlThat file installs with npm. For a project made with pnpm or Yarn, create a new project with the same package manager, such as
pnpm create jant jant-new, and copy its.github/workflows/deploy.yml.wrangler.toml:keep_vars = trueat the top level, before the first[section]. Without it, every deploy deletes the variables you set in the Cloudflare dashboard. Check thatR2_PUBLIC_URLandIMAGE_TRANSFORM_URLare still there, and set them again or move them into[vars].package.json: the scripts aredev,deploy, andreset-password, as in a new project. Remove anexportscript;npx jant db exportreplaces it.tailwindcsscan go too.AGENTS.md: replace it with the current one, and delete.agents/skills/and.claude/skills/:curl -fsSL -o AGENTS.md https://raw.githubusercontent.com/jant-me/jant-starter/main/AGENTS.md rm -rf .agents/skills .claude/skills
Docker and Node
compose.yml: replace it with the current one. A copy from before April 2026 never runs the migrations ondocker compose up:curl -O https://raw.githubusercontent.com/jant-me/jant/main/compose.ymlTRUST_PROXY=truebehind a reverse proxy, or every visitor shares one sign-in rate limit.compose.ymlsets it; withdocker run, pass it yourself..env:jant startand thejantcommands read.envin the working directory. Rename.env.nodeto.env, or setJANT_ENV_FILE=.env.node.jant startdidn't read.envbefore, so check what it holds.Health checks:
/healthzand/readyz./healthis gone.@jant/core/nodeis gone; start the server withjant start.
API clients
| Before | Now |
|---|---|
GET /api/upload, and GET, PATCH, or DELETE /api/upload/:id |
GET /api/media, and GET, PUT, or DELETE /api/media/:id |
PATCH /api/media/:id |
PUT /api/media/:id |
GET /api/posts/:id/content |
GET /api/posts/:id?content=markdown, then read bodyMarkdown |
GET /api/public/posts (the list) |
GET /api/public/threads. GET /api/public/posts/:slug stays |
GET /api/public/archive |
GET /api/public/threads?visibility=any&sort=published |
/api/upload/multipart |
Upload sessions at /api/uploads, or one POST /api/upload |
POST /api/collections/:id/posts with postId, DELETE /api/collections/:id/posts/:postId |
POST /api/collections/:id/threads with threadId, DELETE /api/collections/:id/threads/:threadId |
/api/collections/sidebar-items |
/api/collections/directory-items |
POST /api/export/zola |
POST /api/export/hugo |
Appearance settings sent to PUT /api/settings/import |
PUT /api/settings |
- Sign-in:
GET /api/search,GET /api/collections,GET /api/collections/:id, andGET /api/nav-itemsneed a session or an API token. Search results include private posts; readers search on the/searchpage. - Paging: pass
nextCursorback as it is. A post or media ID ascursoranswers400.GET /api/custom-urlspages withlimitandcursor, and answers{ customUrls, nextCursor }. - Order:
GET /api/postslists bypublishedAt, newest first. Pinned posts no longer come first. - Errors:
{ error, code }, plusdetailsfor a validation error. Branch oncode. - Removed fields:
siteId, everywhere. On posts,previewImageKey,previewKind,previewProvider, andtranslationGroupId. On media,filename(readoriginalName),provider, andposition(read the order of a post'sattachments). - Uploads:
POST /api/uploadandPOST /api/uploads/:id/completeanswer201with the media object. AmediaIdalready attached to another post answers409; upload the file again for the second post. - Values:
pinnedAtandfeaturedAttake Unix seconds ornull, andratingtakes a number.PUT /api/posts/:idrefuses a field the post's format doesn't take, such asurlon a note. - Smart collections:
sortissortOrder, and a single read or write returns the smart collection itself, not{ smartCollection }. - Custom URLs:
pathstarts with/, andredirectTypeis a number. - Text attachments in public posts: the file is at
url.contentUrlis gone. - Settings:
DELETE /api/settings/:keyanswers likeGET /api/settings, andPUT /api/settings/importanswers{ settings, rejectedKeys }.
A request field the API doesn't know is ignored, not refused, so check responses rather than waiting for an error.
MCP clients
| Before | Now |
|---|---|
jant_search_posts with query |
jant_posts_search with q |
jant_media_update_alt |
jant_media_update |
jant_posts_get_content |
jant_posts_get with content: "markdown", then read bodyMarkdown |
jant_collections_add_post, jant_collections_remove_post (postId) |
jant_collections_add_thread, jant_collections_remove_thread (threadId) |
A failed call's structuredContent is { error, code, details }. Cursors, mediaId, and the number fields follow the API rules above.
Custom CSS
| Before | Now |
|---|---|
--site-elevated-bg |
--site-page-bg |
--site-nav-hover-bg |
--site-subtle-bg |
--search-mark-bg, --search-mark-color |
--site-search-mark-bg, --site-search-mark-color |
--site-accent for links |
--site-content-link |
--foreground for post text |
--site-reading-body |
--site-width, --content-max-width |
--layout-body-max-width |
--site-padding, --content-gap between posts |
--site-feed-rhythm |
--card-radius, --card-padding, --card-border-width, --card-shadow, --site-accent-text, --fw-light, and --fw-extrabold are gone; a var() that reads one gets its fallback.
- Dark mode: write overrides for
:root[data-theme-mode="dark"], or for:root:not([data-theme-mode="light"])inside@media (prefers-color-scheme: dark). A plain:rootrule in custom CSS wins over the theme, so:root:rootisn't needed. data-pageis on<body>. A selector that joined it with another attribute needs a space:[data-page="post"] article[data-post].- Quotes: the quoted text is
[data-post-quote]. Nested[data-post-media] [data-post-media]is now[data-post-media].
Theming lists every variable and attribute.
Exports and Hugo templates
- On Collection pages,
.Params.summary_textis.Params.description. - Media entries carry
srcandposter.provider,storage_key, andposter_keyare gone. - A copy of
featured-thread.htmlin your ownlayouts/needs the theme's current version: gaps link to the first hidden post. - An export from 0.11 or later doesn't import into 0.10 or earlier. Upgrade the receiving site first.
jant site importreadsjant-site-export.zipby default. To import a directory, pass--path <dir>.- An export from before 0.7.0 may not import. Export again after upgrading.
Addresses
- A post or Collection with several custom URLs lives at the oldest one. To move it to a newer one, delete the older.
- A custom URL whose first segment starts with
_or.answers404. Give the post a new custom URL and delete the old one. - A redirect to another site gets its
//back after the upgrade, but stays in lowercase. Edit any whose letter case matters.
Command line
| Before | Now |
|---|---|
jant export |
jant db export |
jant import-site |
jant site import |
jant search-reindex |
jant search reindex |
jant site localize-media |
jant site pull-media |
jant site export <url>, jant site import <url> |
--url <url> |
jant site export --directory <dir> |
--output <dir> |
jant telegram register-webhooks --base-url |
--url |
jant deploy --site-path-prefix |
--path-prefix |
jant --remote migrate |
jant migrate --remote: options go after the command |
jant uploads cleanup for one batch |
jant uploads cleanup --once |
Command line lists every command and option.
Release notes
Every release's notes are on GitHub: the @jant/core@<version> release has the full list, and the v<version> release only links to it. A change that asks you to do something carries Upgrade notes. From 1.0, a change to anything Compatibility promises comes only in a major release, and this page collects its upgrade notes before the release ships.