Tokens and scopes
Give CI and agents a credential narrower than your own account, and cap what it can spend.
Anything that is not you sitting at a terminal should have its own credential.
marina tokens create ci --preset deploy A browser opens once so you can see the scopes and approve them, then the token is printed back to the terminal. It is printed once and never stored, because it is a credential for somewhere else: put it in MARINA_API_TOKEN, which always wins and reads no keychain, and is the path CI and agents should take.
Minting needs a browser on the same machine, so on a server with none, mint it on your laptop and copy it across. That is what you would do with a CI secret anyway. Listing and revoking are dashboard-only, on purpose: a leaked token cannot enumerate or revoke the others.
Presets
| Preset | Can do | Can spend |
|---|---|---|
readonly | See what you run, and the wallet balance. | No |
deploy | Ship code to apps that already exist. | No |
agent | Build, deploy, config vars, databases. | Yes |
developer | What marina login grants: everything above, plus domains and email. | Yes |
Two of them cannot grow your bill at all, whatever the thing holding them does. deploy is the one to reach for in CI: creating anything needs billing:spend, which it does not carry, so it ships new code and buys nothing.
It can still delete. Spending and destroying are different powers, and deploy lacks only the first, so a token holding it can tear down an app or a site.
There is no narrower write scope: vessel:write is create, deploy, scale and delete together, and nothing splits them. So a credential that can ship code can also remove what it ships to, which is the argument for giving it an expiry.
Scopes
A preset is a bundle. --scope names them directly, and the two combine:
marina tokens create narrow --scope vessel:read --scope vessel:write | Scope | Grants |
|---|---|
account:read | Who the credential belongs to. Every preset has it. |
vessel:read | See apps. |
vessel:write | Create, deploy, scale, delete. |
vessel:exec | Run a command inside a running app. |
env:read | Read config vars. |
env:write | Set config vars. |
db:read | See databases. |
db:write | Create and attach. |
db:credentials | Read connection details, including the password. |
site:read / site:write | Static sites. |
domain:read / domain:write | Domains and DNS. |
email:read / email:write | Email plans and mailboxes. |
email:credentials | Read a mailbox password. |
audit:read | The account’s activity log. |
wallet:read | Balance and invoices. |
billing:spend | Spend from the wallet. |
billing:manage | Top up and change payment details. |
Four of these are deliberately awkward to get, and no preset includes any of them. env:read is not implied by env:write, and neither db:credentials nor email:credentials is implied by the write scope beside it, because being able to configure something and being able to read every secret it holds are different powers. vessel:exec is a shell inside your running app, which is every secret at once, so it is its own scope too.
No token can carry the permissions that manage the account itself. Minting another token, inviting a member, or changing the account is done from a signed-in session, so a leaked credential cannot mint a replacement for itself or bring in an accomplice.
Spend caps
marina tokens create agent --preset agent --spend 200 Optional. Your wallet is prepaid and cannot go negative, so it already bounds what any credential can spend.
A cap bounds spend across time as well, and that is the bound that matters for something running unattended: an agent stuck in a loop can drain a wallet you keep topped up for hosting, and --spend 200 stops it at two hundred cedis for that credential’s whole life. Not per month.
Expiry
marina tokens create ci --preset deploy --expires 90 A credential that expires is one you cannot forget about. Without it the token lasts until you revoke it, which you do from the dashboard.
