uploads login is how people and agents get workspace credentials. Run with no
flags, it opens a browser for a device sign-in (GitHub or a magic link). On
approval, the CLI mints a scoped, expiring workspace token and saves it locally.
Nobody needs the API's ADMIN_TOKEN to sign in.
/account/developers mints the same up_<workspace>_ token uploads login
does. Pick a workspace, a label, read-and-write or read-only, and 90 days, 1
year, or no expiry. The secret is shown once. A token with no expiry lives
until you revoke it. The CLI reads the workspace from the token. Curl still
puts the workspace in the path (/v1/:workspace/…).
Install once for repeated use:
npm install --global @buildinternet/uploads
uploads login
uploads doctorOr run it once without a global install:
npx @buildinternet/uploads loginWith no code, uploads login prints a URL and a short code, opens the URL in
a browser automatically (unless --no-open), and waits while you approve the
sign-in there. Once approved, the CLI mints a token and saves it.
Pass --workspace <name> if your account can access more than one workspace:
uploads login --workspace acmeThe CLI authenticates as the managed official OAuth client uploads-cli,
visible and toggleable by operators in the admin panel at /admin/oauth.
releases-sh is the other seeded official client: a public PKCE
authorization-code app for releases.sh, scoped to
files:read and offline_access (so refresh tokens can be issued), with
user consent required and icon https://releases.sh/icon.svg. Both rows carry
metadata.official. The server blocks deleting an official client
(DELETE returns 409) until an operator first clears its official flag
(PATCH official:false). That two-step is deliberate. Each seed migration
runs only once (INSERT OR IGNORE, journaled as applied), so the system
does not re-seed a deleted official client automatically. A deletion of
uploads-cli would break CLI login fleet-wide until someone re-inserts
the row by hand, so prefer disabling the client over clearing its official
flag and deleting it.
On success, the CLI saves UPLOADS_API_URL, UPLOADS_WORKSPACE, and
UPLOADS_TOKEN in the shared buildinternet config and runs doctor. It never
prints the raw workspace token. Use --force only when intentionally
replacing an existing configured token. See uploads login --help for the
full flag list (--label, --scopes, --auth-url, --no-open,
--non-interactive, and more).
There are three ways to get workspace access: create your own (self-serve, no admin needed), an organization invitation from someone who already admins a workspace, or an enrollment code shared out-of-band.
Any signed-in user with a GitHub-linked account can create a workspace
without an invitation or ADMIN_TOKEN — /account/workspaces has a "Create a
workspace" form, and uploads login offers the same prompt when your account
has no workspaces yet. Both suggest a name based on your GitHub login when that
makes a valid, unclaimed workspace name; edit or replace it freely. When no name
can be derived — no linked GitHub account, or the name is reserved or already
taken — nothing is prefilled. Scripted or agent logins can skip the prompt with
uploads login --workspace <name> --create, which provisions the workspace
during login when the account doesn't already have it (browser device approval
is still required once). You become the owner of a new organization and a
<name>/ prefix on the shared bucket. Self-serve workspaces are capped at 3 per
user, with tighter default limits than an operator-provisioned workspace.
See workspaces.md#self-serve-workspaces
for the limits, name rules, and error codes.
Magic-link-only accounts get a github_required prompt to connect GitHub
first, in both the web UI and uploads login.
Workspace access also comes from an organization invitation, not a code you redeem. Someone who already admins that workspace (org role admin/owner) invites your email:
- Account UI —
/account/workspaces/<name>/people→ Invite section (also where admins manage members and roles; see ops.md#invitations-and-people) - CLI —
uploads invite create --email you@example.com --workspace <name>(device login as the inviter; noADMIN_TOKEN) - Site operators can also invite from
/admin(global admin session)
You get an accept link (email when Email Sending is configured; otherwise the
inviter shares the link from the UI/CLI). After accepting (GitHub or magic-link
sign-in), run uploads login. See
ops.md#invitations-and-people for the people
permission matrix, operator enrollment codes, and self-hosted email notes.
Administrators can also mint single-use enrollment codes (upe_…) via
ADMIN_TOKEN-authenticated POST /admin/enrollments, and uploads login --code exchanges one for a token directly, with no organization membership
involved. This is a secondary path, useful when you want to share a
code or link without knowing the recipient's email address in advance
(e.g. a link posted to a channel, or handed off out-of-band). Org
invitations above remain the primary, recommended way to onboard someone
whose email you know.
uploads login --code upe_…
# or, to avoid the code in shell history:
UPLOADS_ENROLLMENT_CODE=upe_<workspace>_… uploads login --code-stdinInteractive --code prompts without echoing the code. For automation, use an
ephemeral environment value rather than putting the code in shell history or
the process list.
Login-issued tokens default to:
- 90-day lifetime;
files:readfor list and metadata operations;files:writefor uploads;- no
files:deleteunless explicitly authorized.
The API stores token hashes, scopes, labels, creation time, and expiry — not raw tokens. Revocation uses the admin token-list and revoke endpoints (see admin-tokens). Existing tokens without scopes or expiry remain valid with their legacy access until deliberately rotated or revoked.
D1 stores every scoped/expiring token and, for the legacy enrollment path,
enrollment requests with atomic single-use redemption. REGISTRY KV retains
workspace storage configuration and legacy tokens. Create and migrate the
database before deploying auth-aware API code:
cd apps/api
pnpm exec wrangler d1 create uploads-production
pnpm exec wrangler d1 migrations apply DB --local
pnpm exec wrangler d1 migrations apply DB --remote
# or: pnpm --filter @uploads/api run migrate:d1Bind the database as DB in apps/api/wrangler.jsonc. Commit migrations under
apps/api/migrations/; remote apply runs via deploy:api, the D1 Migrations
GitHub Action on merge to main, or migrate:d1 manually. See deploy.
The dedicated apps/auth worker and its own uploads-auth D1 database carry
Better Auth's session/organization/device-code tables that back
uploads login and the /admin UI — see apps/auth/README.md.
uploads login's device flow (Better Auth's device-authorization grant) is
the default onboarding path today. Direct token minting (/admin/tokens) is
reserved for CI and break-glass administration. Rotate legacy routine-agent
tokens to login-issued tokens gradually, then revoke the old hashes.
The uploads-cli seed lives in 20260822120000_auth_tables.sql (replayed
from the retired apps/auth migration 20260719000000_seed_cli_oauth_client.sql).
20260915200000_seed_releases_sh_oauth_client.sql seeds releases-sh.
Both apply with the rest of apps/api/migrations (pnpm migrate:d1 /
migrate:d1:local). The device flow fails closed without the uploads-cli
row.