Skip to content
← BlogDeveloper

norbelys login: API key vs. browser OAuth, and where your credentials live

The CLI's two auth modes — org-scoped API keys and PKCE browser OAuth — when to use which, and why config.json's 0600 file permission actually matters.

By Gabriel Lara, Developer Relations, Norbelys

Founder-reviewed ·How we research and correct articles

Every norbelys command needs the same thing the raw API needs: a bearer credential sent as Authorization: Bearer <token>. What differs is how that token gets onto your machine in the first place, and the CLI supports two distinct ways of answering that question. If you skipped straight here without a CLI installed yet, start with the install-and-first-command guide and come back.

The two modes

An API key, passed directly:

Terminal
$ norbelys login --api-key ak_...Logged in to Acme Inc (org_8h2k...)

This is a static, org-scoped secret you generate from the dashboard and paste in — or, for automation, hand to the CLI through an environment variable instead of a flag ($NORBELYS_API_KEY, covered below). It works today, for every kind of caller: a human on a laptop, a CI runner, a cron job on a server nobody ever looks at interactively.

Browser OAuth, using the Authorization Code flow with PKCE and a loopback redirect — the pattern RFC 8252 defines specifically for native apps and CLIs that can’t safely embed a client secret in a binary anyone can download and disassemble. The idea: the CLI spins up a tiny local HTTP server on 127.0.0.1, opens your default browser to Clerk’s consent screen, you approve the request in the browser you’re already logged into, and the redirect lands back on that local server carrying an authorization code — no secret ever leaves your machine, and PKCE stops anyone else on the box from intercepting the code and trading it for a token themselves.

When to use which

The honest answer, right now, is that --api-key is the credential every caller uses, because it’s the one that’s live. But it’s worth understanding the intended split, because it explains why both exist and which one to reach for once browser login is switched on.

A human at an interactive terminal is the case OAuth is built for. You already have a browser session logged into Norbelys; clicking “approve” there is less friction than generating a key in the dashboard, copying it, and pasting it into a terminal — and it sidesteps a long-lived static secret sitting in a config file for something you’re only doing to poke around for ten minutes.

A CI pipeline should always use an API key, full stop, both before and after OAuth ships. A GitHub Actions runner has no browser to redirect to and no human present to click “approve” — the loopback flow assumes an interactive session, which a headless runner by definition doesn’t have. The CI walkthrough covers wiring $NORBELYS_API_KEY into a real pipeline in detail.

A script or scheduled job on a server falls in the same bucket as CI: no browser, no human, so it needs a static credential it can read from an environment variable on every run. This is also true for scripts you run locally but want to be deterministic and non-interactive — a nightly cron entry shouldn’t depend on a browser tab popping up while you’re asleep.

The resolution order backs this up: the CLI checks for a bearer token in this priority — an environment variable first, then whatever’s saved in your active profile, refreshing an OAuth token if it’s the kind that expires. That ordering exists specifically so a script can override whatever’s persisted on disk without touching it, by exporting NORBELYS_API_KEY for just that one invocation.

Where the credential actually lives

norbelys login — either mode — writes the result to ~/.config/norbelys/config.json, one profile per org. It’s a plain JSON file; nothing exotic, nothing encrypted at rest beyond your operating system’s own disk protections.

Terminal
$ ls -l ~/.config/norbelys/config.json-rw------- 1 you staff 312 Jul 19 09:14 config.json

That -rw------- is mode 0600, and the CLI sets it deliberately — read and write for the file’s owner, nothing at all for anyone else on the machine. It’s the same permission pattern you’d expect from ~/.ssh/ private keys, an AWS credentials file, or a GitHub CLI token: any of those files being world- or group-readable turns “I have a shared machine” or “a misconfigured backup job” into “anyone with local access can read every credential I have.”

Why the permission actually matters

It’s easy to skim past a file permission as boilerplate, but think through what’s sitting in that file: an API key that can create and delete data in your org, or a token that can be exchanged for one. On a single-user laptop the practical risk is low. It stops being low the moment you’re on a shared build server, a container image someone else can shell into, a machine with multiple local accounts, or a CI runner where an unrelated build step — your own dependency’s postinstall script, say — happens to run as the same user and go looking through $HOME.

The 0600 default also explains why $NORBELYS_API_KEY is the right choice for automation rather than making the CI runner call norbelys login itself: a build environment often persists less predictably than a laptop home directory, and a secret manager injecting an environment variable for the duration of one job is a narrower exposure window than a file sitting on disk between runs.

Logging out

norbelys logout removes the active profile’s saved credential. If you generated an API key specifically for a short-lived task — testing something on a machine that isn’t yours, for instance — log out when you’re done, and revoke the key from the dashboard rather than trusting yourself to remember it’s still valid six months later.

What this looks like once OAuth is live

Once browser login is switched on, the day-to-day experience for a human on their own laptop should feel closer to logging into any other SaaS tool from a CLI — norbelys login, a browser tab opens, you approve, the tab closes, and the terminal picks up a short-lived token it refreshes on your behalf rather than a static key that’s valid until someone manually revokes it. That’s a meaningfully better security posture for interactive use: a token that expires and refreshes limits how long a leaked credential stays useful, in a way a long-lived API key sitting in a config file doesn’t. It doesn’t change anything about how CI or scripts should authenticate — that answer stays --api-key and $NORBELYS_API_KEY either way, for the reasons above.

One profile, one org — worth knowing before you scale up

Everything in this post assumes a single org. The moment you’re managing more than one — an agency with several client workspaces is the common case — the same config.json starts holding multiple profiles side by side, and a new failure mode becomes possible: running a command against the right credential but the wrong org. That’s common enough, and specific enough, to deserve its own walkthrough rather than a paragraph here.

Next steps