CLI
Code
Node 20 or newer.
Code
--json works on every command.
Signing in
Code
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
Code
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:
Code
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 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
Code
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:
Code
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:
Code
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:
Code
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
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:
| Command | What 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|docs | static 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 password | Asks for the password at a prompt that does not echo it |
set forms on|off | Whether 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|off | Only the people on its list can open it (replaces a password) |
set comments off|invited|anyone | Who 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:
Code
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:
Code
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.
Code
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.
Code
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:
Code
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
Code
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:
Code
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
Code
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:
Code
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:
Code
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.
Code
--json on deploy gives the URL, the site id, the file count, and whether anything changed:
Code
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.