alepha@docs:~/lore/docs/apps$
cat 4-deploying.md | pretty
15 min read
Last commit:

#Deploying an app

Lore can deploy your app for you. You lend it a machine running Bay or a Cloudflare account, tell it which copies of your app exist, and then shipping is one command or one button. This page goes from an empty project to a rollback.

Deploying an app through Lore needs an app built with Alepha: what Lore ships is the output of alepha build. For anything else, Stacks deploy any Compose file.

#Do you need Lore to deploy?

No. alepha deploy -e production deploys an Alepha app with no Lore account: it reads the environments declared in your alepha.config.ts and uses the credentials on the machine running the command. That suits a short, fixed list of environments deployed from CI.

Deploy through Lore when the list is not fixed. In Lore an environment is something you create, so a copy that did not exist when the build was made can be deployed to without a rebuild: a preview per branch, or a copy per customer.

#The pieces

Thing What it is
Estate A Bay machine or Cloudflare account. It belongs to a project and is lent to the ones that deploy onto it.
App instance One deployed copy, named by a pair: an app and an environment. my-app in production.
Artifact A build somebody pushed, named by a tag. It never changes, except under the tag latest.
Deployment One run of "put this build on that copy", with its log.

See Apps and instances, Estates and Artifacts.

A new app can start on this path. npm create alepha my-app -- --infra lore (or alepha init --infra lore) sets the project in alepha.config.ts, adds @alepha/lore, wires telemetry, and links the directory to the project when you are signed in. After that, adding an estate and running alepha deploy (production by default, -e for another environment) is the whole loop.

#1. Turn deploys on

Turn on Deploy and watch apps in Settings ▸ General ▸ Capabilities, then open Settings ▸ Apps and turn on Deploy apps. See Capabilities.

Creating a copy needs the app:manage permission. Deploying, rolling back and changing what a copy runs with need deploy:manage. The owner holds both, and a rank can be given them.

#2. Add or borrow an estate

Open Settings ▸ Apps ▸ Estates and choose Add an estate. The estate belongs to this project and is lent to it. An estate another project lends to yours appears on the same page.

A Cloudflare estate needs an account id and an API token, and Lore checks the token against that account before the estate exists. See The Cloudflare token for the permissions it needs. Everything else about estates, lending included, is in Estates.

#3. Create the copy

  1. Open Apps and choose New app.
  2. Type the app and the environment. Both are free names: production, staging, eu-staging and pr-1841 are all fine, so you slice a fleet as finely as you want.
  3. Open the copy's Settings tab and pick its estate under Deploys to. Only estates lent to the project are offered.

Creating the copy creates nothing in your cloud account. Choosing its estate is what makes its Deploy and Environment tabs appear.

The estate is always chosen in Lore, never in alepha.config.ts. A first alepha deploy that creates the copy itself uses the one estate lent to the project, and refuses when several are lent, naming them.

#The name it gets in your estate

A copy's first deploy names everything it creates <project>-<app>-<env>, such as acme-shop-production: on Cloudflare the Worker, the D1 database, the bucket, the cache namespace and the queue, and on a Bay machine the instance, its directory and its default subdomain. The copy's Overview tab shows the name under Resources.

⚠️ That name is fixed on the first deploy and never changes. Cloudflare cannot rename a database, so a name that followed the project would create an empty one beside the live one. Renaming the project, the app or the environment afterwards moves nothing: acme-shop-production keeps its name under a project now called shop, and only copies deployed for the first time after the rename take the new one. A destroy keeps the name too, so a copy deployed again comes back to its own database.

On a Bay machine, the machine builds the instance name from the app and the environment it is sent. A deploy is refused when the copy does not exist in Lore or points at another estate. A copy whose app or environment was renamed after its first Bay deploy is refused too, instead of being started again as a new, empty instance: rename it back.

#Telemetry wires itself

If the app includes the reporting module (see Telemetry), there is nothing to set up. A deploy creates the copy's sigil and writes SIGIL_KEY straight into the copy's environment. The token never appears in your terminal.

bash
lore deploy                # a sigil is created, if the build reports
lore deploy --no-sigil     # none is created
lore deploy --sigil        # one is created, even for a build that does not report

⚠️ Only a fresh sigil can be stored. Lore holds a sigil's token for the moment it creates it and never again. A copy that already has a sigil and no SIGIL_KEY cannot be given one: rotate the sigil, or set SIGIL_KEY yourself. Without a flag, the deploy leaves such a copy alone and goes ahead. With --sigil, it tells you.

#4. Set what it runs with

The copy's Environment tab holds the variables it runs with. Add variable sets one, and Import .env takes a pasted or dropped .env file. A copy holds at most 100 variables.

Each row is one of two kinds:

  • A Variable is a name the app declares as not secret. Its value is shown, and Edit changes it.
  • A Secret is everything else. It is encrypted before it is stored and never shown again, to anyone: the tab shows at most its first characters. Replace sets a new value.

Three families of name are refused, and the refusal says so:

  • DATABASE_URL, R2_BUCKET_NAME and the CLOUDFLARE_* names. The deploy creates those resources and derives the values itself. A value stored here could point a fresh deploy at somebody else's database.
  • Settings the platform sets itself, like LOG_LEVEL.
  • On a copy that runs on a Bay (one deploying to a Bay estate, or a stack's app service), the names Bay writes into every instance: APP_SECRET, APP_NAME, NODE_ENV, DATABASE_URL, DATA_DIR, STORAGE_PATH, SERVER_HOST, SERVER_PORT and the S3_* names.

#Three you do not have to set

A deploy fills these in when the copy has no value of its own. A value you set always wins.

  • APP_SECRET is created on the first deploy that finds none, and stored like any other secret. An Alepha app refuses to start in production without it. ⚠️ It is created once and reused on every deploy after. Changing it signs out every session and makes anything the app sealed with it unreadable, so a copy replacing an existing deployment should set its own before the first deploy.
  • PUBLIC_URL is derived from the copy's address, so absolute links in emails, OAuth callbacks and the sitemap resolve. Set it yourself for a copy answering on some other host, such as one behind a proxy.
  • APP_NAME is set to the copy's resource name, <project>-<app>-<env>, and stored like APP_SECRET. It names the copy in its own logs, and the app also uses it to prefix the session cookie, the cache keys and, when S3_KEY_PREFIX is unset, the keys of the copy's bucket. A name the app declares in code wins too. ⚠️ A copy first deployed without this default gets it on its next deploy: its users are signed out once, and files it stored under its old prefix are not moved. Set APP_NAME (or S3_KEY_PREFIX) yourself before that deploy to keep the old names.

#APP_SECRET on a Bay

On a Bay copy, the machine owns the key. Bay creates it and sends it to Lore, so the APP_SECRET row always holds the key the copy runs on, and the tab says it was set by the machine. A Bay host rebuilt from scratch, or a copy moved onto a Bay estate, takes that key back on its first deploy, so nobody is signed out. If the row held another key, the machine's replaces it and the deploy's log says so. The same holds for a stack's app service.

A copy moved onto a Bay estate keeps the variables it already had. Those among them that Bay writes itself stay listed and are not sent to the machine, so moving the copy back finds them where they were.

⚠️ A copy keeps its key when it moves between estates, in either direction. The estate it left, and that machine's backups on a Bay, still hold the key it runs on: whoever controls them can sign sessions for it. After moving a copy off a machine you do not control, change its key: with bay secret replace on its new Bay, or by setting APP_SECRET on this tab for a copy now on Cloudflare. Either one signs every session out and loses what was sealed with the old key, such as MFA enrolments.

#5. Push a build

Builds come from the machine holding the source: your laptop or your CI.

bash
lore apps build --tag 1.2.3
lore artifacts push --tag 1.2.3

The first builds the app, the second stores the build in your project. Both take --project <slug> (or LORE_PROJECT), and sign in with lore login or with LORE_API_KEY in CI. See Artifacts.

⚠️ latest is the default tag and the only one whose build may be replaced. Two copies both showing latest may be running different code. Name a real tag for anything you intend to promote.

#6. Deploy it

From the browser. The copy's Deploy tab lists what has been pushed for this app, with a Deploy button on each build. A build's own page, under Artifacts, has a Deploy menu listing every copy that can run it.

From a terminal, there are two ways:

bash
lore deploy

With no --tag, this builds, pushes and deploys, in that order. It is the loop for everyday work, where rebuilding is what you want.

bash
lore deploy --env production --tag 1.2.3

With a tag, it deploys the stored build and never builds one. If no artifact carries that tag, the command refuses. This is how you promote exactly what was tested: the build CI pushed on Tuesday and staging ran is the one that reaches production on Friday, not a rebuild from another machine.

⚠️ --env is optional only when the app has one copy. The command asks Lore which copies of this app exist. With one, it takes it. With several, it refuses and names them: docs has production, preview. Pass --env <name>. An explicit --env or LORE_ENV always wins.

lore apps deploy is the same command.

No way of deploying names a cloud account. You name a project, an app and an environment, and Lore finds the estate from the copy itself.

#7. Watch it

The Deploy tab follows a run and prints its log while it happens. Closing the tab does not lose the log: open Log on the run later. lore deploy streams the same log to your terminal and exits non-zero if the run fails, so it works in CI.

#8. Roll back

Every successful run on the Deploy tab offers Roll back.

Cloudflare keeps every Worker version it has been sent, so the usual rollback points the copy at an older version: seconds, no upload, and it works even if you only ever push latest. When that version is gone, or the copy's estate has changed, Lore redeploys the stored build instead. The confirmation says which of the two it will do.

⚠️ A rollback changes the code and not the database. If migrations have been applied since the version you are going back to, the confirmation says how many, and you have to acknowledge it: the old code will run against the new schema.

#9. Take it down

Deleting a copy, from its Settings tab, removes Lore's record of it. It does not remove what its deploys created. That is a command of its own:

bash
lore apps destroy --env staging --confirm my-app/staging

⚠️ Both flags are required and neither has a default. --env does not fall back to LORE_ENV, and it does not take the app's only environment the way lore deploy does, so a forgotten flag can never destroy production. The confirmation is the copy's own app/env, typed, so naming the wrong environment fails the check. There is no --yes.

#On Cloudflare

The command removes what a redeploy puts back: the Worker, the job queue and its dead-letter queue, and the cache namespace. An app that uses websockets also has a Durable Object namespace, which holds live connections and nothing else. It goes with the Worker.

⚠️ The database and the bucket are kept. A D1 database and an R2 bucket are what the app was serving, and Cloudflare offers no rename and no archive to soften deleting one. Resources are found by name, so a copy destroyed and deployed again under the same name gets its own database back with every row still in it. To delete those two, run alepha infra down on your own machine, against your own account.

#Copies that are meant to be thrown away

A copy created as Ephemeral says, before it holds anything, that its data goes when it does. A destroy then takes its database and its bucket too. That is what a preview per branch wants.

⚠️ It can be set only when the copy is created: with the Ephemeral box of the New copy dialog on a build's page, with lore apps instance create --ephemeral, or as ephemeral: true from @alepha/lore/client. It cannot be turned on later, so nobody can flip it on a copy whose database matters.

⚠️ It permits, it does not schedule. Nothing sweeps ephemeral copies and none of them expires. The flag only says what a destroy you ask for may take.

#On a Bay

The same command asks the machine to remove the copy. Bay stops serving it and unregisters it, and its database, uploads and .env stay on the machine, so a deploy of the same copy brings it back. The estate must accept deploys, and the command waits for the machine to say it is done. Lore keeps the copy until you delete it with lore apps instance delete, which it refuses while the machine still runs the copy.

⚠️ Lore never asks a machine to delete data. --purge sends nothing: it prints the bay remove --purge command to run on the machine, which removes the copy and its data together.

#What a destroy will not touch

⚠️ Lore deletes only what it recorded creating. The names are derived from the project, the app and the environment, and your estate may hold resources Lore never made, one of which could bear the same name. A copy with nothing recorded is refused.

If a run removes some resources and fails on others, what went is struck from the record, so running it again retries only the rest.

Deleting the copy itself is refused while any of its resources is still standing, since the copy is the only place their names are written. Only the API's forget option overrides that, for resources you have already removed by hand.

#When a deploy is refused

Every refusal names what is missing, because the person running the command is often not the person who can fix it.

It says What to do
… has no estate Choose one on the copy's Settings tab.
Several estates are lent to this project … Create the copy with lore apps instance create --estate <slug>, or choose its estate on the Apps page, then deploy.
lore() takes no estate … Remove estate from alepha.config.ts; the copy's estate is set in Lore.
… is no longer lent to this project Ask its home project to lend it again.
… does not accept deploys Somebody with estate:update in its home project turns that on from its console.
… does not have a usable Cloudflare credential Somebody with estate:update in its home project checks the token from its console.
… has no artifact tagged '…' Push one, or deploy a tag that exists.
… has no \workerd` build` Build for that runtime: the message names the exact command.
Another copy in this Cloudflare account is already named … Another copy already uses that name: point this copy at another estate, or rename the project before its first deploy.
Another copy on this Bay machine is already named … The same, on one Bay machine.
… has no copy named … Create the copy on the Apps page and choose this estate on its Settings tab.
… was first deployed to this machine as … Rename the copy's app or environment back to the names it was deployed under.

About the missing build: an artifact is identified by its app, its tag and its runtime, so 1.2.3 built for Cloudflare and 1.2.3 built for a Bay machine are two stored builds, not a collision. A deploy looks up the one the estate can run.

#From the command line

Beyond the commands above:

bash
lore apps instance create --app my-app --env production --estate my-vps
lore secrets set --file .env.production --env production
lore deploy status --app my-app --env production
lore deploy rollback --deployment <id>

The full list is in The lore CLI.

#From an agent

An agent deploys a stored build with deploy_start, follows it with deploy_status and goes back with deploy_rollback. It never builds. app_instance_create creates a copy, and is the one tool that can mark it ephemeral. See MCP tools.

#Next