Sign in with Codegraff
Register your app
Client registration is manual for now: email hi@codegraff.com with your app's name, homepage, and exact redirect URIs (https, or http://localhost for development). You receive a client_id and, for server-side apps, a one-time client_secret. Browser-only and native apps are registered as public clients and use PKCE alone.
What your app gets
sub) and, with the email scope, the user's verified email. Never credits or API keys; repositories and sandboxes stay behind cg_sk_ API keys the user issues deliberately, unless the user approves an opt-in scope on the consent screen (sandboxes, below).Endpoints
Everything is advertised by the discovery document, so most libraries only need the issuer or discovery URL:
curl https://codegraff.com/.well-known/openid-configuration
- Issuer
https://codegraff.com - Authorization
GET https://codegraff.com/oauth/authorize - Token
POST https://codegraff.com/api/oauth/token - UserInfo
GET https://codegraff.com/api/oauth/userinfo - JWKS
https://codegraff.com/.well-known/jwks.json
Supported: response_type=code, scopes openid email profile offline_access (plus the opt-in sandbox and inference), PKCE S256 (required for every client), client authentication via HTTP Basic or client_secret_post, refresh tokens. Not supported in this version: implicit or hybrid flows, dynamic client registration.
The flow
1. Send the user to the authorization endpoint with a PKCE challenge:
https://codegraff.com/oauth/authorize ?response_type=code &client_id=cg_client_... &redirect_uri=https://app.example.com/auth/callback/codegraff &scope=openid%20email &state=<random> &nonce=<random> &code_challenge=<base64url(sha256(code_verifier))> &code_challenge_method=S256
The user signs in to Codegraff if needed (email link or Google), sees a consent screen naming your app the first time, and is redirected back with code and state. Returning users skip the consent screen; pass prompt=consent to force it.
2. Exchange the code server-side:
curl https://codegraff.com/api/oauth/token \ -u "cg_client_...:cg_cs_..." \ -d grant_type=authorization_code \ -d code=<code> \ -d redirect_uri=https://app.example.com/auth/callback/codegraff \ -d code_verifier=<code_verifier>
{
"access_token": "cg_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email",
"id_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..."
}3. Optionally read the profile with the access token:
curl https://codegraff.com/api/oauth/userinfo -H "Authorization: Bearer cg_at_..."
# {"sub":"2","email":"you@example.com","email_verified":true}Tokens and claims
The id_token is a JWT signed with ES256; verify it against the JWKS (kid in the header selects the key). Claims: iss, sub (the user's stable Codegraff account ID), aud (your client_id), exp, iat, auth_time, nonce (echoed from your request), and email plus email_verified when you request the email scope. Access tokens are opaque and last one hour; they work at the UserInfo endpoint and, for the opt-in scopes below, the Codegraff APIs those scopes name. Authorization codes are single-use and expire after ten minutes. Errors use the RFC 6749 shape: {"error":"invalid_grant","error_description":"..."}.
Refresh tokens
Access tokens last an hour. If your app needs to keep calling Codegraff while the user is away, add the offline_access scope to the authorization request; the consent screen says so, and the code exchange then also returns a refresh_token. Redeem it at the token endpoint with the same client authentication:
curl https://codegraff.com/api/oauth/token \ -u "cg_client_...:cg_cs_..." \ -d grant_type=refresh_token \ -d refresh_token=cg_rt_...
{
"access_token": "cg_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email offline_access",
"refresh_token": "cg_rt_..."
}Refresh tokens last 30 days and are single-use: every refresh returns a new one and retires the old, so always store the newest. Presenting a retired token is treated as theft and revokes every token from that sign-in; the user then goes through the (silent) sign-in again. An optional scope parameter may narrow, never widen, the original grant.
Running sandboxes for the user
Two more scopes let your app use Codegraff's sandbox API as the user, billed to the user's Codegraff credits. They are switched on per app (email hi@codegraff.com) and each has its own line on the consent screen:
sandboxcreates sandboxes on the user's account and runs commands and moves files in the ones your app created. Your app cannot see the user's other sandboxes, API keys or credits.inference(needssandbox) lets Codegraff agents inside those sandboxes use the user's model credits: create the sandbox withcodegraff: true.
Send the access token as a bearer to the gateway's sandbox routes. It is accepted there only, never for chat or other routes, and only for the sandboxes your app created:
curl https://gateway.codegraff.com/v1/sandboxes \
-H "Authorization: Bearer cg_at_..." -H "Content-Type: application/json" \
-d '{"provider":"fleet","autoStopMinutes":10,"codegraff":true}'Each user and app pair appears on the user's API keys page as "your app (Sign in with Codegraff)". Revoking it ends your app's sandbox access; signing in to your app again restores it. Refreshing the access token keeps the access, but a revoked key stays revoked until the user signs in again. The reservation, snapshot and Harness-device routes are not available to apps. The routes themselves are in the sandbox docs.
Better Auth example
With Better Auth's generic OAuth plugin, discovery does the rest:
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "codegraff",
discoveryUrl: "https://codegraff.com/.well-known/openid-configuration",
clientId: process.env.CODEGRAFF_OAUTH_CLIENT_ID!,
clientSecret: process.env.CODEGRAFF_OAUTH_CLIENT_SECRET!,
scopes: ["openid", "email", "offline_access"], // offline_access: refresh tokens
pkce: true,
},
],
}),
],
});
// then: authClient.signIn.oauth2({ providerId: "codegraff", callbackURL: "/" })