Add neon claim for temporary projects without an account - #423
Merged
Conversation
andrelandgraf
force-pushed
the
feat/claimable-neon
branch
from
August 16, 2026 16:01
d3f9794 to
372ab0d
Compare
neon claim for temporary projects without an account
andrelandgraf
force-pushed
the
feat/claimable-neon
branch
2 times, most recently
from
August 25, 2026 14:08
72755ec to
20aead7
Compare
Let agents provision temporary projects without account credentials, reuse short-lived tokens across CLI and neon.ts commands, and hand the project to a human through the claim ceremony.
Claim create and list already go through writer; joining denied capabilities with newlines flattened into a missing separator, and claim list was not pinned to the full-width no-box contract.
Accept plus auth or link leaves .neon.claimable in place, so the next command still uses the assertion instead of the new account credential.
An empty array still counts as present, so the headline command printed a blank label row.
andrelandgraf
force-pushed
the
feat/claimable-neon
branch
from
August 25, 2026 15:09
2a7d242 to
7a96ceb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Every path into the Neon CLI starts with a Neon account.
neon authopens a browser,--api-keyand--profilewant a key you already minted. An agent that lands in an empty directory on a machine with no Neon credential has nothing to connect to, and the human is the one who has to go and sign up before any code can run against a database.Claimable Neon (
https://claimable.neon.tech) hands out temporary projects to an anonymous caller and lets a human take ownership later. The CLI had no way to talk to it, so an agent that wanted a database still had to stop and ask.What this adds
neon claim, withneon claimableas an alias, and five subcommands:create,status,accept,list,delete.claim createregisters an anonymous identity with Claimable Neon, gets a project back, saves a signed identity assertion on this machine and links the directory. From there the rest of the CLI works against that project: when a directory carries the claimable marker and no account credential is in play, the CLI exchanges the assertion for a short-lived bearer token and points the API host at the Claimable Neon origin.The identity assertion is the only secret that lands on disk. Access tokens are exchanged per command and expire in minutes.
Creating a project
Output, from the built CLI in this worktree against a local stub of the Claimable Neon endpoints:
The capability names printed here are the CLI's own service names (
data-api,object-storage,ai-gateway), the same vocabulary--service,statusandlistuse, so nobody has to learn the service'sdata_api/storage/ai_gatewayspelling.Denied Capabilitiesis dropped from the table when nothing is denied.Services that a
neon.tsin scope declares are added to the request automatically. Services the platform cannot grant before a claim are still sent, so demand is recorded, and they come back denied with a reason rather than being silently dropped from the request.Three things land on disk:
The dotenv target follows the existing rule: an existing
.envif there is one, otherwise.env.local, and--fileoverrides. The file is added to.gitignorethe same wayneon env pulldoes it..env.localfrom the run above:createrefuses to run in a directory that is already linked:Using, claiming and cleaning up
status,acceptanddeletetake an optional project id, so a project stays manageable after the directory it was created in is gone:Real output for each, same stub service:
acceptopens the verification URL in a browser by default.--no-openprints it instead, and CI never opens a browser.listis a full-width writer table likeneon projects list, with no box drawing:stateisexpiredwhen either clock has run out, the identity assertion's or the project's. Both are read locally, solistnever has to reach the service:An empty list prints a sentence:
No Claimable Neon projects are saved on this machine.deletehas two outcomes.deletedmeans the project was removed from the service.clearedmeans the assertion had expired or the service no longer accepts it, so only the local record and the.neonlink were dropped. Either way the directory is unlinked afterwards.Once a human completes the claim,
statusseesreconciled, deletes the local assertion and leaves the.neonproject link in place, with a note that the next command needsneon authorneon link. That is a one-way step on this machine: the anonymous identity is gone and the project now belongs to an account.What decides which credential is used
An account credential always wins. The claimable assertion is used only when
--api-key,--profile,NEON_API_KEYandNEON_PROFILEare all absent. When one of them is set in a claimable directory, the command runs against the account and warns:The
claimsubcommands themselves reject--api-keyand--profilebefore any network call, because there is no account credential involved in registering or claiming. AmbientNEON_API_KEYandNEON_PROFILEare tolerated there, so an agent on a machine that already has a working Neon login is never told to unset it.Nothing changes for anyone who is not in a claimable directory.
ensureAuthreaches the new branch only after the existing checks, and only when the.neonfile carries the marker.Error handling and rollback
Claimable Neon returns errors as
{ "error": { "code", "message", "request_id" } }, one level deeper than the Neon API's flat body. Three places learned to read that shape:codeFromBodyandmessageFromBodyin the CLI, so a proxied error keeps its code.wrapNeonErrorin@neon/config, which also now recognizes aNeonApiErrordirectly rather than only an axios-shapedresponse.data.capability_requires_claimmaps toFeatureUnavailablewith the claim path spelled out, instead of the generic 403 that tells you to check your API key.Every response from the service is validated at the boundary and projected to known fields. A malformed body raises
Claimable Neon returned an invalid response while <action>. The response was not used.rather than being half-consumed. An origin has to be a scheme and a host with nothing else on it, and HTTPS unless the host islocalhost.createis transactional. If anything after registration fails, it deletes the project it just created and restores.neonand the dotenv file to what they were. If that remote delete also fails, it says so and names the retry:neon claim delete --yes.Also in here
declaredNeonServicesis pulled out ofcommands/config.tsintoconfig_services.ts, soclaim createandconfig planagree on which services aneon.tsdeclares.config planoutput is unchanged; the toggle logic moved as-is.claimis exempt from.neoncontext enrichment, soclaim listenumerates the machine rather than the current directory.list_tables.test.tsgains a case pinningclaim listto the full-width columns.neonminor,@neon/configpatch.Verification
packages/cliwas built withpnpm exec tsdownfirst, because the CLI-level tests and the output above spawndist/cli.js. Then:Behaviours covered:
--servicevalue maps to the service's capability name.neon.tsdiscovery walks up to the repository root and stops there.claimablemarker throws instead of falling back to account auth.claim status,claim deleteandclaim accepthandle an expired assertion without contacting the service, spawning the real built binary against an origin with nothing listening on it.claim createfails before the network when--api-keyor--profileis passed, and does not warn when only ambient credentials are set.claim listprints every column at full width, marks a past project expiry and a past assertion expiry as expired and prints a message when empty./v1/projects/{id}/credentials,/v1/projects/{id}/claimandDELETE /v1/projects/{id}, asserted against a real HTTP server.wrapNeonErrormaps the nested capability error from three shapes: an axios-like body, aNeonApiErrorand aNeonApiErrorstuffed intoresponse.data.The command output shown above was produced by running
packages/cli/dist/cli.jsin this worktree against a local HTTP stub of the Claimable Neon endpoints. The flow, formatting and files written are the CLI's; the project id, URLs and timestamps come from the stub.Not run:
packages/cli/e2e/claim.e2e.test.tsagainst the live service. The e2e suite's setup file calls the harness orphan sweep, which needsNEON_API_KEY. That test covers the part no local run can: create against the real service,neon projects getthrough the exchanged token,claim statusand delete by project id from a directory that never had the.neon. It needs one green run before merge.For your attention
claimable/api.ts, and every one of them is a guess about the service until the e2e run happens. A field rename on the service side surfaces asClaimable Neon returned an invalid response, which is the right failure but still a failure.--claimable-hostis hidden, andCLAIMABLE_NEON_HOSToverrides the default origin. Both exist for pointing at a local service during development. Non-localhost origins are forced to HTTPS..neonby hand strands the assertion file.claim liststill shows the project andclaim delete <project-id> --yesstill removes it, so the recovery path exists, but nothing sweeps the config directory on its own.statusdropping the assertion after a claim is irreversible on that machine. If the human claims the project and the agent still needs it, the way back isneon authorneon linkagainst the now-owned project.createwrites the dotenv file before the table prints. On a rollback the previous contents are restored byte for byte, and the file is removed if it did not exist, but an editor holding the old buffer will not know.