Featurevisor

Ship it

Deployment

Featurevisor has no server. Deploying means publishing the datafiles to static hosting you control, from CI, every time a change merges. Applications fetch them from there.

The pipeline

Run these steps on every push to your main branch, and stop at the first one that fails:

Commands
npx featurevisor lint
npx featurevisor test
npx featurevisor build
git add .featurevisor
git commit -m "[skip ci] Revision $(cat .featurevisor/REVISION)"
git push origin main

Then upload the datafiles directory to your hosting.

  • Lint and test again after the merge, because two pull requests that passed on their own can conflict once both are in.
  • The build writes the datafiles and updates the state files in .featurevisor/.
  • Commit only .featurevisor/. The scaffold's .gitignore leaves datafiles/ out, since every deployment builds them again.
  • [skip ci] stops the state commit from starting another deployment.
  • Push the state before uploading, so a rejected push stops the run before anything is published.

GitHub Actions has this pipeline as a ready workflow.

One deployment at a time

Two builds running at once start from the same state and the same revision, and only one of them can push. Run deployments one at a time, through a single queue such as a GitHub Actions concurrency group. If a push is rejected, run the pipeline again from the latest main branch. Never force push the state.

Where to host

Any host that serves static files works: a CDN, object storage behind one, Cloudflare Pages or GitHub Pages.

Serve the datafiles directory as the root, so each file keeps its path. datafiles/featurevisor-all.json is then served at https://cdn.example.com/featurevisor-all.json. In a project with environments and targets, datafiles/production/featurevisor-web.json is served at https://cdn.example.com/production/featurevisor-web.json. Upload every environment and target together, so applications never read datafiles from two different builds.

Each build overwrites the same file names. A cache in front of them delays every change by up to its lifetime, so choose one that matches how quickly a change has to reach users, a rollback included.

How applications get updates

An application keeps evaluating the datafile it has in memory until it is given a newer one. Choose when it fetches one:

ApproachA change arrivesSuits
Fetch on startAt the next start or page loadShort lived processes and page loads
Refresh on an intervalWithin one intervalLong running servers and apps
Refresh when toldSoon after each deploymentChanges that must land at once
Bundle itWith the next application releaseApps that must work offline

Fetching on start is the simplest. To refresh on an interval, fetch again every few minutes and replace the datafile, as the JavaScript SDK's interval refresh shows. To refresh when told, fetch when your application receives an event, such as a message over a WebSocket after each deployment. A bundled datafile works offline, and can be combined with fetching a newer one when the network allows.

Every datafile carries the revision it was built with, so an application can tell whether the file it fetched is newer. To load only the datafiles a screen needs, see Loading datafiles on demand.

How long a change takes

A merged change reaches a user after three waits: the pipeline, the cache in front of the files and the application's next fetch. With a five minute cache and a five minute refresh interval, a change can take ten minutes after the pipeline finishes to reach every user.

Switching a feature off is only as fast as that sum. Agree the cache lifetime and refresh interval with the people who will need the fastest change, usually whoever is on call, before the day they need it.

Roll back

Revert the change in Git and let the pipeline publish the result. Keep the state files as they are, so users stay in the variations they had. Configuration rollback covers the details.

Examples

Previous
State files