Documentation

Alpha

App keys

Generate an RSA key, add your own Ed25519 or P-256 public key, or point Upstream at a key set URL.

The Upstream API is in alpha. Endpoints, fields and limits can change before it's stable. Send feedback to support@coline.app.

An app proves who it is with a JWT signed by one of its keys. Upstream stores only public keys. You choose where the private key lives.

OptionAlgorithmsPrivate key
Generate a keyRS256 with a 2048-bit RSA keyUpstream makes it and you download it once
Add a public keyRS256 with RSA of 2048 bits or more, EdDSA with Ed25519, or ES256 with P-256Stays with you, for example in a KMS or HSM
Key set URLThe same threeStays with you. Upstream reads the public keys from your JWKS.

All three are on your app's Keys and secrets tab.

Generate a key

Under Signing keys, choose Generate key. The private key downloads once, as a PKCS#8 PEM file. Apps made with upstream app create or a manifest start with one generated key.

Add your own public key

Make a key pair and keep the private key:

# Ed25519
openssl genpkey -algorithm ed25519 -out app-key.pem
openssl pkey -in app-key.pem -pubout -out app-key.pub.pem

# P-256
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out app-key.pem
openssl pkey -in app-key.pem -pubout -out app-key.pub.pem

Choose Add public key, paste the contents of app-key.pub.pem, and give it a name if you like. RSA public keys of 2048 bits or more work the same way.

CodeMeans
PRIVATE_KEY_UPLOADEDYou pasted a private key. Upstream refuses it and stores nothing.
PUBLIC_KEY_UNSUPPORTEDThe key is RSA under 2048 bits, or a curve other than Ed25519 or P-256.
PUBLIC_KEY_INVALIDThe text isn't a PEM public key.

The key list shows each key's algorithm, where it came from and its SHA-256 fingerprint.

Sign with your own key

Octokit's App signs only with RSA keys. For Ed25519 or P-256, give @octokit/auth-app a createJwt function:

npm install @octokit/auth-app @octokit/rest jose
import { readFileSync } from "node:fs";
import { createAppAuth } from "@octokit/auth-app";
import { Octokit } from "@octokit/rest";
import { importPKCS8, SignJWT } from "jose";

const key = await importPKCS8(readFileSync("app-key.pem", "utf8"), "EdDSA");

async function createJwt(appId: string | number, timeDifference = 0) {
  const now = Math.floor(Date.now() / 1000) + timeDifference;
  const jwt = await new SignJWT({})
    .setProtectedHeader({ alg: "EdDSA", typ: "JWT" })
    .setIssuer(String(appId))
    .setIssuedAt(now - 30)
    .setExpirationTime(now + 540)
    .sign(key);
  return { jwt, expiresAt: new Date((now + 540) * 1000).toISOString() };
}

const octokit = new Octokit({
  baseUrl: "https://api.upstream.codes/api/v3",
  authStrategy: createAppAuth,
  auth: { appId: process.env.UPSTREAM_APP_ID!, createJwt },
});

const { data: installation } = await octokit.rest.apps.getRepoInstallation({ owner: "acme", repo: "api" });
const { token } = await octokit.auth({ type: "installation", installationId: installation.id });

For P-256, use "ES256" in both places.

Use a key set URL

Host a JWKS at an HTTPS URL and save it as the JWKS URL under Key set URL:

{
  "keys": [
    { "kty": "OKP", "crv": "Ed25519", "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", "kid": "2026-11", "use": "sig" }
  ]
}

Upstream checks your stored keys first, then the key set.

  • It caches the key set for 10 minutes. A JWT with a kid it hasn't seen makes it fetch again, at most once a minute.
  • The key set can hold 20 keys and 64 KB. The URL must answer within 5 seconds.
  • Keys with a use other than sig, and key types Upstream can't use, are skipped.

Choosing a key with kid

kid in the JWT header is optional. It can be a key's fingerprint from the key list, or a kid from your key set. Without it, Upstream tries every active key of the JWT's algorithm.

Rotate a key

  1. Add the new key. Generate it, upload its public half, or publish it in your key set.
  2. Deploy the new private key and start signing with it.
  3. Revoke the old key on the Keys and secrets tab.

JWTs signed with a revoked key stop working at once. Installation tokens already issued keep working until they expire, within the hour.

An app can have 10 active keys. Adding an eleventh returns 409 APP_KEY_LIMIT, and adding one that's already active returns 409 APP_KEY_EXISTS. Revoked keys don't count.

Client secrets

Client secrets are for signing people in, not for the app JWT. A server app can have 5 at once, so you can rotate them the same way.