dropto.run docs
Guides

CLI

TerminalCode
npm i -g drop2run drop2run login

Node 20 or newer.

Code
drop2run login [--device] Sign in and store a token drop2run logout Remove the stored token drop2run init [dir] [--site <subdomain>] Tie this folder to a site drop2run deploy [dir] [--site <subdomain>] Publish a folder drop2run init|deploy --subdomain <name> Create a new site under a name you pick drop2run init|deploy --folder <path> File the new site in one of your folders drop2run ls List your sites drop2run folders List your folders, as --folder takes them drop2run open [site] Open a site in a browser drop2run info [site] Show a site's settings and versions drop2run set <setting> <value> [--site X] Change one setting drop2run unset name|password|expires Clear a name, a password or a takedown drop2run unset upstream <origin> [header] Remove an upstream and its keys, or one header drop2run pause [site] Take a site off the air, keeping its files drop2run resume [site] Put a paused site back on the air drop2run rollback <deployId> [--site X] Put an earlier version back live drop2run rm <site> --yes Delete a site and everything on it drop2run token list List your access tokens drop2run whoami Check the token and whose it is drop2run where Show which token source is in use drop2run --version Print the version

--json works on every command.

Signing in

TerminalCode
$ drop2run login Opening https://app.dropto.run/cli-auth?challenge=…&state=…&port=51274&name=laptop Waiting for authorization of "laptop"… Signed in as you@example.com. Token "laptop" saved to /home/you/.config/drop2run/config.json

It opens a browser and listens on 127.0.0.1. Approving in the browser sends a one-time code to that listener, which the command trades for a token — so the token never appears in a URL bar, a clipboard or your scrollback. The file it lands in is written 0600.

Two things about it decide when it works:

  • The browser and this process must be on the same machine. The last step is a redirect to 127.0.0.1, and nothing off the machine can complete it. Over SSH, in a container or under WSL, use --device.
  • There is no client secret, because a program on other people's machines cannot hold one. A verifier is generated per sign-in and never leaves the process; only its SHA-256 goes through the browser. A code intercepted anywhere along the way cannot be exchanged without the verifier.

--device, when the browser is elsewhere

TerminalCode
$ drop2run login --device Open https://app.dropto.run/device and enter this code: H7KD-9MXQ Or open the link with the code already in it: https://app.dropto.run/device?code=H7KD-9MXQ Waiting for approval of "build-box"… Signed in as you@example.com.

Enter the code at app.dropto.run/device from any machine you are signed in on — a phone will do — and the terminal picks up its token.

The short code is not the credential. It names the pending request; approving it releases a long code that never left the waiting process. Somebody reading it over your shoulder learns which sign-in is waiting, not how to collect its token. It lasts fifteen minutes and works once.

Pressing Refuse on that page ends the sign-in rather than leaving the terminal waiting.

In CI, neither flow applies

Nothing in CI can open a browser or approve anything. Create a token as described in Authentication and set it in the environment:

TerminalCode
export DROP2RUN_TOKEN=d2r_...

drop2run where reports which source is in force without printing the token. That is deliberate: the output of a command line ends up in issue reports, terminal recordings and CI logs, and "why is it using the wrong account" is answerable without showing the secret.

Code
$ drop2run where Token source: DROP2RUN_TOKEN (environment) Config file: /home/you/.config/drop2run/config.json API: https://dropto.run/api

drop2run logout removes the stored token. If DROP2RUN_TOKEN is set it says so rather than claiming a clean sign-out — the variable outranks the file, so every later command would keep using it.

Publishing

TerminalCode
$ drop2run deploy dist Published 42 files. https://brave-otter-4f2a.dropto.live

A publish whose files all match what the site already serves prints Already up to date — nothing needed publishing. and creates no version. It succeeds either way.

Choosing the address

A new site gets a generated name — brave-otter-4f2a — which is fine for something you are looking at once and wrong for anything you will link to. --subdomain picks the name instead:

TerminalCode
$ drop2run init dist --subdomain my-docs Wrote /home/you/project/drop2run.json https://my-docs.dropto.live `drop2run deploy` now publishes dist to this site.

It works on deploy the same way, for a publish with no project file. Names are lowercase letters, digits and hyphens, and cannot start or end with one. A name already taken, reserved, or beyond the number of self-picked names your plan allows is refused, and the message says which.

The name can be changed afterwards with drop2run set subdomain <name> --yes, twice per site. The old name keeps working by redirecting to the new one and nobody else can take it, so each change is spent for good — going back to an old name counts too.

--subdomain and --site cannot be given together, and the command says so rather than picking one: --site publishes over a site that exists, --subdomain creates one that does not.

Filing a new site in a folder

A new site goes at the top level of your dashboard. --folder files it in one of your folders instead, named by path the way drop2run folders prints it, or by folder id:

TerminalCode
$ drop2run folders Clients 01JQ8A… Clients/Acme 01JQ8B… $ drop2run init dist --folder Clients/Acme

Case does not matter, and it combines with --subdomain. It never creates a folder — make those in the dashboard — and a folder that does not exist is refused with the list of those that do, before any site is made. Nor does it move a site that already exists, so it cannot be given with --site, or to deploy in a project whose drop2run.json already names a site — drop2run set folder does that.

Which site a command acts on

--subdomain creates one under the name you give. Otherwise --site, then drop2run.json, then a new site with a generated name. init writes that file, so a normal project is two commands once and one command after that:

TerminalCode
$ drop2run init dist Wrote /home/you/project/drop2run.json https://brave-otter-4f2a.dropto.live `drop2run deploy` now publishes dist to this site. $ drop2run deploy Published 3 files. https://brave-otter-4f2a.dropto.live

The order is not a convenience. A deploy that created a site while a project file sat next to it would leave the real site untouched and hand you a URL you did not expect.

With no --site, no --subdomain and no project file, every publish creates a new site. That is not a default to fight: reusing the most recent one would silently replace whatever you published last.

A --site that matches nothing fails. It does not fall back to creating one, because "I could not find that site so I published to a different one" is not an outcome anybody wants from a deploy command — and a subdomain typed one letter wrong would otherwise become a second site under the typo. Creating is what --subdomain is for, and having to say which you meant is the point of them being two flags.

Managing a site

Settings

info shows what a site is set to and lists its versions, with the one being served marked:

Code
$ drop2run info brave-otter-4f2a brave-otter-4f2a — https://brave-otter-4f2a.dropto.live name Launch notes status active access public mode spa password off forms on takedown none folder Clients/Acme Versions, newest first: 01JQ7Y… 2026-10-06T09:12:44Z live, 42 files ← served 01JQ7X… 2026-10-05T16:03:10Z superseded, 41 files 01JQ7W… 2026-10-01T11:40:02Z superseded, 40 files, files collected — cannot be rolled back to

access is password, invite-only or public. The list is managed with invite, uninvite and people (below); setting a password on an invite-only site replaces invite-only, and set says so when it does.

set changes one setting at a time, on --site or the site in drop2run.json:

CommandWhat it does
set name <text>What the dashboard calls the site. Several words need no quotes; a word starting with - goes after --, as in set name -- Release - v2
set mode static|spa|docsstatic answers 404 for a missing path, spa falls back to index.html for a client-side router, docs reads documents through the viewer. Setting it stops the mode being detected on each publish
set passwordAsks for the password at a prompt that does not echo it
set forms on|offWhether the site's forms accept submissions
set expires <ISO 8601>Takes the site down at that moment, for example 2026-12-31T09:00:00Z
set folder <path|id|root>Moves the site to a folder, or root for the top level
set invite-only on|offOnly the people on its list can open it (replaces a password)
set comments off|invited|anyoneWho may comment: nobody, the people invited to comment, or anybody signed in
set upstream <origin> [header]Lets the site's /api/* routes call an API of yours, attaching that header; see below

unset name, unset password and unset expires clear those three. Changes reach every edge within about a minute, without publishing again. A setting your plan does not include is refused with the reason, and info marks those as (not on this plan).

The password never goes on the command line, where shell history would keep it. set password with a value after it is refused. In a script, pipe it in:

TerminalCode
printf %s "$SITE_PASSWORD" | drop2run set password --site my-docs

A takedown pauses by default. The files, versions and subdomain are kept, and resume brings the site back. --then delete deletes it instead when the date arrives, and asks for --yes the way rm does:

TerminalCode
drop2run set expires 2026-12-31T09:00:00Z --then delete --site my-docs --yes

Calling your own API

On paid plans, a site can send its /api/* calls on to an API of yours with the key kept off the page. Declare the API's origin and a header, then point a line of _redirects at it — Redirects has the rules and what visitors can do with it.

TerminalCode
drop2run set upstream https://api.openai.com authorization --site my-chat # Value for authorization: (typed, not shown)

One header per run, and its value is never an argument, for the reason the password is not: it is typed at a hidden prompt, or piped in. Run it again with another header name to add a second one; the same name replaces the value.

TerminalCode
printf %s "Bearer $OPENAI_API_KEY" | drop2run set upstream https://api.openai.com authorization --site my-chat

Visitors may only send GET (and HEAD) unless you open more with --methods. A method that writes lets anybody who can open the site write with your key:

TerminalCode
drop2run set upstream https://api.openai.com --methods GET,POST --site my-chat

info lists each upstream with its methods and each header by name and last four characters, the routes the live version sends there, and the month's allowance. unset upstream <origin> removes the upstream with its keys; unset upstream <origin> <header> removes one header.

Pausing

pause takes a site off the air now and resume puts it back. Nothing is deleted, so neither asks for --yes. resume is refused, with the reason, when your plan has no room for another active site.

Rolling back

TerminalCode
$ drop2run rollback 01JQ7X… brave-otter-4f2a is back on 01JQ7X…. https://brave-otter-4f2a.dropto.live

Deploy ids come from drop2run info, the site's page, or the --json output of an earlier deploy.

Deleting

rm is the one command that works out what it would act on and then stops:

TerminalCode
$ drop2run rm brave-otter-4f2a This would delete brave-otter-4f2a and every version published to it, with no way back. Run `drop2run rm brave-otter-4f2a --yes` if that is what you want.

There is no undo — the files go and the subdomain is released — and a terminal has no confirmation dialog, so --yes is the dialog. It has to be typed after seeing the name.

Sharing for review

TerminalCode
drop2run invite dana@example.com --role comment --site brave-otter-4f2a drop2run set invite-only on --site brave-otter-4f2a drop2run set comments invited --site brave-otter-4f2a

invite puts addresses on the site's list and emails each one; --role comment lets them comment as well as open it. It does not make the site private by itself — set invite-only on does, and says so when you invite before switching it on. drop2run people lists who is on it and drop2run uninvite <email> takes someone off. Both are plan features, refused with the reason where the plan lacks them.

Comments

On a plan with comments, comments reads what people left on a site's pages — open threads by default, --status resolved or --status all for the rest, --path /pricing for one page:

TerminalCode
$ drop2run comments brave-otter-4f2a 1 open comment thread on brave-otter-4f2a. 01JQ8Y… /pricing "free tier" Cam: The price here is last year's https://brave-otter-4f2a.dropto.live/pricing?d2r_comment=01JQ8Y…

The account's owner can answer, resolve and reopen. Those three name the site with --site or drop2run.json, like set, because the words after them are the thread and the reply:

TerminalCode
drop2run deploy dist drop2run comments reply 01JQ8Y… "Fixed — the new price is live" --site brave-otter-4f2a drop2run comments resolve 01JQ8Y… --site brave-otter-4f2a

Deploy before replying, or the person who commented opens the old version. A reply leaves the thread open unless --resolve is given, so the person who raised it can check the fix and resolve it themselves. A resolved thread is listed with the version it was resolved on. A reply text that starts with - goes after --.

In a script

Exit codes are what they should be: 0 on success, 1 on anything else. Failure text goes to stderr, so drop2run ls --json | jq works and an error never lands inside the JSON.

TerminalCode
set -e drop2run deploy dist --site my-docs --json > result.json jq -r .url result.json

--json on deploy gives the URL, the site id, the file count, and whether anything changed:

JSONCode
{ "url": "https://my-docs.dropto.live", "siteId": "01JQ...", "subdomain": "my-docs", "files": 42, "unchanged": false }

unchanged: true means every file matched what the site already served, so no version was published. It is a success, and a different one from having published.

On every push

Publishing from CI is one workflow step calling this same command — no action to install. See GitHub Actions.

What is not here

token create and token revoke, and those two never will be: a token cannot manage tokens, for the reason in Authentication. token list exists because it answers the question a terminal can answer — which machines are still holding a credential — and it prints prefixes, never secrets. Typing either of the other two fails with that explanation rather than listing instead, which would read as having worked.

Last modified on