I'm a second-year IT engineering student. What I do have is GitHub, a laptop that overheats, and a habit of picking up problems that are slightly too big for me.
This is the story of how I built the official CLI for urBackend a BaaS (Backend as a Service) platform from scratch, over three hard days, and somehow got it merged.
How I Found This
I was scrolling through GSSOC (GirlScript Summer of Code) organizations looking for something interesting to contribute to. Most projects wanted documentation fixes or UI tweaks. Then I saw urBackend:
"The developer's backend. Instantly generate secure CRUD APIs for your frontend applications using Node.js and MongoDB. Focus on the UI, let us handle the DB."
That got me. It's a real product. Real users. Real codebase. And they had an open issue for something ambitious — building an official CLI tool so developers could manage their projects from the terminal instead of clicking through a dashboard.
I claimed it.
What I Was Building
The idea was simple: a command called ub that lets developers do things like this from their terminal:
ub login
ub project list
ub collection list
ub status
ub doctor
No browser. No dashboard. Just the terminal.
The CLI needed to talk to urBackend's dashboard API using Personal Access Tokens — basically long-lived keys that prove you're a real developer without needing a browser session.
Simple enough on paper. Three days of reality had other plans.
Day 1: Building the Foundation
I scaffolded the entire CLI in TypeScript using Commander.js and tsup. The architecture I landed on had a clean separation:
-
core/— HTTP client, config reader/writer, logger, error classes -
services/— pure API wrappers, no console output, no filesystem -
commands/— orchestrate service calls and print results -
types/— TypeScript interfaces for every API response
The rule I enforced on myself: commands never touch JSON directly. Config persistence goes through config.ts. Network calls go through services/. Commands only orchestrate and print.
One thing I'm proud of from day one is atomic config writes. When you run ub login, your token gets saved to ~/.ub/config.json. If the process crashes mid-write, you don't end up with a corrupted file. The trick is simple:
const tmp = CONFIG_PATH + ".tmp";
fs.writeFileSync(tmp, JSON.stringify(config, null, 2));
fs.renameSync(tmp, CONFIG_PATH); // atomic on most OS
Write to a temp file first. Rename it. The rename either completes or it doesn't — there's no half-written state. Small thing. Matters in production.
By end of day one, the build was compiling clean and ub --help was showing commands. Nothing actually worked yet, but it existed.
Day 2: The Most Frustrating Six Hours of My Life
This is the part I almost didn't include. But it's the most honest part, so here it is.
I tried to run ub login against the local backend. I pasted my test token. And I got this:
✓ Logged in successfully.
✖ Unable to connect to the urBackend API.
Success and failure. At the same time. For six hours.
The first problem was that the PAT UI wasn't built yet. The dashboard had no "Generate Token" page. So I had to manually insert a fake token directly into MongoDB by computing its SHA-256 hash and writing it to the pats collection. That took an hour just to figure out.
const raw = 'ubpat_test_localdevtoken123';
const hash = crypto.createHash('sha256').update(raw).digest('hex');
db.pats.insertOne({ developer: ObjectId('...'), tokenHash: hash, ... });
Second problem: even after fixing that, ub login would print "Logged in successfully" and then immediately print "Unable to connect." I added a debug line to see the raw API response:
{
"error": "Not Found",
"replayId": "kiroo-1782642930559-63fbf1"
}
Not Found. But the curl command worked fine. Same token. Same endpoint. Different result.
I spent two hours on this before I realized: every time I ran ub login before setting the URBACKEND_API_URL environment variable, the CLI saved https://api.urbackend.bitbros.in (the production URL) to ~/.ub/config.json. Then even after I set the env variable, the config file took priority and the CLI kept hitting production.
rm ~/.ub/config.json
export URBACKEND_API_URL=http://localhost:1234
ub login
That fixed it. One deleted file. Six hours of debugging.
The third problem was CSRF. The dashboard API protects all routes with CSRF tokens for browser sessions. The CLI doesn't use cookies — it uses Bearer tokens — but CSRF middleware was still running and blocking requests. I had to add a bypass in app.js:
if (req.path === '/api/billing/webhook' || req.path.startsWith('/api/user/cli')) {
return next(); // skip CSRF for CLI routes
}
The fourth problem: all the project routes used authMiddleware which validates JWT session cookies. The CLI sends a Bearer PAT token. So every ub project list call got "Invalid Token."
The fix was a new middleware called authFlexible.js:
const authFlexible = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ubpat_')) {
return authenticateCLI(req, res, next);
}
return authMiddleware(req, res, next);
};
One middleware. Accepts both. Browser requests unchanged. CLI requests work.
By end of day two, ub login, ub whoami, ub project list, ub project use, and ub collection list were all working.
Day 3: CI, Review Comments, and What I Learned About Hidden Files
I opened the PR on day three. 12 commits. The CI pipeline immediately failed:
FAIL src/__tests__/routes.user.test.js
● Test suite failed to run
REDIS_URL is not defined in .env
My authenticateCLI middleware imports from @urbackend/common, which imports Redis, which crashes on import if REDIS_URL isn't set. The test environment didn't have it.
Two fixes. First, add the env var to CI:
env:
REDIS_URL: redis://localhost:6379
Second, mock the middlewares in the test file so the ESM import chain never reaches Redis:
jest.mock('../middlewares/authenticateCLI', () =>
jest.fn((req, _res, next) => { req.user = { _id: 'mock_user_id' }; next(); })
);
155 tests passing. 0 failing.
Then came the review comments. The maintainer pointed out that my ub doctor command was collapsing all errors into the same bucket. A timeout looked the same as a bad token, which would tell users to re-login when the real problem was connectivity.
He was right. I added a three-state system:
-
true— check passed -
false— definitively failed (HTTP 401, 404) -
"unknown"— could not validate due to network or 5xx
So now ub doctor shows:
⚠ PAT Could not validate — API may be unreachable
instead of:
✖ PAT Invalid or expired — run 'ub login'
when the server is just down. Small change. Much less confusing for users.
Things I Actually Learned
PATs (Personal Access Tokens) — I had used GitHub tokens before without thinking about how they work. Building this made it concrete: the raw token is shown once and never stored. What gets stored is a SHA-256 hash. When you authenticate, your token gets hashed and compared against the stored hash. The raw token never touches the database.
Hidden files start with a dot — ~/.ub/config.json is hidden because the folder is .ub. That's it. No special flag, no metadata. Just a dot prefix. Same reason you never see .gitconfig, .npmrc, or .ssh in your home folder without ls -a. Decades-old Unix convention that every CLI tool follows.
CSRF doesn't apply to Bearer token auth — CSRF attacks work by tricking a browser into making requests using its stored cookies. If you're not using cookies, CSRF protection does nothing useful. The CLI uses Bearer tokens, not cookies, so bypassing CSRF for CLI routes is correct and safe.
Tests mock things for a reason — The marked package is ESM-only. Jest runs in CommonJS mode. When my middleware imported @urbackend/common, which imported emailService.js, which imported marked, the entire test suite crashed. Mocking the middleware in tests isn't laziness — it's isolating what you're actually testing.
Read the config file before debugging for six hours — If something worked yesterday and doesn't today, check your config file first.
The Final Command List
ub login # authenticate with PAT
ub logout # remove stored credentials
ub whoami # show authenticated developer
ub project list # list all projects with usage
ub project use # select active project
ub project info # show project details
ub collection list # list collections with schema
ub collection delete # delete collection (with confirmation)
ub status # account plan, limits, recent activity
ub doctor # diagnostic checks
ub doctor --json # same output as JSON for CI/AI agents
The --json flag on ub doctor is something I'm especially happy with. AI agents and CI pipelines can consume it directly without parsing terminal output. One command tells you everything about your setup in a machine-readable format.
What's Next
ub pull, ub push, and ub generate aren't implemented yet — they need a schema sync endpoint on the backend that doesn't exist yet. That's Phase 3 of the roadmap. I'll build it when the backend is ready.
Why I'm Writing This
I'm a second-year student with no internship and no referral. What I have is a merged PR in a real open source project, a CLI that actual developers will use, and a much better understanding of how auth middleware actually works.
If you're in the same position — no experience, no connections — find a project with a real issue, claim it, and do the work. The debugging is miserable. The CI failures are annoying. The review comments sting a little. But the PR number at the end is real, and nobody can take it away.
PR #340 if you want to see the code: github.com/geturbackend/urBackend/pull/340
- Follow me on X for more open source, systems programming, and the unglamorous parts of learning to code.*
👉 @KATSUKI_EXPO
👉 Github
Top comments (1)
How did you handle error handling and input validation in your CLI, and are there any specific libraries you used for this purpose? I'd love to swap ideas on building robust command-line tools.