#Deploy to Azure
StoryLark runs on Azure the same way it runs on Cloudflare: one deployment per brand, from the same codebase. This guide provisions a branded site on Azure App Service, PostgreSQL Flexible Server, and Blob Storage.
The supported publisher path is the same installer used for Cloudflare:
npm create storylark my-site -- --deploy
Choose Azure in the wizard. It installs compatible StoryLark packages, writes
the Azure configuration, verifies your CLI session and template, and asks for
confirmation before provisioning. Use npm create storylark my-site without
--deploy, followed by npm run doctor and npm run setup, when you want to
review the project first. The commands below explain and troubleshoot what the
installer does; cloning the engine is not required.
Full detail (env vars, publishing content, how the Azure path differs
internally) lives in platforms/azure/README.md
— this page is the quick path. For the exhaustive reference — every binding,
environment variable and secret the worker reads on either platform,
migrations, cron triggers, and custom domains — see
deploy-worker.md.
#What gets created
platforms/azure/infra.bicep provisions, per brand:
| Resource | Purpose |
|---|---|
| Azure App Service (Node 20, Always On) | Runs the app + API (platforms/azure/server.mjs) |
| PostgreSQL Flexible Server + database | Accounts, sessions, progress — the database driver |
| Storage Account + public Blob container | Published content — the storage driver |
#Picking a region — check before you deploy, not after
Not every Azure region works for every resource on every subscription.
This isn't about which regions Azure offers in general — it's about what
your specific subscription is allowed to provision, which varies
per-subscription and can differ for each resource type independently. On
the subscription this was built and tested against, eastus turned out to
be blocked for both resources this template needs (Postgres Flexible
Server provisioning restricted; zero App Service B-series VM quota), while
centralus had neither problem. There's no way to know in advance which
region is open for your subscription — you have to check.
Before setting AZURE_LOCATION, run both of these checks:
- Is PostgreSQL Flexible Server provisioning open in this region?
An empty/null result means it's open. Any text back (e.g. "Provisioning is restricted in this region...") means it's blocked for your subscription — try a different region for this check.az rest --method get --url "https://management.azure.com/subscriptions/<sub-id>/providers/Microsoft.DBforPostgreSQL/locations/<region>/capabilities?api-version=2025-08-01" --query "value[0].reason" - Does this region have App Service compute quota for the SKU you want?
Ifaz vm list-usage --location <region> --query "[?contains(localName, 'BS Family')]" -o tableCurrentValueequalsLimit(often0/0), there's no quota here — either pick a different region or use the freeF1tier (APP_SERVICE_SKU=F1ininstall.env— see the note on F1 below).
If the two checks disagree — say Postgres is open in eastus but App
Service quota is only open in centralus — you don't have to pick one
region for everything. AZURE_LOCATION covers App Service + Storage;
DB_LOCATION (defaults to AZURE_LOCATION if unset) covers Postgres
independently, so the database can live in a different region from
everything else. This template does that by design.
#Steps
- Fill the installer's env file:
Setcp platforms/azure/install.env.example platforms/azure/install.envBRAND_ID,AZURE_RESOURCE_GROUP,AZURE_LOCATION,DB_ADMIN_PASSWORD,APP_NAME— run the region checks above first.APP_NAMEis the human-readable display name (WebAuthn passkey prompt, transactional emails); set it to the same value you'd use for a Cloudflare install of the same brand. Seeinstall.env.examplefor the optional overrides (DB_LOCATION,APP_SERVICE_SKU,BRAND). - Verify before provisioning anything (creates nothing — checks your
values, that you're logged into Azure, and that the infrastructure
template compiles):
cd platforms/azure node install.mjs --verify - Deploy (creates real resources and real cost — confirm the plan
with whoever approves cloud spend before running this). This one command
does everything: provisions the infrastructure, applies the database
schema, builds the app for your brand, and deploys the app code to the
App Service.
It prints the live URL when done.node install.mjs --deploy --yes - Publish content through Azure Blob instead of the Cloudflare default:
Seenpm run publish -- --storage azure-blobauthoring-stories.mdfor the content format.
#Things that go wrong (found by actually deploying, not just reading the template)
ParameterOutOfRange: 'Version' should be in: []on the PostgreSQL resource, orSubscriptionIsOverQuotaForSkuon the App Service Plan. You skipped (or need to redo) the region checks above — see "Picking a region." Note onF1: the free tier doesn't support Always On (the template already handles this) and has a small daily compute quota that a crash-loop can burn through fast — it's a workaround for zero App Service quota, not the recommended target for anything beyond a quick test.extension "citext" is not allow-listed for usersduring migration. Already fixed ininfra.bicep(anazure.extensionsconfiguration resource allow-lists it) — if you're deploying against an existing server that predates this, allow-list it manually:az postgres flexible-server parameter set --name azure.extensions --value citext.
#Nothing about your brand changes
Switching platforms never touches brands/<id>/ — the same theme,
presentation, and content publish identically whether the site runs on
Cloudflare or Azure. Only the infrastructure underneath differs.
Found a gap? StoryLark is open source — improve these docs on GitHub.