Skip to main content

Deploying places with nevermore deploy

nevermore deploy builds a Rojo project, uploads it to a Roblox place via the Open Cloud API, and (optionally) publishes the new version so players see it. nevermore test is built on the same pipeline, so everything that runs in a Roblox place goes through this command.

This guide walks you from zero to your first uploaded place. For advanced features (merging with a Studio-authored base place, smoke tests, batch deploys, CI), see Integration Testing.

New to Nevermore?

Start with the Intro for an overview and Install for setting up Node, Rojo, and the Nevermore CLI itself. This guide assumes the CLI is already on your PATH.

When should I use this?

For most Roblox projects, especially solo work and small teams, you don't need this. Open Roblox Studio, click Save to Roblox or Publish to Roblox, and you're done. That's normally the right answer.

You probably want nevermore deploy when:

  • Your code lives in a git repository and Studio is just where you preview it. Deploys should come from the same commit history that reviews and tests run against, not from whichever developer's Studio happens to be open.
  • More than one programmer ships to the same place. Studio's publish flow is last-writer-wins, and a CLI deploy from CI gives you one path from "merge to main" to "live in game", traceable to a specific commit.
  • You want one config to drive both tests and deploys. nevermore deploy and nevermore test both read deploy.nevermore.json, so the place you smoke-test against on every PR is configured exactly like the one you ship to. See Test Infrastructure.
  • You want CI to gate releases. Batch deploys plug into PR checks, so a deploy only runs after lint, tests, and smoke tests pass, and shows up as a PR comment instead of an ad-hoc Studio session.

If none of those apply, stick with Studio Publish. Come back when you outgrow it.

What deploy actually does

When you run nevermore deploy run:

  1. Reads deploy.nevermore.json in your current directory and resolves the target you asked for (default: test).
  2. Runs rojo build on the target's project file to produce an .rbxl place file in a temp directory.
  3. Uploads the .rbxl to the configured universeId / placeId over Open Cloud.
  4. Saves the new version as a draft. If --publish is passed, it is also published as the live version.

That's the whole pipeline. There are no deploy hooks or post-processing steps to register.

Prerequisites

  • Node.js v18+ and the Nevermore CLI installed. The Install guide walks through both. The short version is npm install -g @quenty/nevermore-cli, or use npx nevermore ... from any package that depends on it.
  • Rojo v7+ on your PATH.
  • A Roblox universe and place you own. You can create both at create.roblox.com/dashboard/creations.
  • A Roblox Open Cloud API key. See Logging in below.

Logging in

nevermore deploy authenticates against Open Cloud with an API key. Create one at create.roblox.com/dashboard/credentials and grant it these scopes for the universe you want to deploy to:

ScopeUsed for
universe-places:writeUploading new place versions
universe.place.luau-execution-session:writeRunning scripts (used by nevermore test and smoke tests)
universe.place.luau-execution-session:readReading script execution results
legacy-asset:manageDownloading a base place (only needed if you use basePlace)

Save the key to your machine once:

nevermore login

This stores the key at ~/.nevermore/credentials.json (mode 0700) after validating it against Open Cloud. Other useful flags:

  • nevermore login --force swaps the stored key.
  • nevermore login --clear removes it.
  • nevermore login --status shows what's loaded and re-validates it.

How the CLI finds your key

The CLI resolves credentials in this order (first match wins):

  1. The --api-key CLI flag
  2. The ROBLOX_OPEN_CLOUD_API_KEY environment variable
  3. The ROBLOX_UNIT_TEST_API_KEY environment variable (kept for backwards compatibility)
  4. ~/.nevermore/credentials.json (from nevermore login)

In CI, set ROBLOX_OPEN_CLOUD_API_KEY as a secret. nevermore login is for local developer machines.

Setting up a package for deploy

deploy.nevermore.json is the only file the CLI needs to know about. The fastest way to create one is the interactive init wizard.

nevermore deploy init

From inside the directory you want to deploy from (a package under src/, a game under games/, or any directory with a package.json):

nevermore deploy init

The wizard:

  • Detects a test/default.project.json if one exists and offers it as the default Rojo project.
  • Detects test/scripts/Server/ServerMain.server.lua (or .luau) and offers it as the default script template.
  • Walks up the filesystem looking for a parent deploy.nevermore.json with a universeId and reuses it. Once you have one game configured, sibling packages can inherit the universe automatically.
  • Lists every existing place in the universe so you can pick one, or offers to create a new place.
  • Prints the resulting config and asks you to confirm before writing it.

Non-interactive setup

Pass --yes to skip prompts. You must supply enough flags for the wizard to resolve everything without asking:

nevermore deploy init --yes \
--universe-id 12345 \
--place-id 67890 \
--project default.project.json \
--target test

If you have the universe but no place yet, use --create-place to create one. Place creation is not exposed in Open Cloud, so this uses your .ROBLOSECURITY cookie instead. It only works on a machine that's logged in to Roblox.

nevermore deploy init --yes \
--universe-id 12345 \
--create-place \
--project default.project.json

Other flags:

FlagDescription
--target <name>Name of the target to create (default: test)
--script-template <path>Set the Luau script template that nevermore test will run
--forceOverwrite an existing deploy.nevermore.json

The deploy.nevermore.json schema

{
"targets": {
"test": {
"universeId": 12345,
"placeId": 67890,
"project": "default.project.json",
"scriptTemplate": "test/scripts/Server/ServerMain.server.lua",
"basePlace": {
"universeId": 12345,
"placeId": 11111
}
}
}
}
FieldRequiredDescription
targetsyesMap of target name to deploy config. Most packages start with a single test target.
targets.<name>.universeIdyesRoblox universe ID to deploy into.
targets.<name>.placeIdyesRoblox place ID. The build is uploaded here as a new version.
targets.<name>.projectyesPath to the Rojo project file, relative to the package directory.
targets.<name>.scriptTemplatenoLuau file nevermore test executes via Open Cloud after upload. Not used by nevermore deploy itself.
targets.<name>.basePlacenoUniverse/place to download and merge with the rojo build before uploading. See Merging with an existing place.

You can declare any number of targets. A common setup is one test target for CI and a separate production or staging target for live deploys:

{
"targets": {
"test": { "universeId": 1, "placeId": 10, "project": "test/default.project.json" },
"production": { "universeId": 1, "placeId": 20, "project": "default.project.json" }
}
}

Running a deploy

From the directory containing deploy.nevermore.json:

# Build + upload to the default "test" target as a saved (draft) version
nevermore deploy run

# Same, but publish so the new version is live for players
nevermore deploy run --publish

# Deploy a specific target
nevermore deploy run production --publish

nevermore deploy run and the bare nevermore deploy <target> form are equivalent. run is the default subcommand.

On success you'll see one of:

Saved v42 — not yet live.
Published v42 — live in game.

A "saved" version is uploaded but not visible to players. You can publish it later from the Roblox dashboard, or re-run with --publish to publish a fresh build. That version number matches what you'll see on the place page.

Run flags

FlagDescription
--publishPublish the new version (default: save only)
--api-key <key>Open Cloud API key (overrides credential lookup)
--universe-id <id>Override the target's universeId
--place-id <id>Override the target's placeId
--place-file <path>Skip the rojo build and upload an existing .rbxl instead
--output <path>Write a JSON record of the deploy result to this path

Global flags (available on every nevermore command):

FlagDescription
--yesNon-interactive (fails fast instead of prompting)
--dryrunPrint what would happen without doing it
--verboseVerbose logging (rojo output, upload details)

Overriding the configured place

--universe-id and --place-id let you redirect a single deploy without editing the config. This is useful when you want to push the same build to a personal staging place for a one-off test:

nevermore deploy run --universe-id 999 --place-id 8888

Uploading a pre-built place

If you already have a .rbxl (for example, one produced by rojo build in an upstream CI step), skip the rebuild:

nevermore deploy run --place-file ./build/my-place.rbxl

The project field in deploy.nevermore.json is ignored when --place-file is set, but universeId and placeId are still required.

Batch deploys

If you want to deploy every game affected by a code change (for example, on every PR), use nevermore batch deploy instead. It scans the pnpm workspace for packages with a matching deploy target, uses pnpm ls --filter to figure out which ones changed since origin/main, and runs them in parallel.

See Integration Testing → Batch deploy for the full flag list and CI usage.

Common workflows

First-time setup for a new game

If you're starting from a clean directory, nevermore init scaffolds a working Nevermore game template: a default.project.json, server/client entry scripts, and the default packages (loader, servicebag, binder, etc.). After that, nevermore deploy init only needs your universe and place IDs.

mkdir my-game && cd my-game
nevermore init # scaffold a Nevermore game template
nevermore deploy init # configure the deploy target (interactive)
nevermore deploy run # first upload, draft only
nevermore deploy run --publish # publish when ready

The wizard auto-detects the project file nevermore init creates, so you only answer prompts for universe and place. See Install for the full breakdown of what nevermore init produces, and Integration Testing → Setting up a new integration game if you're building a game inside the Nevermore/Raven monorepo instead.

Promoting a tested build to production

If you have separate test and production targets, the common pattern is to run the test target on every PR and only deploy production from main:

# In CI on main
nevermore deploy run production --publish

There is no separate "promote" command. You deploy the same code to whichever target you want.

Debugging a failed deploy

  • Pass --verbose to see the rojo build output and the raw Open Cloud responses.
  • Pass --dryrun to confirm which target, universe, place, and project the CLI would use without uploading anything.
  • Check nevermore login --status if you suspect a credential problem.
  • Place uploads can fail with HTTP 403 when the API key is missing the universe-places:write scope for that specific universe. The scope is granted per-universe, not globally.

See also

  • Intro — Why Nevermore, the major packages, and how the library is organized.
  • Install — Setting up Node, Rojo, the Nevermore CLI, and scaffolding a project with nevermore init.
  • Integration TestingbasePlace merging, smoke tests, batch deploys, CI integration.
  • Test Infrastructure — How nevermore test reuses the deploy config to run Jest specs in Open Cloud.