Skip to main content

Story: deploy an API from the CLI, end to end

This is the full life of a small API: you write one file, deploy it, publish an address, decide who may call it and how fast, and finally put your own domain on it. Everything happens in one shell with platformctl. Budget about 20 minutes.

The CLI is the whole control plane here. Nothing in this story is clicked.

The shape of the thing

Before you begin

  • A platform account and a project — see Create an account.
  • platformctl installed and signed in — see Install the CLI.
  • Your project connected to Crusoe Cloud. Everything you deploy is built into your own registry; a project with no credential is refused before anything is built. Check with platformctl crusoe-cloud show. If it says not connected, see connect your Crusoe Cloud account.

Step 1: the function, in one file

An API here is a function: one file that receives the parsed JSON body and returns a dict.

mkdir usage-api && cat > usage-api/handler.py <<'EOF'
import time

def handle(event):
# Whatever the caller sent, tell them what arrived and when.
return {
"received": event,
"at": int(time.time()),
}
EOF

Deploy it:

platformctl functions deploy ./usage-api --name usage-api

You should see the build move through building → deploying → ready, ending with:

usage-api is ready at https://usage-api-ab12cd.apps.codyhill.dev

That address is the workload's own. It answers to you — you are signed in, and the control plane knows it. It is not yet a public API, because nothing here is public until you publish it.

Call it once, through the control plane:

platformctl invoke usage-api '{"hello": "world"}'
{"received":{"hello":"world"},"at":1771234567}

Step 2: publish it

platformctl gateway publish function/usage-api --name usage --auth apikey

One command: the argument is <type>/<resource>, the endpoint is named usage, and --auth apikey means callers must send a key you issue. Because an endpoint that checks keys but holds none cannot serve, the publish refuses to leave you unprotected — it issues the first key and prints it. Copy it now: keys are shown exactly once.

You should see:

url: https://usage-ab12cd.apps.codyhill.dev
...
Anyone presenting one of this endpoint's API keys can call this.

Test the door both ways:

# No key — refused at the edge, your function never runs
curl -s -X POST "https://usage-ab12cd.apps.codyhill.dev" -d '{}'

# With the key — reaches the function
curl -s -X POST "https://usage-ab12cd.apps.codyhill.dev" \
-H "X-API-Key: <the-key>" -H "Content-Type: application/json" \
-d '{"hello": "world"}'

The refusal happens before your code. A rejected caller costs you nothing and never appears in your function's logs.

If you lost the key — you will, everyone does — issue another:

platformctl gateway key issue usage

Keys are per-endpoint, revocable one at a time. When a laptop leaves the company, revoke its key; the other callers keep working.

Step 3: slow a caller down

A partner script in a retry loop can turn one bug into your whole capacity. Cap it:

platformctl gateway endpoint update usage --rate-limit 100/minute

Past the limit, the caller gets a refusal at the door — again, before your function. The limit is per caller, so one noisy integration does not starve the rest. See rate limits.

Step 4: close it to the networks you trust

If the API is for one office or one partner, say so:

platformctl gateway endpoint update usage --allow-cidr 203.0.113.0/24

Repeat the flag to allow several ranges. A caller from anywhere else is refused with no key check consulted — the address-range check runs at the edge, before anything of yours is asked. See address allow lists.

Stack the two protections and the checks are both cheap and both early: wrong network? refused. Right network, no key? refused. Right network, right key, too fast? refused. Only the fourth caller reaches your function.

Step 5: put your own name on it

The default address works but reads like a machine name. Claim a domain you own, then publish on it:

platformctl gateway domain claim api.example.com
# the command prints a DNS record to create — one TXT for proof, one CNAME for traffic
platformctl gateway publish function/usage-api --name usage --auth apikey --domain api.example.com

The address is chosen when the endpoint is published, so moving to your own name is a re-publish rather than an edit. The claim prints the DNS records to create; once the proof resolves, the endpoint answers on https://api.example.com. The mechanics — order of operations, the failure states, unclaiming — are on custom domains.

Step 6: change the code, keep the address

Edit handler.py — return something different — and redeploy:

platformctl functions deploy ./usage-api --name usage-api

A new revision appears: a frozen snapshot of code and settings. Traffic keeps flowing to the old one until the new one is ready, and rollback is selecting the older revision, not redeploying. The published endpoint never moves — it points at the function, not at a revision. See traffic and revisions.

What you just used, and where to read more

You usedThe page that documents it
Functions and the handler contractFunctions overview, HTTP and events
Publishing and keysPublish an endpoint, Authentication
Rate limitsRate limits
Address allow listsAddress allow lists
Custom domainsCustom domains
Revisions and rollbackTraffic and revisions

Next: stop calling the API yourself. Build a real-time chat monitor wires a topic to a function so the platform calls it for you.