carlos / carloku Public

🤖 The console, members and service credentials, limits and help

The last three docs pages: what each console page shows (written from
internal/console handlers and templates, not from a browser), who can act
on an account and how a credential a server holds is minted, rotated and
revoked, and the rate limits a customer can meet with the numbers read
from internal/console/authcli.go.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev a66359bd7c06ce4db576aa94f6a72b65412644f9 parent 9309861
3 files changed, +263 −0
  • src/docs/console.md +78 −0
  • src/docs/limits.md +76 −0
  • src/docs/members.md +109 −0
diff --git a/src/docs/console.md b/src/docs/console.md
new file mode 100644
index 0000000..edbdd85
--- /dev/null
+++ b/src/docs/console.md
@@ -0,0 +1,78 @@
+# The console
+
+[console.carloku.com](https://console.carloku.com) is the same platform your
+terminal talks to, with a browser in front of it. You sign in with a passkey
+through Keymail, and the account you sign in to is the one
+`carlos auth login` connects a terminal to — the short code the CLI prints
+is approved in this browser.
+
+Most weeks you will barely open it. Deploys, config, logs and domains are all
+commands, and the console reads the records those commands write. Where it
+earns its place is the handful of jobs with no command behind them, and
+looking at an app somebody else on your team deployed.
+
+## What each page shows
+
+The bar across the top is per account: Apps, Fleet, Team and Settings, plus
+a switcher for moving between accounts when you belong to more than one.
+
+Apps is the list: each app's name, the address it answers on, a health chip,
+and the version live right now. A Trash link appears under it once something
+is in it. Opening an app gives you six tabs.
+
+Overview answers *is it up* in a sentence — the hostname, the running version,
+when it was published and by whom — then tiles for running copies, CPU and
+memory, web addresses and versions, and a left-to-right diagram of the
+channels a version travels through. [Channels](/docs/channels/) explains what
+that diagram is drawing.
+
+Versions is the release history. Every version you have shipped with its
+label, kind and build time and a promote control per row; underneath, the
+channels, what each holds, when a baked version goes live, and rollback.
+Canaries and semver tags sit at the bottom of the same page.
+
+Running is the copies themselves — address, health, version, channel, CPU,
+memory — and where the app is placed. A copy reading *asleep* is annotated
+there as waking on the next request, which [Restarting and
+sleeping](/docs/sleeping/) explains at more length.
+
+Logs is the window `carlos logs` prints, with a form for narrowing it.
+Activity is the other one: when, who, what, for every change made to this
+app. That record has no CLI verb, so the console is where you read it.
+
+Settings carries the app's name and rename, its domains with their DNS and
+TLS state alongside the attach form, its object store, its email
+configuration, a link through to the pipeline rules, and deletion.
+
+Back at account level, Fleet answers where this account's work actually runs:
+the shared platform boxes your apps are placed on, and any boxes of your own.
+Team and Settings are the subject of [Members and service
+credentials](/docs/members/).
+
+## Jobs that only happen here
+
+Approving a terminal is the first one you meet: `carlos auth login` prints a
+code, and nothing is granted until you type it into the console and confirm.
+
+The rest cluster around authority and permanence. Inviting a teammate,
+removing one and handing over ownership live on the Team page; minting,
+rotating and revoking a credential a server holds live on Account settings.
+
+Deletion is the other cluster. A deleted app goes to Trash and stays
+restorable for 30 days, keeping its releases, channels and history —
+restoring gives all of that back with no routes, so run
+`carlos add <host>` afterwards to serve it again. An owner can also destroy
+one immediately from the same page, which asks for the app's name typed out
+and then for your passkey.
+
+And when the day's app-creation budget is spent, the console's Create app
+form is the way through — it has no limit of its own. [Limits and getting
+help](/docs/limits/) has the numbers.
+
+## Your own settings
+
+Your address in the header opens a page listing every terminal signed in as
+you: where it signed in from, when, when it was last used, and when it
+expires. Each row has a revoke control. A laptop that went missing is the
+reason this page exists — revoking there kills that terminal's access
+without touching anyone else's.
diff --git a/src/docs/limits.md b/src/docs/limits.md
new file mode 100644
index 0000000..9c7f36d
--- /dev/null
+++ b/src/docs/limits.md
@@ -0,0 +1,76 @@
+# Limits and getting help
+
+The numbers on this page exist to make a stolen token boring, not to ration
+ordinary work. You are unlikely to meet any of them by hand. An automated
+loop is another matter, so here they are, along with what the refusal looks
+like when one does bite.
+
+## Shipping
+
+Two uploads a minute, counted per terminal token. That is past what an honest
+build-test-ship loop can produce, since a build worth shipping takes longer
+than half a minute to make.
+
+A refusal is a 429 that names the wait in seconds and repeats it in a
+`Retry-After` header, so a script has a number to sleep on instead of a
+guess. The window is a minute for exactly that reason: the worst a refused
+caller is ever asked for is sixty seconds. Sleep, retry, carry on.
+
+Size is the other ship limit. One request carries 256 MiB across everything
+in it. A static release is a single archive; a binary release can carry up to
+eight targets at once, which is well past what a normal build produces.
+
+## Everything else you type
+
+Ordinary mutations — env, secrets, promotes, rollbacks, restarts,
+schedules — get thirty a minute, counted against your address and not your
+terminal, so opening a second shell does not double it. The instance verbs
+get sixty a minute on their own budget, because a burst of signups creating
+instances is the workload that API exists for.
+
+Claiming an app name is the tight one: ten attempts a day per address, and
+attempts, not successes, so refused tries count. A claimed name persists
+whether or not you ever use it, which is why this ceiling runs over a day and
+the others run over a minute. When you hit it, the message points at the
+console's Create app form, which has no limit of its own. That is the
+intended way through, not a loophole.
+
+## Credentials a server holds
+
+A credential minted with `carlos services` draws on its own budgets, larger
+than a person's and entirely separate: ten ships a minute, a hundred and
+twenty other writes a minute, a hundred app claims a day. A CI fleet cutting
+releases back to back therefore cannot starve the person who minted it, and
+the person's noisy afternoon cannot stop CI.
+
+The counters key on the credential's name, not its secret, so rotating one
+does not hand it a fresh budget. [Members and service
+credentials](/docs/members/) covers the rest of how they work.
+
+## Things that expire
+
+Logs go back seven days, with a size ceiling per app that drops the oldest
+hour first — [Logs](/docs/logs/) has the detail. A deleted app sits in Trash
+for thirty days before the platform destroys it. A terminal you have signed
+in stays valid for ninety days, and you can revoke it sooner from your own
+settings page. An invitation to join an account lasts seven days and works
+once.
+
+## When something is refused
+
+Read the message before anything else. The platform's refusals are written to
+be acted on: a rate limit names the seconds to wait, an app you cannot claim
+today names the console form that will let you, an attached domain that is
+not resolving prints the DNS record it wants. Very few need interpreting.
+
+After that, in rough order of how often it helps: `carlos <verb> -h` for the
+flags, [the CLI reference](https://carlosframework.com/docs/cli/) for the
+full page, and `carlos logs -app <app> -since 1h` for anything where your app
+started and then misbehaved. If a deploy looks stuck, check
+`X-Carlos-Version` on the live hostname, not the page in your browser —
+[Deploying](/docs/deploying/) explains why the header is the honest answer.
+
+If none of that gets you there, open an issue on
+[the carloku repo](https://github.com/carlosframework/carloku/issues). These
+pages are written and kept there, so a question worth asking is usually a
+page worth fixing, and both happen in the same place.
diff --git a/src/docs/members.md b/src/docs/members.md
new file mode 100644
index 0000000..69c98b1
--- /dev/null
+++ b/src/docs/members.md
@@ -0,0 +1,109 @@
+# Members and service credentials
+
+Two kinds of thing act on your apps. A person is a Keymail address with a
+role in the account. A credential is something a server holds — a CI runner,
+a job, a router — with its reach decided when it was minted and no human
+attached to it. They are administered in different places, and the mistakes
+they invite are different too.
+
+## People
+
+Invites go out from the Team page in the console, addressed to a Keymail
+address. The invitation lands in that person's Keymail Apps inbox rather than
+their email; the platform does not send mail to anyone. It works once and
+expires after seven days, and an owner can revoke a pending one before it is
+used.
+
+There are two roles. A member does the day-to-day work on the account's apps
+— deploy, promote, roll back, config, domains, logs. An owner does all of
+that plus everything that changes who can act or ends something permanently:
+inviting, removing a member, handing ownership over, deleting an app or the
+account, and every credential in the next section. Handing over ownership is
+a single transaction that steps you down to member as it lifts them up, so
+there is no moment with two owners or none.
+
+Some of those actions ask for your passkey again even though you are already
+signed in — removing a member, transferring ownership, deleting the account,
+approving a terminal or revoking one, minting or rotating a credential. The
+gesture has to be within the last five minutes. A session cookie proves who
+you are and not that you are still at the keyboard, which is the whole
+question when the action is irreversible.
+
+None of this has a `carlos` verb. Membership is console work, deliberately:
+the passkey prompt is the point of it, and a terminal has nowhere to put one.
+
+## Credentials a server holds
+
+```sh
+carlos services create -role publish -app hello ci-runner
+```
+
+The secret prints once, right there in your terminal, and only its hash is
+stored. There is no page and no command that will show it to you again — a
+lost secret means rotating, not re-running. Get it into the app the way you
+get any credential in:
+
+```sh
+carlos secrets set -app hello CARLOS_SERVICE_TOKEN=<the secret>
+```
+
+Pick the narrowest role that does the job. `publish` is the deploy loop:
+claim an app, ship, promote, roll back, manage hostnames, retire. `operate`
+is runtime work with no release authority — restart, config, logs. `instance`
+is the older app-token shape, kept for what already depends on it. `admin` is
+the union of those plus ledgers, storage and placement, and it includes
+granting object-store access, which mints a real IAM user. That last one is a
+handover, not a convenience.
+
+`-app hello` binds the credential to one app, which also means it cannot
+claim new ones. Leave it off for an account-wide credential. Either way the
+reach is fixed at mint: if you join another account next month, the
+credential you minted does not follow you there. It draws on its own rate
+budgets rather than yours, too — see [Limits](/docs/limits/) — and it never
+expires, so revoking is the control.
+
+## Rotate replaces, revoke stops
+
+This is the distinction to have straight before you need it.
+
+```sh
+carlos services rotate ci-runner
+```
+
+Rotation mints a fresh secret and prints it once, keeping the name, the role,
+the scope and the history. The old secret is refused on the credential's
+*next* call — work already in flight is unaffected. That makes rotation a
+hygiene move, not an incident response.
+
+```sh
+carlos services revoke ci-runner
+```
+
+Revocation is the hard stop: the credential's next call fails whatever secret
+it is holding. It is idempotent, so revoking a name twice, or one that never
+existed, answers the same way. And unlike minting and rotating, it does not
+ask for your passkey — one step, at whatever hour you find out.
+
+If you suspect a secret has leaked, revoke. Then mint a replacement under a
+new name if the work still needs doing.
+
+To see what exists:
+
+```sh
+carlos services ls
+```
+
+Live credentials only — name, role, scope, when it was minted, when its
+current secret began, when it was last used, and who minted it. A revoked one
+is gone rather than listed as revoked, so this is inventory, not history.
+
+Every verb here is owner-only, and closed to service credentials themselves:
+no credential can mint, rotate or revoke any credential, not even its own.
+A leaked one therefore cannot quietly issue itself a quieter sibling. The
+console's Account settings page carries the same list, the same mint form,
+and rotate and revoke controls per row.
+
+Each verb takes `-account` when you belong to more than one; with a single
+account you can leave it off.
+[carlos services](https://carlosframework.com/docs/cli/#carlos-services) has
+the rest of the flags.