Skip to content

feat: add Claimable Neon page, docs and auth.md agent discovery - #5507

Merged
andrelandgraf merged 52 commits into
mainfrom
feat/claimable-postgres-page
Aug 25, 2026
Merged

feat: add Claimable Neon page, docs and auth.md agent discovery#5507
andrelandgraf merged 52 commits into
mainfrom
feat/claimable-postgres-page

Conversation

@andrelandgraf

@andrelandgraf andrelandgraf commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The job

neon.com is where both humans and agents arrive first, and neither had a path to a Claimable Neon project from here. A human had no page on neon.com that provisions one. An agent had no document on neon.com that describes the protocol, so the only discoverable entry was the retired claimable-postgres agent skill, which described npx neon-new and neon.new rather than the current identity and claim flow.

This branch makes neon.com the entry for both:

  • /claimable-neon provisions a temporary project from the browser, backed by a server route.
  • /auth.md is the protocol document an agent fetches, linked as the first Common Queries entry in docs/llms.txt.
  • /.well-known/oauth-authorization-server/claimable is the RFC 8414 metadata that points an agent at the token, revoke and JWKS endpoints.

claimable.neon.tech remains the service. neon.com only discovers and proxies.

Why the issuer is a path identifier

Publishing this metadata at https://neon.com/.well-known/oauth-authorization-server would make neon.com itself the authorization server for every agent that probes the apex. RFC 8414 allows an issuer with a path component, and the metadata for such an issuer is served with that path inserted after .well-known. So the issuer is https://neon.com/claimable, the document is at /.well-known/oauth-authorization-server/claimable, and neon.com's apex well-known path stays unused.

The protocol document has to live on the issuer host for that to hold, which is why auth.md sits at https://neon.com/auth.md and not on claimable.neon.tech. scripts/verify-agent-endpoints.mjs asserts both: that agent_auth.skill shares the issuer's origin, and that the issuer path is non-empty.

The page

/claimable-neon renders three sections.

The hero holds the provisioning form. Lakebase Postgres is always included and shown as a locked row. Data API and Managed Better Auth are toggles, each labelled "Stays enabled after claim." Below the button: "No account or payment details required. Unclaimed projects expire in 72 hours and are capped at 100 MB storage and 1 GB transfer."

On success the form is replaced in place by a result card that focuses its own heading and announces itself through role="status". It shows the project id, one pill per capability decision with denied ones spelled out underneath, and copy buttons for DATABASE_URL, NEON_DATA_API_URL, NEON_AUTH_BASE_URL and the claim link. The claim link is copyable as well as openable, because opening it in a new tab is not the only way a person needs it. Two buttons follow: "Open the claim link" and "Create another project", the second of which resets the form.

The card states what claiming costs, keyed to what was actually granted rather than to a fixed sentence:

Copy these values now. This page will not show them again. The claim link expires at {time}. If it expires, create another project from this page. Opening the claim link does not freeze access. Continuing to Neon on the claim page rotates DATABASE_URL. After the transfer finishes, pull a new one from the Neon console. Managed Better Auth and the Data API stay enabled. The project itself expires on {time}.

The middle section shows the same service through auth.md, the Neon CLI and neon.ts, each with a copyable sample. The CLI card carries the fallback in its own description: if neon claim is not a command, use the auth.md card.

POST /api/claimable-neon

The page calls one route. The request body is the optional service list; anything outside auth and data-api is refused before a single upstream call.

POST /api/claimable-neon
Content-Type: application/json

{"services": ["auth", "data-api"]}
{
  "project": {
    "id": "quiet-fog-12345678",
    "branch_id": "br-...",
    "expires_at": "2026-08-14T12:00:00.000Z"
  },
  "capabilities": [
    { "capability": "postgres", "granted": true },
    { "capability": "data_api", "granted": true },
    { "capability": "auth", "granted": true }
  ],
  "credentials": {
    "project_id": "quiet-fog-12345678",
    "branch_id": "br-...",
    "database_url": "postgresql://...",
    "services": {
      "data_api": { "url": "https://....../rest/v1" },
      "auth": { "base_url": "https://....../api/auth" }
    }
  },
  "claim": {
    "verification_uri_complete": "https://claimable.neon.tech/claim?user_code=ABCD-2345",
    "expires_in": 900
  }
}

Status is 201, and every response carries Cache-Control: no-store.

The route makes four upstream calls in order: register an anonymous identity with postgres plus the requested services and source: "neon_website", exchange the returned assertion for an access token, read credentials, create a claim code. The identity assertion is used server-side and is not part of the response, so the browser holds the project credentials and the claim link and nothing durable. The CLI path is where an assertion gets persisted.

Every upstream payload is shape-checked before it is used. The claim link is additionally checked against the configured origin, so a redirected or substituted verification_uri_complete never reaches the page as a button.

If anything fails after registration succeeded, the route deletes the project it just created, minting a token first if the failure happened before it had one.

Status error.code When
400 invalid_request Content-Type is not application/json, the body is not JSON, or services contains anything other than auth and data-api
500 invalid_configuration CLAIMABLE_NEON_ORIGIN is not a bare http(s) origin
502 claimable_service_unavailable the request to Claimable Neon threw
502 invalid_claimable_response a non-JSON body, a registration, credentials or claim payload that fails its shape check, or a claim link off the configured origin
500 claimable_cleanup_failed setup failed and the follow-up delete also failed; the message says the project will expire on its own
upstream upstream code Claimable Neon returned a structured error; status and body are passed through unchanged, so rate_limit_exceeded and its retryable flag survive to the page

CLAIMABLE_NEON_ORIGIN is new in .env.example and defaults to https://claimable.neon.tech. It must be an origin with no path, query or fragment; a value like https://claimable.example/v1 fails the request with invalid_configuration rather than being silently trimmed.

What an agent fetches

docs/llms.txt gains a first Common Queries entry, "Need a Neon account and the user is not around: provision one now; they claim it later", pointing at https://neon.com/auth.md.

auth.md puts the CLI first and the HTTP flow second. It opens with an account check, so an agent that already has credentials does not end up on this path:

If NEON_API_KEY is set, or neon profile list -o json lists a profile whose account is not -, use that account and stop. A DEFAULT row with account: "-" and file: "missing" is not an account.

Then the CLI, with the fallback stated rather than assumed:

npm i -g neon@latest
neon skills -s neon -s neon-postgres

neon claim create --service data-api --service auth --env-pull
neon branches list

The HTTP sections document registration, the JWT bearer exchange, credentials, the claim ceremony and revocation. Two details are called out because they are the ones a client gets wrong: the metadata's claim_endpoint takes {"claim_token": "<identity_assertion>"} while the project-scoped POST /v1/projects/<id>/claim uses the access token, and both create the same claim code; and continuing to Neon revokes existing access tokens, so the assertion has to be re-exchanged for a token that authorizes claim-status polling only.

claimable.neon.tech/auth.md is named as the authority for field names, so this copy cannot drift into being wrong about the wire format.

Issuer metadata

{
  "issuer": "https://neon.com/claimable",
  "token_endpoint": "https://claimable.neon.tech/v1/oauth2/token",
  "revocation_endpoint": "https://claimable.neon.tech/v1/oauth2/revoke",
  "jwks_uri": "https://claimable.neon.tech/.well-known/jwks.json",
  "grant_types_supported": ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
  "token_endpoint_auth_methods_supported": ["none"],
  "response_types_supported": [],
  "agent_auth": {
    "skill": "https://neon.com/auth.md",
    "identity_endpoint": "https://claimable.neon.tech/v1/agent/identity",
    "claim_endpoint": "https://claimable.neon.tech/v1/agent/identity/claim",
    "identity_types_supported": ["anonymous"]
  }
}

The values come from a CLAIMABLE block in src/constants/agent-discovery.js, which is where every other agent-discovery surface already keeps its facts. The route handler is a one-line wrapper around the builder, and config/agent-endpoints.yaml registers the endpoint so the existing verifier picks it up. The new claimable-authorization-server validator checks the RFC 8414 shape, compares issuer, token endpoint, JWKS, skill and identity endpoint against the constants, requires none in token_endpoint_auth_methods_supported, and reads public/auth.md to confirm it contains all five URLs. public/auth.md is added to the workflow's path filter so editing the document reruns that check.

CLI reference

content/docs/cli/claim.md documents neon claim, with neon claimable as an alias, in the setup group of the CLI index.

neon claim create --env-pull
neon claim create --service data-api --service auth --env-pull
neon claim status
neon claim accept --no-open
neon claim list
neon claim delete --yes

The page states what the generated schema cannot: --api-key and --profile are refused, --env-pull is on by default and writes to an existing .env or otherwise .env.local, neon.ts supplies the service list when it is present, and opening the accept URL does not freeze access while continuing to Neon rotates DATABASE_URL. scripts/docs-checks/neonctl/overrides.json describes the repeatable --service flag.

Naming and redirects

Claimable Postgres becomes Claimable Neon across docs, and the reference page moves.

From To Kind
/claimable-postgres /claimable-neon permanent
/claimable /claimable-neon temporary
/launchpad, /instagres /claimable-neon temporary, previously https://neon.new
/docs/reference/claimable-postgres, .md /docs/reference/claimable-neon permanent
/docs/ai/skills/claimable-postgres/* and the four .well-known skill paths /auth.md permanent

The retired skill's URLs go to auth.md rather than to the docs page, because whatever followed those URLs was an agent looking for a protocol.

The claimable-postgres skill is gone from config/skills.json, both .well-known skill indexes, the AI catalog and the static markdown manifest. Its content is now split between public/auth.md and a reference file under the neon skill.

The neon skill is pinned to 8a4200b2fdf8c4a5b301c717064d8aeb58c8eea5 instead of tracking main, because this branch depends on the version of that skill which carries references/claimable-neon.md and the "Starting without a Neon account" section. That section gates the path twice: the account check first, then an explicit ask before using Claimable Neon at all, since a temporary project is a workaround rather than the normal way to get a Neon project.

Also in here

  • neon-postgres-agent-platforms still tracks main, so re-running the skill sync pulled its upstream "SDK vs agent tools" content into public/docs/ai/skills/. Unrelated to Claimable Neon, and it is what the sync produces on this ref.
  • The llms.txt tagline in src/scripts/llms-index-config.js was rewritten to the current product wording (Lakebase Postgres, Managed Better Auth) while the Common Queries entry was added to the same file.
  • generate-skills-index.test.js used claimable-postgres as its second fixture skill. It now uses neon-functions, so the test no longer depends on a skill that is not installed.
  • content/docs/guides/tanstack-start.md loses a five-tab npx neon-new block in favour of one sentence, and content/docs/guides/platform-integration-overview.md now distinguishes the two flows: neon.new keeps the connection string, Claimable Neon rotates DATABASE_URL.
  • One formatting-only change in scripts/verify-agent-endpoints.mjs inside the existing models.dev branch, from the formatter.
  • Rebased onto current main by merge. neon plugins (landed on main) and neon claim both stay in the CLI nav and the 4.4.0 schema.

Verification

At 3119597:

  • npx vitest run passes, 778 tests across 46 files.
  • npm run verify:agent-endpoints passes, 9/9 endpoints and 44/44 checks, including all 14 checks of the new claimable-authorization-server entry.
  • npx eslint clean on the route, the page, the component, the metadata handler and the constants.

The 12 route tests in the diff cover, as behaviours:

  • the happy path, asserting the four upstream calls in order and that services: ["auth", "data-api"] becomes capabilities: ["postgres", "data_api", "auth"]
  • an unknown service and a non-JSON content type, both rejected with zero upstream calls
  • a registration whose capability entries are unusable, rejected as a contract violation, and the project deleted
  • a claim link on a foreign origin, and a claim expires_in of -1, both rejected and the project deleted
  • a structured upstream error passed through with its status and body intact
  • a transport failure surfaced as claimable_service_unavailable
  • a credentials call that 503s, with the project deleted afterwards
  • CLAIMABLE_NEON_ORIGIN set to an origin with a path, and to a bare hostname, both refused before any call

The page was driven in a browser against live Claimable Neon: idle form, create (postgres only, and Data API plus Auth), result card with Copy Claim link (accessible name becomes Copied), explainer copy that opening the claim link does not freeze access, and Create another project returning the idle form. Redirects are declarative in next.config.js and were not exercised against a running server.

For your attention

  • The page is not in the site navigation. /claimable-neon is reachable through the redirects, the docs pages and links.claimableNeon, but nothing in the header or footer menus points at it. That is deliberate for now; adding it is a separate decision.
  • /launchpad and /instagres no longer leave neon.com. They were temporary redirects to https://neon.new and now land on /claimable-neon. Anyone who bookmarked them expecting neon.new gets a different product surface with a different claim behaviour.
  • Claiming rotates DATABASE_URL; neon.new did not. Every surface in this branch says so, and the platform integration guide now contrasts the two explicitly, but it is a real behavioural difference for partners who documented the old flow.
  • The route trusts Claimable Neon's expiry values for display. expires_at and expires_in are rendered as local times without a server-side sanity check beyond "parses" and "positive". A wrong clock upstream shows a wrong deadline.
  • claimable_cleanup_failed returns 500 and leaves the project behind. The response says it will expire on its own, which is true, but a run of these leaves unclaimed projects that nobody asked for. The failure is logged with the project id.
  • The 72 hour window, 100 MB storage and 1 GB transfer caps are hard-coded in page copy. The registration response carries the real expires_at and the result card uses it, but the pre-provisioning line under the button does not, so a change to the service defaults needs a change here.
  • The neon skill pin will go stale. It is there so this branch does not depend on an unmerged upstream state. It should go back to main once the pinned commit is the tip.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neon-next Ready Ready Preview Aug 25, 2026 10:12pm

Request Review

@andrelandgraf
andrelandgraf force-pushed the feat/claimable-postgres-page branch from 7fd5cb9 to a53288d Compare August 16, 2026 16:02
@andrelandgraf
andrelandgraf marked this pull request as ready for review August 16, 2026 16:02
@andrelandgraf andrelandgraf changed the title Add Claimable Postgres agent flow Add Claimable Postgres page and hop llms.txt to auth.md Aug 16, 2026
@andrelandgraf andrelandgraf changed the title Add Claimable Postgres page and hop llms.txt to auth.md Add Claimable Postgres page and llms.txt hop to auth.md Aug 16, 2026
@andrelandgraf andrelandgraf changed the title Add Claimable Postgres page and llms.txt hop to auth.md Add Claimable Postgres page and hop agents to auth.md Aug 16, 2026
Keep both neon plugins (from main) and neon claim (this branch) in the CLI nav and schema.
@andrelandgraf
andrelandgraf merged commit 9db6a1d into main Aug 25, 2026
5 checks passed
@andrelandgraf
andrelandgraf deleted the feat/claimable-postgres-page branch August 25, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants