Python, Go and other languages
What to do when your app is not Node: check the visitor's token yourself and call bindings over plain HTTP.
The typed platform object is TypeScript. Apps in Python, Go or anything else can use every binding all the same. There are two pieces to do by hand: check the visitor's token and call the bindings.
Checking the visitor's token
Every visit carries X-Platform-Context, a signed token (a JWT). Check it before you trust anything it says. Working versions are in the sample folders fastapi-verify (Python) and go-verify (Go) under skills/cohostly-app-code/assets/examples/.
The token
Headeralg is ES256, typ is JWT, and kid names the signing key.Signatureheader.payload, written as 64 raw bytes (r then s), not DER. JWT libraries read this correctly. Hand-written code must split the 64 bytes in half.Claimsiss, aud, iat, exp, jti, sub (when the caller has an id) and ctx.issis exactlyPLATFORM_AUTH_URL, with no trailing slash.audis the address the visitor used, in lower case. The platform also sends it in theX-Platform-App-Hostheader. Compare against that. UseX-Forwarded-Hostand thenHostonly if it is absent, taking the first entry of a list.expis 5 minutes afteriat.jtiis the request id.subis the caller's stable id, and is absent for an anonymous visitor.ctxis the context, grouped. A field that is unknown or not allowed is absent. One the app asked for but the person does not share isnullnext to<name>_status: "not_shared".
The keys
The public keys are at <PLATFORM_AUTH_URL>/.well-known/jwks.json, a list of P-256 keys each with a kid. Cache them. When a token names a key you do not have, fetch again, but no more than once every ten seconds.
The checks, in order
- There are three parts, and the header says
alg: "ES256". Refusenoneand every shared-secret algorithm. - The
kidis one of the published keys. - The signature verifies.
exphas not passed andiatis not in the future, each with up to 30 seconds of clock difference allowed.issequalsPLATFORM_AUTH_URL.audequals the address inX-Platform-App-Host.
Any failure is a 401 with one plain sentence. Never echo the token. Log the reason on the server only.
Careful The plain
X-Platform-Caller-Idand similar headers are claims, not proof. Decide nothing from a header you did not check against the token. An anonymous visitor (nocaller.id,caller.kindofanonymous) is a valid answer. Decide per route whether to let them in.
Calling bindings from another language
Each binding publishes an OpenAPI description at <binding address>/openapi.json. The addresses are in the PLATFORM_BINDINGS environment variable. Request the description with your binding token in the X-Platform-Binding-Token header, then generate a client with any OpenAPI generator. The platform checks that its binding descriptions validate and that the Python and Go clients from OpenAPI Generator compile.
A generated client covers the calls and their types. It does not cover what your app must still do itself:
- Check the visitor's token (above).
- Send the visitor's token as
X-Platform-Contexton calls made for them, so a row rule or a "connect as the visitor" connection knows who is asking. - The per-table types of your database. A generated client sees
findManyand a body, not your columns. - Retries, and an
Idempotency-Keyon writes that must not run twice.
Anything the platform calls into your app, such as a scheduled job or an approval decision, is a web hook. Serve it and check the platform's signature (see approvals and the clock).
For AI there is a shortcut: most stock AI client libraries can be pointed at the gateway with AI_BASE_URL as the address and AI_API_KEY as the key. See AI.
What exists in Python today
The clock helper has a Python form:
@platform.cron("0 9 * * 1-5", name="daily-report", time_zone="Asia/Kolkata") def daily_report(run): ...
platform.cron.once(...) and platform.cron.handle(name, body, caller) for a web route also exist. Typed Python files for other bindings are not available for every binding yet. cohostly types --lang python writes them where supported.