Self-hosting Field
Run a published Field release, not a branch of this repository. Each
installation pins one release by version and fingerprint, checks it before it
runs, and upgrades by changing the pin. docs/RELEASING.md describes what a
release publishes and how it is made.
Every installation needs the same things, whatever runs it:
- PostgreSQL:
DATABASE_URL, withFIELD_STORAGE=postgresso job files are stored durably and rebuilt into the data directory at boot. - Configuration:
FIELD_CONFIG_JSON(the whole config as one secret JSON value, shaped likeconfig.json.example), or aconfig.jsonnamed byFIELD_CONFIG.docs/REPLIT.md("Configuration") lists what goes in it. - A data directory:
FIELD_DATA, writable. - A price catalogue with at least one active item. Field refuses to start
without one (
README.md, "Getting started").
Keep every secret in your platform's secret store. Never put one in a file in a repository, including your own deploy repository.
Update notice
An installation that runs a release (GET /health shows it as
build.release) tells its owners in Settings when a newer release is out on
its channel, and puts a dot on the Settings link. The notice links the
release notes and this guide's upgrade steps. The server checks at most every
six hours (server/update-check.js), with an anonymous GET of public release
data from GitHub, or npm when GitHub cannot answer. It sends nothing about
the installation. An owner can turn it off in Settings, Account and security,
"Check for new Field releases".
The channel is stable unless the installation runs a beta, or
FIELD_DEPLOYED_REF (Replit) says beta; FIELD_UPDATE_CHANNEL=stable or
beta overrides it. A build that is not a release, such as a checkout of
main, asks nothing.
Pin a release
Each release's release-manifest.json (on its GitHub Release) records its
npm integrity, its image digest and its commit. deploy/replit/verify-release.mjs
turns one release into a lock file:
node deploy/replit/verify-release.mjs pin 2026.10.14
It downloads that release's manifest, checks the manifest against its
.sha256, checks that npm serves the same integrity, and writes field.lock
(see deploy/replit/field.lock.example). Commit field.lock to your deploy
repository. To upgrade, pin a newer version. To roll back, pin the earlier
one.
Docker
Run the image by the digest in field.lock, after you verify GitHub's
attestation for that digest:
gh attestation verify oci://ghcr.io/arcforgelabs/field@sha256:<digest> --repo arcforgelabs/field
docker run -d -p 19200:19200 -v field-data:/app/data \
-e DATABASE_URL -e FIELD_CONFIG_JSON -e FIELD_STORAGE=postgres \
ghcr.io/arcforgelabs/field@sha256:<digest>
The image listens on 19200 as an unprivileged user and keeps its data
directory at /app/data. GET /health names the commit it was built from.
Node
Install the exact version on Node 24. The package's shrinkwrap pins every dependency to the tree CI tested. Then check its signatures and provenance:
mkdir field && cd field
npm init -y
npm install --omit=dev --save-exact @arcforgelabs/field@2026.10.14
npm audit signatures
npm audit signatures checks the registry signature and the provenance
attestation cryptographically. To check what the provenance claims (the
commit, this repository and its release workflow) against your field.lock,
copy deploy/replit/verify-release.mjs from this repository next to your
field.lock and run node verify-release.mjs verify <install-dir>. Use your
own copy, not one from the installed package: a package must not vouch for
itself.
Start it with the field-server command, with the environment above:
FIELD_DATA=/var/lib/field FIELD_STORAGE=postgres npx field-server
Run it under a supervisor (systemd, for example) that restarts it. Field sends scheduled emails in-process, so it must stay up.
Replit
deploy/replit/ is a template for a Replit deployment that runs a pinned
release:
| File | Does |
|---|---|
.replit |
A Reserved VM deployment. Its build runs install-and-verify.sh; it runs start.sh. |
install-and-verify.sh |
Installs @arcforgelabs/field at exactly field.lock's version, runs npm audit signatures, then verify-release.mjs verify. Any failure fails the build, and Replit keeps the previous deployment. |
start.sh |
Starts the installed release with FIELD_STORAGE=postgres, the data directory, and the release's commit as FIELD_REVISION. |
verify-release.mjs |
Writes field.lock (pin) and checks an install against it (verify). No dependencies. |
field.lock.example |
The shape of field.lock. |
.npmrc |
Lets npm install through Replit's package firewall. |
To use it:
- Create your own private repository and copy the contents of
deploy/replit/into its root. Your deploy repository holds your pin and your Replit settings; this public repository never does. - Pin a release:
node verify-release.mjs pin <version>, and commitfield.lock. - Import the repository into Replit. Add a PostgreSQL database, which sets
DATABASE_URL. Add the Secrets fromdocs/REPLIT.md("Configuration"). SetFIELD_AUTHand anything else your installation needs in.replit's[userenv]or as Secrets. - Publish. The build log shows
Verified @arcforgelabs/field@<version>and the commit, orREFUSED:with the reason.
To upgrade, open a pull request in your deploy repository that changes
field.lock (pin <newer version>), merge it, and publish again. Before you
upgrade, read the release's notes and the migrations in its manifest.
docs/REPLIT.md covers the rest of running Field on Replit: durable storage,
issued documents in R2, sign-in, Sentry and data migrations.