Skip to content
sitesolide

Docs People and tokens

People and tokens

Each project has a general access, Public, Restricted or Anyone with the code, and a list of people with access, each with a role: Can open, Viewer, Developer or Admin. The owner can do everything; tokens let people and agents deploy, never beyond their person's roles.

The owner of the machine deploys over SSH, as root. Nobody else needs that, and nobody else should have it. Two things let others work on the machine, and neither is root:

  • people with access: someone, by their email, holds a role on a project, opens its site when it is restricted, and from Viewer up signs in to the dashboard with their company account;
  • tokens: a key, personal and revocable, with which sitesolide deploy sends a project to the dashboard over HTTPS. The machine installs it the way it installs the owner's, and the holder never holds root. The same works for an agent in a sandbox with no SSH key: a token in its environment is all it needs. A token minted with Manage secrets also sets the variables of its projects, never reading one back.

In the dashboard, each of them has one place: a site's Access section for who may open that site and who may do what on it, the People page for everyone across projects, the owner's alone, and the Tokens page for the tokens.

A person with accessA token
Issomeone, by email, with a role on each of their projectsa key, sst_..., held by a person or an agent
Signs into a restricted site, with their company account or their password access; to the dashboard, with their company account, from Viewer up or with the right to create projectsnowhere: the CLI and the control API present it
Seesin the dashboard, the projects they are Viewer, Developer or Admin ofthe projects it may deploy, through sitesolide status
Doesby role, see below: from opening a site to looking after all of the projectdeploys, reads logs, restarts the service, gives Can open on what it deploys; with Manage secrets, lists variable names and sets and removes variables, never reading one back
Lastsuntil they are removed; half a day per sign-in to the dashboard, an unlock ten minutes; password access until its expiryuntil it expires or is revoked; a person's own, also until that person no longer signs in to the dashboard
In the audittheir emailtoken:<id>, and the person's email for their own

The two meet in one place: a person mints tokens of their own, from the Tokens page, never stronger than their roles. See Your own tokens.

A person holds one role on each of their projects. Can open is the lowest, Admin the highest, and each role includes the ones below it:

RoleOn that project
Can open (can-open in the CLI, visitor in the API)opens its site when its general access is Restricted, and nothing in the dashboard
Vieweralso sees the project in the dashboard: its state, audience and activity
Developeralso deploys it with a token of their own, restarts its service, and sets, replaces and removes its secrets without ever reading one back: the dashboard shows them names, the server never hands them a value
Admineverything of the project: reads its secrets, chooses its general access, Public, Restricted or Anyone with the code, and a new code, gives people access to it, a role at most their own, and restores its backups

Every role opens the site when its general access is Restricted: a Developer needs no Can open besides. A whole domain, written @acme.com, is Can open only. A person outside the company's domains, OIDC_ALLOWED_DOMAINS in the portal's settings, or anyone when signing in with a company account is not set up, is Can open only, with password access: a password the machine draws for them, for that one site, shown once to whoever gives it, until an expiry chosen then, 24 hours, 7 days (the default), 30 days or none (portal/README.md).

Who signs in to the dashboard: someone with a role above Can open on at least one project, or the right to create projects, with their company account. Someone with Can open alone, by name or through a domain, opens sites and nothing more: their sign-in is refused with can-open-only, "This account can open some sites, but the dashboard starts at Viewer. Ask an Admin of the project if you need more." Someone on no list is refused with no-role, "This account has no access to any project here. Ask the owner, or an Admin of the project, to add you." When their last role above Can open goes, removed or lowered, and they do not hold the right to create projects, their dashboard sessions close and their tokens are revoked at once.

The machine itself stays the owner's: the platform's own projects, the dashboard and the portal among them, their settings, the tokens of others, the connectors, and the roles people hold on projects they are not Admin of.

The owner is whoever holds the dashboard's password, the first account, made by setup, and may do everything, on every project and on the machine. Their People page lists everyone across projects, their roles, their password access, who may create projects, the domains, and the admin emails, "Every site, as admin: set on the server in OIDC_ADMIN_EMAILS"; sitesolide people prints the same from any folder (commands.md). Taking someone off there takes them off every project and revokes their tokens, which the confirmation lists; Remove expired clears every password access that has ended.

Creating projects is a right the owner grants per person, beside their roles: from People, or sitesolide people <email> --may-create, taken back with --no-create. It may be all they hold, someone asked to create their own projects, none yet; it takes an address that signs in with a company account. Whoever holds it mints a token that may create, and becomes Admin of each project that token creates, once its first deployment has ended, provided nobody has been given access to the project meanwhile; otherwise, and for a creation not settled within a day, the owner gives its roles by hand.

Giving someone access is the project's Admin's, or the owner's: from the site's Access section, or sitesolide share <email> --role <role> in the project's folder (commands.md). An Admin gives at most their own role, on their project alone; a domain only among the company's. No email is sent: the command prints the line to send, and a password once for password access. Removing someone, or lowering them, holds from their next request.

Signing in to the dashboard goes through the portal: the dashboard sends the browser there, the portal runs its usual sign-in with the provider, and hands the dashboard a short assertion it signed, which the server checks before it opens the session (dashboard/README.md). A person sees Sites and Activity, each reduced to their projects, Tokens for their own tokens when a role or the create right lets them mint one, and in a project the sections their role opens:

RoleA project's sections
ViewerOverview, Audience, and Access read only: its general access, its people with access, and whom to ask to add someone
Developerthe same, and Secrets, whose values they write and never read back
Admineverything: Overview, Audience, Secrets, Access, where they give people access, and Backups

The rest of the dashboard is the owner's, hidden and refused: People, Connectors, and every project they hold no role on.

A site's Access section reads like a "Share" dialog, in one column, with a link to its changes in Activity. First its general access, the three ways it may open, Public, Restricted or Anyone with the code, the current one marked; the owner and its Admins choose among the three there, Make public, Restrict or Use a code. A site that opens with a code shows it to them, with its link to copy, and New code, which stops the old one at once; a Viewer or a Developer reads that the site opens with one, and whom to ask. The code is drawn on the machine, six characters without O, I, 0 or 1, and sitesolide lock from the project's folder reaches the same path. A site that serves its own domain opens the same way on every address, the preview and the domain together, and the section names both before anything changes. Then its people with access: an Add people field taking an email or a @domain and a role, which says before anything is sent whether the person signs in with their company account or gets password access, and every entry with its role, password access with its expiry. One line under the list names who also opens the site without being on it: the owner, and the admin emails set on the server. What each role can do opens under the field, the reader's own marked. A Viewer or a Developer reads all of it without a control, and whom to ask to add someone.

Unlocking, for a person, is signing in again. Whatever reads or writes a secret, makes a site public, opens it with a code or gives it a new one, restores data, mints a token, gives someone a role above Can open, or gives password access asks for the person's own unlock: Unlock changes sends them to the identity provider, which asks them to prove themselves once more even if they are signed in there, and brings them back unlocked for ten minutes, for their session alone. The owner unlocks with the dashboard's password; neither unlock replaces the other, nor another person's. Restricting a site, giving Can open to a company account or to one of the company's listed domains, removing someone, lowering them and restarting a service never wait for an unlock. A whole domain does while the company's domains are not listed on the server, OIDC_ALLOWED_DOMAINS empty: anyone the provider vouches for would then come in. A token minted with Manage secrets sets variables without one: minting it took the unlock, and that permission is the opt-in, see Secrets with a token.

Someone who signs in to the dashboard deploys with a token they mint themselves on its Tokens page: for their workstation's CLI, for an agent. The server judges it, never the dashboard, and a person's token is never stronger than its person:

A token of theirsTakes
a project to deployDeveloper or Admin there
creating projectsthe create right the owner granted them; each project it creates makes them its Admin
deploying public sites, declaring a domain, outbound networkAdmin of every project the token reaches; with creating alone, what it creates is theirs to administer
giving Can open, sitesolide shareAdmin of that project
managing secrets, sitesolide secretsDeveloper or Admin there
restarting the service, sitesolide restartDeveloper or Admin there, as for deploying it

A Viewer everywhere, without the create right, mints nothing, and sees no Tokens page.

  • Minted under their own unlock, the forced sign-in at the provider that every secret write asks for: Unlock to create, then New token. The dialog offers the projects where they are Developer or Admin, creating projects only with the right, Manage secrets, and the options only for projects they administer. The token carries their email, whatever is sent, and is shown once, as the owner's are. Its row on Tokens names each project with their role there today, "calendar (Admin)", or "cms (paused: Viewer now)" where it no longer deploys, and Can manage secrets when it may, "Can manage secrets (paused: no project to deploy now)" once it reaches none.
  • Narrowed live. The server reads the access registry at every use of the token, not only when it was minted: a role lowered to Viewer or Can open stops that project's deployments at the next request, the create right taken back stops new projects, and the options hold only while the person is Admin of every project the token reaches; one lowered turns them off for the whole token, and the person mints another for what they still administer. Manage secrets holds project by project, wherever they are Developer or Admin at that request. The installer reads the registry once more when it starts.
  • An existing project keeps its general access. A deployed site's general access, Public, Restricted or Anyone with the code, is chosen by its Admin or the owner, never by a deployment: a Developer's token deploys a public project as it stands, and opens nothing. A new project is restricted unless the token may deploy public sites; leaving paths open to anyone (portalExempt) takes that option too.
  • Giving access, sitesolide share, takes the person's own power: Can open alone, on a project where they are Admin now.
  • When they no longer sign in to the dashboard, their tokens go with them: revoked by the server at once, under whoever removed or lowered them, and refused anyway, since the registry no longer gives them a role. Revoking one needs no unlock, from their Tokens page, or the owner's, which lists every token and who made it, "Made by alice@example.com" or "Made by you for alice@example.com".
  • Ten live tokens per person, so that one person cannot fill the machine's registry.

Everything is audited under the person: the server journals token.create and token.revoke under their email, and project.create for a project their token created; the dashboard's deployments, under token:<id>, name the person in their detail, and so do the secrets their token set or removed and the restarts it made, which the steward journals under token:<id> with the person beside it. The Tokens page's Token activity lists both.

  • the dashboard's address, https://dashboard.<zone>;
  • a token, sst_..., shown to the owner once when they created it on the dashboard's Tokens page. Keep it like a password: whoever holds it deploys what it allows. Someone who signs in to the dashboard mints their own instead, see Your own tokens.

A token the owner creates deploys only what they allowed when creating it: the existing projects they granted, and, if they allowed it, new projects, which are then yours. It sets secrets only if they ticked Manage secrets. Everything is private by default: a project you create is Restricted, unless your token may deploy public sites.

You need the CLI, one executable that needs neither Bun nor a clone:

curl -fsSL https://github.com/cthiriet/sitesolide/releases/latest/download/install.sh | sh
sitesolide login --url https://dashboard.<zone>

login asks for the token, at a prompt that does not echo it, checks it against the dashboard, and says who it belongs to and what it may do. It keeps the token in ~/.config/sitesolide/secrets/team-token (0600), the address it checked it against beside it, team-token-api, and the address in ~/.config/sitesolide/config.json under api. --token-stdin reads the token from standard input instead of a prompt.

The token in the vault goes to that address alone. config.json is no secret, and the environment chooses which one is read: a SITESOLIDE_API or a config.json naming another dashboard would otherwise have sent the token there. Either is refused, unauthenticated, saying to run sitesolide login --url for the new address. A vault filled before login wrote the address sends its token to the api of the config.json beside it, as it did; one kept elsewhere, by vault or SITESOLIDE_VAULT, needs sitesolide login once more. A team-token or a team-token-api that is a link, or that has a second name, is not read. The dashboard's certificate is checked whatever the environment says, NODE_TLS_REJECT_UNAUTHORIZED=0 included. What the CLI cannot refuse is an environment that adds an authority of its own, NODE_EXTRA_CA_CERTS, beside a proxy, HTTPS_PROXY, that answers with a certificate it signed: an agent's sandbox is set up by whoever trusts it.

A workstation with no server in its configuration uses the dashboard for deploy, status, logs, share, secrets and restart. Every other command (lock, domain, remove, run, people) needs the owner's SSH access and says so.

No files to keep: set two variables, which win over the files.

export SITESOLIDE_API=https://dashboard.<zone>
export SITESOLIDE_TOKEN=sst_...
sitesolide deploy

Both, together: a SITESOLIDE_API alone does not take the vault's token elsewhere.

From the project's folder, the one with its sitesolide.json:

$ sitesolide deploy
-> project notes, through https://dashboard.example.com
   deployment 3f2a..., a new project
-> archive
   12 file(s), 48211 bytes, 9120 compressed
-> upload
-> manifest, validated on the machine
   port 3002 chosen for the service
   project notes, service, restricted
-> the machine
...
-> verify
   https://notes.example.com/ 401, restricted: visitors are asked to sign in
-> deployed: https://notes.example.com/

In order:

  1. The manifest goes first. The dashboard judges it, with the same validation as the owner's CLI and your token's scope, before anything is built or sent. Every reason it refuses comes back at once.
  2. The build runs on your workstation, as for the owner: the machine receives its result, not your toolchain.
  3. The archive carries your code in app/, without what exclude lists and the manifest, and your publicDir in public/: exactly what the owner's rsync would send. Neither tree ever carries .git nor a .env, at any depth. Symbolic links are refused; replace them with the files they point to.
  4. The machine installs it: it extracts the archive as the project's own account, runs your install there, then follows the same steps as the owner's deployment. You read its log as it goes, and the command exits non-zero if the deployment fails.

The port is optional. A manifest with a start and no port gets the lowest free one, and keeps it on later deployments. The CLI prints it: write it in sitesolide.json to make it explicit.

In the manifestRefused unlessWhy
no portal, or portalExemptthe token may deploy public sitesprivate by default
a static site (no start)the token may deploy public sitesa static site cannot be restricted yet
"network": "outbound"the token may use outbound networkservices reach only the loopback otherwise
egressthe token may use outbound networkthe hosts it lists are reached through the egress proxy, a way out all the same
domainthe token may declare a domainswitching to it stays the owner's job: the machine's active stands whatever the manifest says, and a name or an alias it did not cover before stays inactive
secrets other than <slug>.envneverthe unit hands the file to the service as root
locknevera preview code is chosen in the site's Access section, by its Admin or the owner, or with sitesolide lock over the owner's SSH
memory above 1G, more than 6 servicesneverone machine serves everyone

A project that already exists keeps the general access the machine carries: it is switched from the dashboard, never from a deployment. A token without the public permission does not deploy an existing public site either, so that a stolen one publishes nothing; a person's own token does, general access being its Admin's choice, which the deployment leaves as it is.

connectors needs no permission of the token: a manifest only asks for one, and nothing reaches it until the owner grants it to the project from the dashboard.

Every string of the manifest that lands in the project's Caddy block or systemd unit is judged for that, the same way for the owner and for a token: header values without ", \, {, }, $ or a backtick, routes that are plain paths, a start that begins with the program itself, environment values with no space or quote, secret names that are plain file names. See docs/manifest.md.

Secrets live on the machine and nowhere else. A project that declares <slug>.env deploys only once the file exists: the owner, or the project's Developer or Admin, creates it in the dashboard's Secrets section, or a token that may manage secrets with sitesolide secrets set, see Secrets with a token. Until then the deployment says so and stops before the restart.

install runs on the machine as the project's account, with the network but not the loopback, none of the machine's ports to listen on, a throwaway HOME, and fifteen minutes at most. The tools it calls must be on the machine (bun is; ask the owner for others), and a tool that installs an interpreter into HOME must use the machine's own instead, since HOME is gone afterwards.

sitesolide status          # the projects your token reaches, their service, and their general access
sitesolide logs            # the journal of this folder's project
sitesolide logs --follow

--json works here as over SSH (see agents.md): one event per line, and one final result or error carrying a hint. deploy --dry-run is refused, and nothing is sent: a dry run reads the machine over the owner's SSH access, which a token does not carry. Review sitesolide.json instead; the machine judges it before anything is built or uploaded. Every other option a command does not take with a token is refused the same way.

A token minted with Manage secrets lists the secret files of the projects it reaches, with their variable names, and sets and removes the variables of <slug>.env. It never reads a value back: no route returns one, nor a file's size, nor its previous version, whoever holds the token, an Admin's own or the owner's included. From the project's folder:

sitesolide secrets                         # its files and variable names
sitesolide secrets set API_KEY             # the value typed at a prompt that does not echo it
sitesolide secrets set API_KEY --stdin < key.txt
sitesolide secrets remove OLD_KEY LEGACY_URL --restart
sitesolide restart                         # restart, then say whether the service holds
$ sitesolide secrets
-> secrets of notes, through https://dashboard.example.com
   service: active, running
   notes.env: API_KEY, SMTP_URL
      restart pending: changed since the service started, sitesolide restart applies it
   notes-signing.key: a file read whole, set from the dashboard's Secrets section
   a value is never read back: sitesolide secrets set NAME writes one

The value never goes on the command line, where the shell's history and every user's process list would keep it. secrets set NAME asks for it at a prompt that does not echo it, or, with --stdin, reads all of standard input, one trailing line break dropped; --stdin is refused while standard input is a terminal, where the value would show as it is typed (stdin-is-terminal): pipe it, or drop --stdin to be prompted. At the prompt, Backspace, Ctrl-U and Ctrl-W edit the value; an escape sequence, an arrow key among them, any other control key, or a paste of several lines is refused, what remains of the paste drained, and nothing is sent. NAME=value, or anything after the name, is refused before a request leaves, and so is a name that looks like a value, over 64 characters or starting like a service's key (sk_, ghp_, AKIA and their kind, looks-like-value); neither refusal quotes it, and a value typed there may be in the shell's history, to be changed at its source. The value is checked before it leaves by the steward's own rules, one line, no null byte, valid Unicode with no noncharacter, 8 KiB at most, so that a refused one was never sent; an empty one is refused too: an unset variable piped by mistake would empty the secret in service. Under --json nothing prompts, and set needs --stdin. A change sent whose answer never came, the connection cut, is unconfirmed: it may be written; sitesolide secrets --json lists the names, and the same command run once more writes it again, harmlessly.

A token with Manage secrets mayIt may not
list its projects' secret files, their variable names, and whether a restart is pendingread a value, a file's size or its previous version
set, replace and remove the variables of <slug>.env, when the deployed sitesolide.json declares ittouch any other file: another .env, a file read whole, a file of the platform's projects; those are the dashboard's Secrets section
create <slug>.env when the deployed manifest declares it and it is missingcreate or change a <slug>.env the deployed manifest does not declare
restart the service, with --restart or sitesolide restartchange a password hash, PASSWORD_HASH, which the owner alone changes
set a variable the deployment or the unit already sets, PORT among them, or one that changes how the runtime behaves, LD_PRELOAD or HTTPS_PROXY: refused to anyone, from the dashboard too
  • One change, one write, all or nothing. Every name and value is judged before anything is written; the file keeps its previous version, which the owner or an Admin restores from Secrets. Fifty names a change at most; a value on one line, with no null byte, valid Unicode, 8 KiB at most, and no Unicode noncharacter, U+FDD0 to U+FDEF or the last two code points of a plane, for which systemd would read none of the file's variables; the file 256 KiB at most. A <slug>.env a token creates starts with no previous version, whatever an earlier file of that name left. A change that would grow the file past that is refused whole, which tells whoever holds the token, roughly, how large the file already is: the one thing it learns of what it cannot read.
  • A set never says whether the value changed: it is always written and journaled. A name to remove that the file does not carry comes back in absent, nothing done for it.
  • Listing needs Manage secrets too: a token without it is refused, and the refusal is journaled, as "Tried to list the secrets", never as a value read.
  • Restarting takes no permission of its own: any token that deploys the project restarts it, a person's own while they are Developer or Admin there. It is refused busy while a deployment of the project runs, which restarts it at its end, asked again once the restart's turn has come, and not-found for a static site. A token restarts one service at a time: a second restart while its first runs is busy. Restarts of one project wait for each other, never for anyone's change of secrets. The service is watched for eight seconds: active, looping or failure, and sitesolide restart exits non-zero unless it holds.
  • Bounds, per token and per hour: 120 for changes, each variable name set or removed costing one, a request one at least, {} included, and a file it created one more; 30 restarts; and 60 refused requests, counted before the refusal is journaled, a refused change of access among them (see Giving access). Past any of them, too-many-attempts and the seconds to wait, and a refusal past its bound is journaled no more. One token writes 210 lines of the steward's journal an hour at most, and the journal keeps its latest 500 lines at least: a stolen token cannot push what it did in the last hour out of it by itself.
  • No unlock. A person's change in the dashboard asks for their unlock; a token's does not. Minting it took the unlock, and Manage secrets is what its maker opted into: whoever holds the token sets values until it expires or is revoked, within the bounds above, every change in sight on Token activity.
  • An owner's token without the public permission changes the secrets of a project only when it could deploy that project as the machine carries it, no path exempted from the portal, no domain or outbound network it could not declare itself, and the project is Restricted, the portal in place in front of it: a stolen one changes nothing of a project in the open, nor of one it could not deploy. What the people given access to a restricted project reach, the values it changes still drive. It still lists and restarts. A person's own token follows their role instead, as for a deployment.
  • A person's own token needs them Developer or Admin of the project at each request: lowered to Viewer, it is refused, the message naming their role. A token revoked, or its person lowered, while the change waited its turn writes nothing.

Every change is journaled by the steward, one line per variable, its name and never its value, under token:<id> and, for a person's own token, their email: the project's Activity reads "Set API_KEY" by "alice@example.com (token)", and the Tokens page's Token activity lists it beside the token's deployments. So is every refusal the steward decides, within the bound above: a person lowered, a token without Manage secrets, a file other than <slug>.env. The dashboard refuses a malformed body alone before asking it. Token activity reads the journal's latest 250 lines: on a busy machine they cover a short while, and an older change is on the project's Activity until the journal drops it.

For an agent: sitesolide secrets set NAME --stdin is for the user to run, or for a value the agent generates itself and pipes, a random session key; never a value read from the conversation, which the conversation then keeps. There is no MCP tool for it.

The value stays out of every command line, and out of the shell's history: IFS= read -rs takes it without echo, its spaces and backslashes kept as typed, printf, a shell builtin, hands it to jq on its standard input, and curl sends the body from its own.

API=https://dashboard.<zone>
AUTH="Authorization: Bearer $SITESOLIDE_TOKEN"

# the files and variable names, never a value
curl -s -H "$AUTH" $API/api/v1/projects/<slug>/secrets

# one variable set, then the service restarted
IFS= read -rs VALUE
printf '%s' "$VALUE" | jq -Rs '{set: {API_KEY: .}, restart: true}' |
  curl -s -H "$AUTH" -X PATCH -H 'Content-Type: application/json' --data-binary @- \
    $API/api/v1/projects/<slug>/secrets/<slug>.env
unset VALUE
# -> 200 { "file": { "name": "<slug>.env", "variables": ["API_KEY", ...], "writable": true, ... },
#          "created": false, "set": ["API_KEY"], "removed": [], "absent": [],
#          "restart": { "verdict": { "kind": "active", "state": "active", "subState": "running", "restarts": 0 } } }

# names alone carry no secret: they go on the line
curl -s -H "$AUTH" -X PATCH -H 'Content-Type: application/json' \
  -d '{"remove": ["OLD_KEY"]}' $API/api/v1/projects/<slug>/secrets/<slug>.env

curl -s -H "$AUTH" -X POST $API/api/v1/projects/<slug>/restart

-H "$AUTH" puts the token among curl's arguments, as everywhere on this page; on a machine others share, -H @file reads the header from a file instead. When a change asks for a restart and the restart is refused, busy or past its bound, the write stands with its own status, and restart says why. A change with a restart may take two of the steward's long waits, the write's then the restart's: the dashboard keeps the connection open for both.

A project you deploy with a token is Restricted, and opens to its people with access, the owner and the admin emails alone. Once the owner has set up signing in with a company account (portal/README.md), you give people Can open from its folder:

sitesolide share                              # its general access and its people with access
sitesolide share alice@acme.com bob@acme.com  # Can open for them
sitesolide share @acme.com                    # everyone at acme.com, one of the company's domains
sitesolide share --remove bob@acme.com        # refused from their next request
$ sitesolide share alice@acme.com
-> access to notes, https://notes.example.com/, through https://dashboard.example.com
-> alice@acme.com: Can open on notes
   general access: Restricted: visitors are asked to sign in.
   people with access:
     alice@acme.com  Can open
     @acme.com       Can open
   The owner also opens it.
   send: Open https://notes.example.com/ and sign in with your company account.

The send: line comes only after a change that gives someone access with an account, never on a plain listing. A token never reads the admin emails, so the line before it names the owner alone. The server judges each change by the access rules, writes it, and records it under your token, token:<id>, never as the owner; it holds from the next request. What a token may do is narrower than what an Admin or the owner may:

You mayYou may not
give Can open on a project your token reaches, your own or one granted to it; with a person's own token, one where that person is Admin nowtouch another token's project, which reads as unknown
give it to people who sign in with a company account, by their company emailgive password access: someone outside the company's domains is refused, and an Admin or the owner gives it from the dashboard
give it to one of the company's domains, OIDC_ALLOWED_DOMAINSgive another domain, or any domain at all when that list is empty
take a Can open entry offgive, change or take off Viewer, Developer or Admin: those are given from the dashboard, or by the owner over SSH
change a site's general access, Public, Restricted or Anyone with the code: it is the dashboard's, its Admin's or the owner's

Bounded per token and per hour. Each change is counted among the token's 60 refused requests of the hour before it is judged, and given back once accepted, or when it takes off someone who had no access: a refused change is a line of the steward's journal, and past 60 the token is refused too-many-attempts, journaled no more, so that a stolen token cannot push the hour's changes out of the journal. What a token lets in also stops once the access log is half full, see When it goes wrong.

Giving access never touches Caddy, and a project not deployed yet is refused with not-found. A project whose general access is Public keeps its list, and Can open matters once it is Restricted: the command says so, "notes is public, so anyone can open it. Viewer, Developer and Admin still apply; Can open matters once you restrict it."

Everything the CLI does is plain HTTPS with Authorization: Bearer <token>. Every refusal is { "error": "<code>", "message": "<what to do>" }, with details listing every reason for a refused manifest.

API=https://dashboard.<zone>
AUTH="Authorization: Bearer $SITESOLIDE_TOKEN"

curl -s -H "$AUTH" $API/api/v1/whoami

# 1. the manifest, judged before anything is sent
curl -s -H "$AUTH" -H 'Content-Type: application/json' \
  -d "{\"manifest\": $(cat sitesolide.json)}" $API/api/v1/deployments
# -> 201 { "deployment": { "id": "...", "state": "awaiting-bundle", ... } }

# 2. the archive: a gzip-compressed tar holding app/ and public/ only
tar --format=ustar -czf bundle.tar.gz app public
curl -s -H "$AUTH" -X PUT --data-binary @bundle.tar.gz \
  $API/api/v1/deployments/<id>/bundle

# 3. follow: `after` is the previous answer's `next`
curl -s -H "$AUTH" "$API/api/v1/deployments/<id>?after=0"

# 4. give Can open on it
curl -s -H "$AUTH" -X PUT -H 'Content-Type: application/json' \
  -d '{"who": "alice@acme.com"}' $API/api/v1/projects/<slug>/access
# -> 201 { "entry": { "who": "alice@acme.com", "kind": "person", "role": "visitor", ... }, "change": "add", "access": { ... } }
RouteWhat it does
GET /api/v1/whoamithe token's holder, scope and projects
POST /api/v1/deployments { manifest }judges the manifest, opens a deployment for 15 minutes; a token holds three waiting for their archive, a fourth replaces the oldest
PUT /api/v1/deployments/<id>/bundlethe archive, 100 MiB compressed at most; starts the installer
GET /api/v1/deployments/<id>?after=<n>state, log lines from n, next, error, url
GET /api/v1/projectsthe projects the token reaches, with their status
GET /api/v1/projects/<slug>one of them
GET /api/v1/projects/<slug>/logs?lines=<n>&cursor=<c>the journal, and a cursor for the next call
GET /api/v1/projects/<slug>/access{ access }: slug, host, url, general (access: public, restricted or code, modifiable, reason; null for a project not deployed), entries (each who, kind: person, domain or password, role, by, createdAt, updatedAt, and password, { expiresAt, expired } for password access, null otherwise), and signIn (configured, and allowedDomains, the only domains your token may give)
PUT /api/v1/projects/<slug>/access { who, role }gives who, an email or a @domain, Can open: role is visitor, its default, and any other is refused; { entry, change, access }, change being add (201) or none for someone who already had it
DELETE /api/v1/projects/<slug>/access { who }takes a Can open entry off; { entry, change: "remove", access }
GET /api/v1/projects/<slug>/secretsneeds Manage secrets. { secrets }: slug, service (unit, state, subState, startedAt; null without one), and files, each name, kind (variables or content), state (managed, absent or unmanaged, then with a reason), expected (its owner and mode), variables and passwords (names), modifiedAt, restartPending, and writable, true for a declared <slug>.env alone; readable is false, bytes null and previous false, whatever the token
PATCH /api/v1/projects/<slug>/secrets/<slug>.env { set, remove, restart }needs Manage secrets. set, an object of names and values, remove, a list of names, restart, true to restart once written: each optional, {} alone creates the file; 50 names at most, none in both, 512 KiB at most. One write, all or nothing: 200, or 201 when it created the file, { file, created, set, removed, absent, restart }, restart being null, { verdict }, or { error, message, wait } for a restart refused, wait there for too-many-attempts alone, the write's status kept
POST /api/v1/projects/<slug>/restartany token that deploys the project. Restarts its service and watches it eight seconds: 200 { verdict: { kind, state, subState, restarts } }, kind being active, looping or failure, 200 even when it did not come back
CodeStatusWhat to do
unauthenticated401the token is missing, unknown, expired or revoked, or its person no longer signs in to the dashboard: ask the owner, or mint another if it is your own
too-many-attempts429too many wrong tokens from your address: wait wait seconds; or, creating a project, your changes of access for this hour used up: try again later; or the token's hour of secrets, restarts or refusals used up, 120 changes, 30 restarts and 60 refused requests, refused changes of access included: wait wait seconds
out-of-scope403the token may not do this: outside its scope, its person's role no longer allows it, or, for access, a role above Can open, someone who would need password access, a domain outside the company's, an entry above Can open to take off; for secrets, a token without Manage secrets, a file other than <slug>.env, a <slug>.env the deployed manifest does not declare, PASSWORD_HASH, or, with an owner's token that does not deploy public sites, a project not Restricted on the machine or one it could not deploy as it stands; the message says whom to ask
reserved403the slug belongs to the platform, or, for a new project, starts with the name of one of its projects and a dash (dashboard-*, portal-*), which names the platform's own files, or is the name of a removed project the machine still keeps backup snapshots or connector grants of, which a project created under it would be handed: pick another, or ask the owner, who may deploy it over SSH or clear what was left
invalid-manifest422fix every point of details
invalid400the request itself is malformed: who neither an email nor a @domain, a domain given more than Can open, or a domain while signing in with a company account is not set up; for secrets, an unexpected field, a name that is not a variable's or one the machine keeps for itself, a value over one line, with a null byte, not valid Unicode, with a Unicode noncharacter or over 8 KiB, a name both set and removed, over 50 names, or a file that would pass 256 KiB; the message names a variable only when it has a variable name's shape, and never quotes a value
not-found404no such deployment or project for this token, a project not deployed yet, or, taking someone off, someone with no access; a static site, which has no service to restart; a <slug>.env missing when the change only removes
busy409a deployment of this project is already running, or, when the archive arrives, three deployments already run on the machine: send it again in a minute, the deployment waits for it until its 15 minutes are up; for secrets and restarts, a deployment of the project running, which restarts it at its end, the token's previous restart still under way, or the machine busy with other changes: ask again in a moment
unmanaged409the secret file is there, in a form the dashboard does not rewrite, written by hand: nothing was changed; the owner fixes it from Secrets
too-large413exclude dependencies and caches; a change of secrets over 512 KiB: split it
expired410the archive arrived after 15 minutes: start again
not-available503the machine does not carry the control API yet, or predates the access registry or secrets by token: tell the owner, who runs sitesolide upgrade; or, creating a project or giving Can open, the access log is half full, the half a token may fill, or full: ask the owner, or someone who gives access from the dashboard
failure500, 502something broke on the machine, an access registry that does not read among others, or, creating a project, the backups' index or the connectors' grants that do not read, which never pass for nothing left under the name: the message says where the owner should look

locked never comes back from this API: it is the dashboard's answer to a change that needs its unlock, a role above Can open or password access, which a token never gives.

A failed deployment carries its own error.code, the step that stopped it: install-failed, secret-missing, edited-by-hand, system-unit, caddy-busy, bundle-refused, service-failed, verify-failed and a few more, each with a message that says what to do. secret-taken stops it before anything is written when the project would read a secret file that is not its own: a file of the machine, one another project holds, or a <slug>.env already there that does not belong to the project's account; it names the file, never what it holds. A schedule systemd on the machine does not read comes back as invalid-manifest, before anything is written; a server or a worker that falls within ten seconds of its restart, as service-failed.

  • not-available: the owner has not installed the control API on this machine yet, see dashboard/README.md, "The control API"; or, for share, the machine predates the access registry, which sitesolide upgrade brings.
  • out-of-scope on share: the message says which rule: a role above Can open, someone outside the company's domains, who needs password access, or a domain the company does not list. Ask an Admin of the project, or the owner, to give it from the dashboard; never try another token.
  • Too many changes, or the access log full: the server keeps every change of access 180 days and never pushes one out early, and counts, per person or token and per hour, the changes that let more people in or more done: adding someone, raising a role, creating a project. Past 120 in the hour it refuses them: wait, then try again. Once the access log is full it refuses them until older changes age out, while removing and lowering someone still work: ask the owner, who still changes access from their workstation with sitesolide share and sitesolide people. A token stops earlier, at half the log, the rows its removals may still write counted: the other half stays for the people who give access from the dashboard, so that no token fills it for them.
  • out-of-scope on secrets: the token was minted without Manage secrets, the file is not <slug>.env, or the deployed sitesolide.json does not declare it: add "secrets": ["<slug>.env"], deploy, then set. Mint a token with the permission, or ask the owner; never try another token.
  • unmanaged on secrets set: someone wrote the file by hand, export lines or quotes the dashboard does not read. Nothing was changed: the owner rewrites it from Secrets.
  • A warning that the portal still decides from its own tables: the machine is halfway through an upgrade. What you give is kept, and opens the site once the owner has run sitesolide upgrade, which deploys the portal.
  • edited-by-hand: a file of your project on the machine, its unit or its Caddy block, was changed by hand. The owner reads it and deploys once over SSH with --force; yours works again afterwards.
  • caddy-busy: the owner or the dashboard is changing Caddy. Nothing served changed: deploy again in a moment.
  • A project you created and can no longer deploy: your token was revoked. Your projects stay on the machine; the owner grants them to your new token. Once the owner removes one with sitesolide remove, its name is free again: any token that may create projects may create one of that name, once the machine keeps none of its backup snapshots and none of the connector grants it had. Until then the name is reserved to every token: a project created under it would list its predecessor's snapshots, restore them, and be lent its credentials. The owner deploys it over SSH, or clears what was left. A person's own token answers to their role instead: a project they created is theirs to deploy as long as they are its Developer or Admin, with any token of theirs that names it.