# Connect

> Let your app or tool set up ExtraOrbital resources for its users and receive their env vars, with the user's approval.

<Callout title="In brief">

- Send your user to `https://extraorbital.dev/connect` with the resources your app needs.
- They sign in, pick a team and project, see every env var name and price, and approve.
- Your server exchanges a one-time code and its PKCE verifier for the env vars, once.
- No app registration and no client secret. Users revoke an app under **Team settings → Connected apps**.

</Callout>

Connect is for apps and developer tools that need resources on their user's account: a component kit whose components read `MONGODB_URI`, a starter that needs a bucket, a hosted builder. It follows the shape of OpenRouter's PKCE flow: your app never holds an ExtraOrbital token, only the values it asked for.

## The request

```text
https://extraorbital.dev/connect
  ?app_name=Acme
  &resources=mongo,s3/uploads
  &redirect_uri=https://acme.dev/api/extraorbital/callback
  &code_challenge=<base64url(sha256(verifier))>
  &code_challenge_method=S256
  &state=<random>
```

| Parameter | |
| --- | --- |
| `app_name` | Shown to the user, marked unverified, beside the host of `redirect_uri`. |
| `resources` | Comma-separated `service` or `service/name`, at most 8. Names default to `default`. Two resources that would set the same env var are refused. |
| `redirect_uri` | `https://…`, or `http://localhost:<any port>/…` for a local tool. No fragment. |
| `code_challenge` | Required with `redirect_uri`. S256 only. |
| `state` | Returned untouched. Check it. |
| `project` | Optional: the id a new project gets. Defaults to your app name. |
| `ref` | Optional: an [affiliate](/docs/affiliates) slug. |

The user lands back on `redirect_uri?code=eoc_…&state=…`, or `?error=access_denied&state=…` if they deny. The code works once, for 10 minutes.

## The exchange

```http
POST https://extraorbital.dev/api/v1/connect/token
Content-Type: application/json

{ "code": "eoc_…", "code_verifier": "…", "redirect_uri": "https://acme.dev/api/extraorbital/callback" }
```

```json
{
  "grant_id": "cg_…",
  "team_id": "team_…",
  "project_id": "acme",
  "env": { "MONGODB_URI": "mongodb+srv://…", "S3_BUCKET": "…" },
  "resources": [{ "resource": "mongo/default", "env": ["MONGODB_URI"], "shared": false }]
}
```

Call it from your server: the response has no CORS headers, so a browser page cannot read it. Errors are OAuth-shaped, `{ "error": "invalid_grant", "error_description": "…" }`. Store the values the way you store any secret, or write them to your user's hosting provider with its API.

Each resource gets a credential of its own for your app, which ExtraOrbital does not keep a copy of. `shared: true` means the provider could only hand out the resource's shared credential.

## Next.js example

```ts
// app/api/extraorbital/start/route.ts
import { createHash, randomBytes } from "node:crypto";
import { cookies } from "next/headers";

export async function GET() {
  const verifier = randomBytes(32).toString("base64url");
  const state = randomBytes(16).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  const jar = await cookies();
  const options = { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600, path: "/api/extraorbital" } as const;
  jar.set("eo_verifier", verifier, options);
  jar.set("eo_state", state, options);

  const url = new URL("https://extraorbital.dev/connect");
  url.searchParams.set("app_name", "Acme");
  url.searchParams.set("resources", "mongo,s3/uploads");
  url.searchParams.set("redirect_uri", `${process.env.APP_URL}/api/extraorbital/callback`);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set("code_challenge_method", "S256");
  url.searchParams.set("state", state);
  return Response.redirect(url, 302);
}
```

```ts
// app/api/extraorbital/callback/route.ts
import { cookies } from "next/headers";

export async function GET(request: Request) {
  const url = new URL(request.url);
  const jar = await cookies();
  const verifier = jar.get("eo_verifier")?.value;
  if (!verifier || url.searchParams.get("state") !== jar.get("eo_state")?.value) {
    return new Response("State mismatch", { status: 400 });
  }
  jar.delete("eo_verifier");
  jar.delete("eo_state");
  if (url.searchParams.get("error")) return Response.redirect(new URL("/?connect=denied", url), 302);

  const response = await fetch("https://extraorbital.dev/api/v1/connect/token", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      code: url.searchParams.get("code"),
      code_verifier: verifier,
      redirect_uri: `${process.env.APP_URL}/api/extraorbital/callback`,
    }),
  });
  if (!response.ok) return new Response("Could not connect ExtraOrbital", { status: 502 });
  const { env, project_id } = (await response.json()) as { env: Record<string, string>; project_id: string };
  await saveSecretsForCurrentUser(project_id, env); // your storage
  return Response.redirect(new URL("/?connect=done", url), 302);
}
```

## An "Add to ExtraOrbital" button

Without `redirect_uri`, nothing is sent to anyone: the user approves, the resources are created, and the last screen shows the `npx extraorbital add …` commands that write them to `.env` in their project folder. That suits a README:

```md
[Add to ExtraOrbital](https://extraorbital.dev/connect?app_name=my-starter&resources=mongo,s3&ref=my-starter)
```

## What the user controls

- The consent screen names your app (unverified) and where the values go, the team and project, every resource with its env var names and price. Resources go to a new project named after your app unless the user picks an existing one.
- Resources are billed to the user's team like any other.
- **Team settings → Connected apps** lists every app with its project and env var names, and revokes it: your credentials stop working, and the resources stay with the user.
- Every approval, exchange and revocation is in the team's activity log, without values.

---

More for agents: [Sitemap](https://extraorbital.dev/sitemap.md) · [llms.txt](https://extraorbital.dev/llms.txt) · [agents.md](https://extraorbital.dev/agents.md)
