Repository navigation
[Epic] PR preview deployments #3042
Description
Turn on pull-request previews for an application on a Fabric cluster, and every PR's build is served at its own subdomain, <slug>.app.<cluster-domain>, with a valid certificate. Each push updates the preview, and closing the PR removes it. Customer setup should be one toggle in Studio plus one workflow file, with no stored secrets, DNS records or certificates to manage. Fabric already serves app. on every cluster, so the toggle no longer touches DNS or certificates.
Example: a PR from branch david/1778-custom-regions is served at https://david-1778-custom-regions.app.<cluster-domain>.
This is the next step after staged component deploys (#2315), and the "preview deployments" item of the Ephemeral Instances roadmap lane, beside #641 and #642. Studio is the first consumer.
Where things stand
Update, 2026-10-30: the cluster side is done for 5.4. #3617 adds enable_previews, disable_previews and get_preview_policy, per-application limits (maxPreviews, maxTtlDays), and harper preview setup / teardown, which write the starter workflow. #3463 is fixed by #3600 and #3763. Every harper-core sub-issue is merged except #3055, plus four follow-ups from review: #3608, #3609, #3613 and #3758. Every row of the table below now has its fix merged, except a reusable action (#3055). Names Studio's panel must match, from derivePreviewNames in utility/previewIdentity.ts: role and user <app>-ci-preview; trust policies github-actions-<app>-preview (environment preview) and github-actions-<app>-preview-cleanup (environment preview-cleanup, drop only); workflow .github/workflows/preview.yaml, which reads its target from the HARPER_PREVIEW_TARGET repository variable.
The recipe described below is the 5.3 one: no documentation covers #3505's or #3617's features yet, and HarperFast/documentation#750 (draft) covers #3339's.
Update, 2026-10-26: every harper-core sub-issue except #3055 and #3463 is merged for 5.4: #3045 and #3091 in #3339, the rest in #3505.
Decision, 2026-10-26: the customer's cluster owns previews, and Central Manager stays out of the loop. Trust is between GitHub (or another issuer) and the cluster. A core enable/disable operation on the cluster creates the scoped role, user and trust policy, stores and enforces each application's preview policy, and removes them all again. The CLI and Studio's panel (HarperFast/studio#2044) call it directly, as HarperFast/studio#1788 does for deploys. harper preview deploy reports a preview host it cannot reach, in place of a Central Manager or host-manager gate. So HarperFast/central-manager#900, HarperFast/central-manager#904, HarperFast/central-manager#905, HarperFast/central-manager#907, HarperFast/central-manager#909 and HarperFast/host-manager#242 are closed as not planned. That core work merged in #3617. What remains: #3055, the follow-ups #3608, #3609, #3613 and #3758, Studio's panel (HarperFast/studio#2044) and its own previews (HarperFast/central-manager#908, HarperFast/central-manager#910, HarperFast/central-manager#911, HarperFast/central-manager#912), and HarperFast/host-manager#243.
Previews already work by hand on 5.3.0. HarperFast/documentation#715 (stacked on HarperFast/documentation#713) documents a recipe, "Per-PR previews with shared databases":
- A
pull_request_targetworkflow runs from the default branch and never checks out PR code. - It deploys each PR's head commit with
by_ref, as an isolated applicationmy-app-pr-<n>atpr-<n>.<suffix>. - Previews share the cluster's databases. On 5.3.0 and 5.3.1, no deploy path produces a database branch. Since 5.3.2, a
by_refor package deploy that setsbranchedDatabasesruns the preview on its own fork (Apackagedeploy'sbranchedDatabasesis ignored at load, so the application runs on its base databases #3071, fixed by Honor branchedDatabases for package-deployed applications #3083). A payload deploy refuses the field from 5.4 (deploy_componentacceptsbranchedDatabaseson a payload deploy and silently ignores it #3044, fixed by RefusebranchedDatabaseson a payload deploy, which used to succeed and leave the application on its base databases #3066); 5.3.x still drops it silently. - Cleanup calls every node directly, and checks that the component and its worker are gone.
On Fabric, the custom-domain step is going away. Preview hosts merged on 2026-10-21 in HarperFast/central-manager#917 and HarperFast/host-manager#244, and are documented in HarperFast/documentation#718:
- Every Colocated and Dedicated cluster serves
*.app.<cluster>.<org>.harperfabric.com, with no setting. - Central Manager writes the wildcard record with the cluster's own record, and backfills existing clusters from a staff endpoint. It lets the cluster's live instances prove
_acme-challenge.app.<cluster>. - Host-manager adds the wildcard to each instance's certificate at its next order or renewal. It routes a host exactly one label under it only to the isolated application that lists it, and only once that instance's certificate carries the wildcard. A host no isolated application lists gets no route.
- New clusters serve previews at once. Existing ones gain them as each instance's certificate renews, within about seven weeks.
The label is app., not the cluster name itself. Let's Encrypt refuses an order that lists a name beside a wildcard covering it, and the instance and replication names sit one label under the cluster name. Let's Encrypt staging confirmed the refusal for *.<cluster>, and issued the same order with the wildcard one label down, every name included.
It works, but it takes a lot of setup. This epic removes that cost one mechanism at a time:
| What the recipe needs today | Removed by |
|---|---|
| On Fabric, each preview hostname is added, verified and bound as a custom domain, for every PR | HarperFast/central-manager#917 and HarperFast/host-manager#244, merged 2026-10-21; they supersede HarperFast/central-manager#900, HarperFast/central-manager#901, HarperFast/central-manager#902, HarperFast/central-manager#903, HarperFast/host-manager#240 and HarperFast/host-manager#241 |
| The first deploy of each preview restarts the shared workers on every node at once | #3045 |
| Cleanup calls each node's own URL, with a trust policy per node URL, and checks files and workers itself | #3045, #3091 |
| The preview role can deploy and drop any component, and read every component's configuration | #3046 |
| An app that needs a build publishes each PR commit to a registry first | #3043 |
| A private repository needs an administrator to grant a credential to each preview, and delete it afterwards | #3090 |
| Previews share the cluster's data | #3053 for the bare databases/tables globals, #3043 for payload deploys (#3071 and #3044 are done) |
| Nothing removes a preview whose close event was missed | #3048 |
| A request with a preview's host on Harper's shared port is answered by production | #3052 |
| Each application needs a hand-made role, user, trust policy and workflow | #3617 (enable_previews, harper preview setup) |
| Each repository carries its own long workflow | #3054, #3617's starter workflow, #3055 |
What already exists
- Isolated applications: a dedicated worker per application, bound to one
host(feat(threads): run an isolated application in a dedicated worker thread #2524).threads.maxIsolatedcaps them at 8 per node by default; a deploy past the cap answers 409. - Scoped restarts:
restart_servicewithscope: '<app>'restarts one isolated application's worker and nothing else (5.3.0). - Branched databases: a per-application copy-on-write fork (Give an application a private, durable fork of a database #2352, feat(branches): declare tables into a branch through @table, ensureTable and defineTable #2523, feat(branches): remove an application's branched databases on drop_component #2517). Reachable through a
by_refor package deploy since 5.3.2 (Honor branchedDatabases for package-deployed applications #3083). A payload deploy cannot set it yet (Payload deploys can sethost,urlPath,isolatedandbranchedDatabases#3043) and is refused from 5.4 (RefusebranchedDatabaseson a payload deploy, which used to succeed and leave the application on its base databases #3066). An application reaches its fork only throughimport { databases } from 'harper'until Bind the baredatabases/tablesglobals to an isolated application's branches #3053. - OIDC trusted publishing: CI deploys with a GitHub Actions token instead of a stored credential (feat: OIDC trusted publishing — deploy from CI with no stored credential #2173, in 5.3.0). A policy can pin
workflow_refto the default branch's workflow andevent_name: pull_request_target. - Staged deploys: build aside, canary certification, provenance and
deployment_id(Staged component deploys: land build-aside-then-swap as a sequence of small PRs #2315). - Fabric routing: host-manager routes a host one label under a claimed
*.domainto an isolated application's worker sockets, and serves a wildcard certificate for it (HarperFast/host-manager#224).
Shape
- A preview is an isolated application named
<component>-pr-<n>, withhost: <slug>.app.<cluster-domain>. Every application on the cluster sharesapp., and a host two applications list gets no route, so the slug must be unique on the cluster: it carries the component when a cluster previews more than one. Applications with tables addbranchedDatabases, which aby_refdeploy honors since 5.3.2 (Honor branchedDatabases for package-deployed applications #3083). An application that uses the baredatabases/tablesglobals still writes the shared data until Bind the baredatabases/tablesglobals to an isolated application's branches #3053. - CI deploys it straight to the cluster, authenticated by OIDC, as an identity that can only deploy and drop that application's previews.
- DNS, certificates and routing need nothing per application: every cluster serves
*.app.<cluster-domain>(HarperFast/central-manager#917, HarperFast/host-manager#244). Everything per application lives on the cluster: enabling previews there creates the trust policy, scoped role and user and stores the application's preview policy, and disabling them drops every preview and the identity. Central Manager has no part in it. - Closing the PR drops the preview. An expiry and a per-application cap catch anything the close event missed.
The branch slug is one lowercase DNS label:
- Anything outside
[a-z0-9]becomes-, and runs collapse. - It is cut to 40 characters, with a hash suffix when cut.
- Reserved names fall back to
pr-<n>.
For example, david/1778-custom-regions becomes david-1778-custom-regions. The documented recipe uses pr-<n>.<suffix>, which needs no slug at all.
Sub-issues
Harper core
- Payload deploys can set
host,urlPath,isolatedandbranchedDatabases#3043 -
deploy_componentacceptsbranchedDatabaseson a payload deploy and silently ignores it #3044 - Start and stop one isolated application without restarting the shared workers #3045, done in Start and stop one isolated application without restarting the shared workers #3339
- Scope deploy permissions to component names #3046
-
deploy_componentdeploysproject=shop.pr-12ontoshop#3047 - Record preview metadata on a deployment, and expire previews #3048
- Per-component build retention, so previews keep fewer copies #3049
- Allow origin patterns in
corsAccessList#3050 - Let an isolated application with a host use
fastifyRoutes#3051 - Shared workers answer an isolated application's host while its worker is down #3052
- Bind the bare
databases/tablesglobals to an isolated application's branches #3053 - Grant a deploy credential to a component-name pattern #3090
- Report per-node results from a replicated
drop_component, and make dropping an absent component a no-op #3091, done in Start and stop one isolated application without restarting the shared workers #3339 - A fork-removal failure after drop_component answers early is only logged #3463, a follow-up from Start and stop one isolated application without restarting the shared workers #3339, done in Retry a branched database removal that drop_component could not finish, instead of only logging the failure #3600 and Keep branch removals from deleting a fork the main thread serves, and retry them when a worker lets go #3763
- A
packagedeploy'sbranchedDatabasesis ignored at load, so the application runs on its base databases #3071 - Cluster-owned setup:
enable_previews,disable_previews, per-application limits andharper preview setup, done in Enable PR previews from the cluster: enable_previews and disable_previews, per-app preview limits, and harper preview setup #3617
Follow-ups from review of #3600 and #3617:
- Trust-policy writes from enable_previews are not exclusive across nodes or against add_oidc_trust #3608, trust-policy writes from
enable_previewsare not exclusive across nodes - harper preview setup enables the cluster side before the GitHub-side writes that can still fail #3609,
harper preview setupenables the cluster before GitHub-side steps that can fail - A preview deploy already swapping, or applying on a peer, can go live after disable_previews #3613, a preview deploy in flight can go live after
disable_previews - A fork that a still-loading worker creates after drop_component's last census is never removed #3758, a fork a still-loading worker creates after the drop's last census is never removed
CI tooling
-
harper preview: deploy, drop and list PR previews from any CI #3054 - A reusable GitHub Action for Harper deploys, with a PR preview mode #3055
Central Manager (superseded: the cluster owns previews)
- HarperFast/central-manager#900. Not planned: the cluster stores and enforces each application's preview policy.
- HarperFast/central-manager#901. Covered by HarperFast/central-manager#917, as one
*.app.record per cluster rather than one per application. - HarperFast/central-manager#902. Covered by HarperFast/central-manager#917, through
_acme-challenge.app.<cluster>. - HarperFast/central-manager#903. Superseded: host-manager derives the wildcard from the cluster name, so Central Manager sends nothing new.
- HarperFast/central-manager#904. Not planned: the cluster's enable operation creates the identity.
- HarperFast/central-manager#905. Not planned: the cluster's disable operation cleans up, and HarperFast/central-manager#917 deletes the wildcard with the cluster's own record.
- HarperFast/central-manager#906. Replaced by PR previews: Previews panel for an application studio#2044: the panel calls the cluster directly.
- HarperFast/central-manager#907. Not planned: the cluster enforces capacity, and
harper preview deployreports an unreachable host.
Studio
- PR previews: Previews panel for an application studio#2044, the Previews panel: it calls
enable_previews,disable_previewsandget_preview_policyon the cluster directly.
Studio as the first consumer of previews:
- HarperFast/central-manager#908
- HarperFast/central-manager#909. Not planned: superseded by
*.app.hosts. - HarperFast/central-manager#910
- HarperFast/central-manager#911
- HarperFast/central-manager#912
Host Manager
- HarperFast/host-manager#240. Superseded by HarperFast/host-manager#244, which routes under the derived
*.app.<cluster>with no claim. - HarperFast/host-manager#241. Covered by HarperFast/host-manager#244.
- HarperFast/host-manager#242. Not planned: no Central Manager gate needs the proxy mode.
- HarperFast/host-manager#243. The deployed canary in HarperFast/central-manager#917's rollout is this check.
Security model
- The workflow is trusted, not the PR. The job runs on
pull_request_targetfrom the default branch and never checks out PR code. The trust policy pinsworkflow_refto that branch's workflow file,event_name: pull_request_target, and an environment, so a PR can't change the privileged workflow. Fork PRs are skipped by the workflow's same-repository condition. - The PR's code still runs on Harper, with the preview role's rights. From 5.4 (Scope deploy permissions to component names #3046, Enable PR previews from the cluster: enable_previews and disable_previews, per-app preview limits, and harper preview setup #3617), that role can deploy and drop only its application's
<app>-pr-<n>previews, within its grant's limits. On 5.3 it can deploy and drop any component, so previews there belong on a separate preview cluster, or behind required reviewers when they share a production cluster. - Data is shared unless the preview is branched. Without
branchedDatabases, a preview's writes, schema changes and roles apply to the cluster's databases. With it, writes to its branched databases stay in its fork. Thesystemdatabase (users and roles) cannot be branched, and the baredatabases/tablesglobals reach the base until Bind the baredatabases/tablesglobals to an isolated application's branches #3053. - A preview is the same site as what production serves from the cluster's host name. It can set cookies production receives, and
SameSitedoes not separate them. Document preview hosts and custom domain name rules on Fabric documentation#718 recommends__Host-cookie names and CSRF orOriginchecks. What production serves only from a custom domain is a different site. - Public repositories: GitHub's default execution protection blocks
pull_request_targetfor affected public repositories from 2026-11-02. The repository's Actions policy has to allow it for the preview workflow.
Related, tracked elsewhere
- Document isolated applications, branched databases, and per-PR CI previews documentation#715 and Deploying from CI: lead with OIDC, and correct the deploy reference where it disagrees with Harper documentation#713: the CI guide, and the reference for isolated applications and branched databases.
- Isolated applications are admitted when no SNI route can reach them (no host, or a host shared by urlPath) #2757: refuse unroutable isolated applications at admission.
- Token operation scope is not enforced on the REST/GraphQL resource path #2201: token operation scope is not enforced on REST/GraphQL, so the preview role should hold no table permissions.
- Branched databases: blob store sharing, allocator, and GC safety #644: branched-database blob sharing and GC safety.
- Failed deploy payloads are retained forever; add age/count expiry so retried rollouts stop consuming tenant storage quota #2681: failed-deploy payloads are kept forever.
- HarperFast/central-manager#845: custom-domain name hygiene, which lands in HarperFast/central-manager#917. Customer wildcard custom domains moved to HarperFast/central-manager#933. HarperFast/central-manager#917's custom-domain follow-ups (HarperFast/central-manager#965 to HarperFast/central-manager#969) are tracked apart from this epic.
- HarperFast/central-manager#887: multi-value ACME TXT, a prerequisite. Merged on 2026-10-09.
- HarperFast/central-manager#570: ephemeral environments. This epic is its lightweight tier.
Not in this epic
- Customer-owned preview domains, via
_acme-challengeCNAME delegation (HarperFast/central-manager#933). - A GitHub App mode that needs no workflow file.
- Promoting a previewed build to production by
deployment_id. Builds are bound to their component today. - Static-only previews served from the shared workers.
- Stopping idle previews, and resetting a branch from its base.
- A cluster per PR (HarperFast/central-manager#570).
- Central Manager setup, policy or cleanup for customer previews: the cluster owns them.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
host,urlPath,isolatedandbranchedDatabases#3043deploy_componentacceptsbranchedDatabaseson a payload deploy and silently ignores it #3044deploy_componentdeploysproject=shop.pr-12ontoshop#3047corsAccessList#3050fastifyRoutes#3051databases/tablesglobals to an isolated application's branches #3053harper preview: deploy, drop and list PR previews from any CI #30543 remaining items
Enable PR previews from the cluster: enable_previews/disable_previews…
Enable PR previews from the cluster: enable_previews/disable_previews…
Enable PR previews from the cluster: enable_previews/disable_previews…
Enable PR previews from the cluster: enable_previews and disable_prev…