StoryLark
← All guides

Updating

How your deployed site receives platform updates — from the operator's chair, not the code's.

Nothing updates without you

StoryLark never updates itself. There is no background process that changes what's running and no schedule that applies anything. What there is: the site tells you a new release exists, and you take it — normally with one button in /admin, always available as one command from your own machine.

The portal path is the same on every platform, because of how it works underneath: the common release only changes the engine (the app your readers load), and the site installs that into its own storage — the same storage your content and themes already live in — then serves it from there. No deploy API, no credential, no rebuild, identical code on Cloudflare, Azure, and anything else that runs StoryLark.

Part 1 — how you find out

Two ways, both free of credentials:

  1. Check yourself — open /admin on your site. The "Platform update" card shows what you're running versus the latest release, with a link to the release notes, and a Check for updates button. The check is an unauthenticated read of the public npm registry.
  2. Get told — if you've set ADMIN_EMAIL and RESEND_API_KEY, a daily check emails you when a new release exists. You never have to remember to look. See admin-guide.md for setting these up.

Part 2 — Update now

Press Update now in /admin and the site:

  1. downloads the prebuilt engine for that release — a single zip attached to the GitHub Release, downloaded with no credentials because it is a public file;
  2. checks it against the sha256 published beside it, and refuses to go on if it does not match;
  3. checks the package's own per-file checksums, and refuses any package that contains a brand.json, theme.css, presentation.json, manifest.webmanifest or icons/ — those are yours, and a release artifact carrying them would be somebody else's identity;
  4. applies any pending database migrations;
  5. switches the site over.

Step 5 is where the mechanism differs, and it is the site's business, not yours:

Rolling back

The card keeps the last five installed engines and shows them with a Roll back to this button. Rolling back restores exactly the bytes that were installed — no download, no redeploy — and Serve the built-in engine instead returns to whatever your deployment was built with. The history survives both, so every move here is reversible.

If a release involved a database migration, note that rolling the engine back does not undo the migration — migrations are written to be additive, so old code under new schema is a supported state.

The command — the floor that always works

From your copy of the site, on the machine you already deploy from:

# Cloudflare
node platforms/cloudflare/install.mjs --update --yes

# Azure
node platforms/azure/install.mjs --update --yes

The /admin card shows the right one for your platform, ready to copy — always, whatever state the button is in. The button depends on a release artifact existing and your platform having a good day; the command depends on neither. It pulls the latest engine packages from npm (pinned exactly), migrates, rebuilds with your brand untouched, and redeploys with the platform login you already have. It installs nothing on the deployment, stores no new secret, and creates no infrastructure. It is safe to run repeatedly — including as the way to repair a deployment that has drifted.

--update refuses to run without --yes, for the same reason --deploy does: it changes a live site.

What never changes

Your brand — brands/<id>/brand.json, theme.css, icons — your presentation and deployment config, and your content are never touched by an update, by construction, not by care: the engine artifact format rejects a package containing any brand file, and the installer re-checks every path before it writes. There is no code path in the update mechanism that can write your identity.

When a new engine release adds a UI feature, it always ships with a sensible default look, so your existing theme keeps working exactly as before.

Skipping a release

Nothing to do. If you don't press the button, nothing happens — no PR to close, no opt-out step. The portal keeps showing the newer version until you take it (or a later one supersedes it).

Before you update, on Postgres

Cloudflare D1 has time-travel recovery built in, so a bad migration is recoverable without preparation. Azure Postgres has no in-place undo, so take a snapshot first if the data matters to you:

pg_dump "$DATABASE_URL" --format=custom --file="pre-update-$(date +%Y%m%d%H%M%S).dump"

DATABASE_URL is an app setting on the Web App — read it with az webapp config appsettings list. This is a deliberate manual step rather than something the updater does silently: where your database backup goes is your decision, not the installer's.

Self-update for API-server releases — how it's set up, and how to turn it off

Engine releases need nothing: the storage the site writes is storage it already has. The one thing that needs a permission is a release that changes the API server, and the installer provisions that as part of a normal --deploy (and --update), telling you exactly what it did:

Turning it off:

node platforms/<cloudflare|azure>/install.mjs --disable-one-click --yes

This also records SELF_UPDATE=off in install.env, so a later --update does not silently re-enable it. With it off, the portal still updates engine releases exactly as before; for a release that changes the API server it says plainly that self-update is off for this deployment, how to turn it back on, and shows the command that always works.

What the button deliberately cannot do

If it fails

The portal shows the log and the error. Every failure before the switch is a pure no-op — download, checksum and package validation cannot touch the deployment. Take the update with the command instead; that path needs none of this and is always available.

Running an update from CI instead

Nothing stops you: the update command is an ordinary Node script, so a CI job can run it with a platform credential held as a CI secret. But it is your CI holding your credential, on a runner you control — it is not something the deployed site can trigger.


Found a gap? StoryLark is open source — improve these docs on GitHub.