Product
The first five minutes
Aaron at Job Umbrella · September 4, 2026 · 8 min read
First, the answer to last week’s open question. It took four days and it was not in my code.
Four days, and it was a database grant
Matching had been failing with my own useless error message. The runtime log said it in one line, the first time I bothered to read it: Postgres error 42501, permission denied for that table.
The table had been created by a one-off database role, while every other table in the schema belongs to the application’s role. In Postgres the creator owns the object, so the application’s own role had never been granted anything on it at all. Migrations were fully applied. The table existed. The app simply was not allowed to look at it.
There is no commit that fixes this, which is part of why it took so long — I kept looking in the repository for a problem that was not in the repository. The fix was a grant, plus a default-privileges rule so that a future table created by that role cannot repeat it. The lesson is embarrassing and universal: read the runtime log before you read your own code. Mine was telling me the answer on day one.
The review that found the actual problem
With matching working, I put the whole product in front of four reviewers, each with a different lens: information architecture, first-session activation, retention, and accessibility on a phone. They worked independently and returned 69 findings between them.
All four ranked the same thing first.
A new account opened onto an empty board whose only available action was a twelve-field form — and none of the AI tools that had convinced the person to sign up were anywhere in sight.
That is a brutal finding and it was obviously right the moment I read it. I had built the product in the order that made sense to me, which is the order it was written. Somebody arriving for the first time is out of work, probably tired of the whole process, and I was opening with data entry.
So the first thing you meet is a free resume review
The headline change: you can now get your resume reviewed at /review without an account at all. Upload it, and you get the same parse and the same review the profile uses — a score, and a note pinned to each weak line.
This is the same argument as the post three weeks ago, moved to the front door. The first thing this site should tell a stranger is whether their resume is any good, because that is the thing nobody else tells them. Asking for an email address before doing anything useful is the standard pattern and I dislike it.
Mechanically: the uploaded file is never stored. The result is held encrypted for seven days behind a cookie, so creating an account claims the review and lands you on a profile that is already filled in. Guests are metered per connection and site-wide per day, so an anonymous endpoint that costs real money cannot starve the people who signed up.
The load test that turned into an architecture fix
Before shipping 32 changes I ran a load test, expecting a capacity answer. What came back was stranger: throughput sat at about 38 requests a second no matter the concurrency. Ten workers or a hundred, the same number, with latency simply queueing behind it. A flat ceiling like that is not capacity. It is something serial.
Every public page — all twelve guides, the Help Center, privacy, terms — was being rendered on demand, per request, despite being completely static content. Bisecting with real builds ruled out the layout, the middleware and the config. The cause was the site-wide 404 page.
It called the authentication helper, to decide whether to offer “go to your board”. Next.js renders the root not-found boundary into every page’s shell. One cookie read, in a page almost nobody visits, opted the entire site out of static rendering.
Moving that check into a small client component took mixed public pages from 38 to 92 requests a second, and a guide’s median response from 899 milliseconds to 28 under twenty-five concurrent readers. Static pages alone now serve about 1,585 a second. The rule I wrote down afterwards: never read cookies in the root 404 or the root layout.
Then 112 functional checks, and a security review
The functional pass found two things worth repeating. Calendar dates displayed a day early for every reader west of Greenwich, because date-only values are stored at UTC midnight and were being formatted in the reader’s own timezone. And the API rejected appliedDate: null — which is exactly the shape the API itself returns for an unset date. Any client that read a job, changed one field and saved it hit that.
The security review closed four high-severity findings. The one that bothers me most was an account-linking flaw: someone could register a stranger’s email address with a password, and when the real owner later signed in with Google the two accounts merged, leaving the squatter’s password working against the victim’s data. Also fixed: server-side request forgery through the bring-your-own-key endpoint, an open redirect that a backslash slipped past, and a rate limiter that one forged header defeated — verified by sending eight password resets without a single refusal.
Two of the fixes broke something in their own first deploy, which is exactly why the verification pass exists. The new limits on array sizes made an oversized profile fail to parse, and a document that fails to parse is coerced to an empty one — so a 201st skill would have wiped the other 200. Fixed, then verified again: eight of eight clean.
Release notes
September 4, 202629 commits · 5 migrations · merge dedf91dShipped
- Free resume review before sign-up, with no account. The file is never stored.
- A first-run home with three ways in, replacing an empty board and a twelve-field form.
- Add to board from any match or feed listing.
- A needs-attention strip: quiet applications, follow-ups due, and deadlines within a week.
- Match and weekly digest email, with one-click unsubscribe.
- The board works by touch and by keyboard, not only by dragging.
- An accessibility pass: labels, announced errors, focus management, and a skip link.
- The Dashboard is merged into the Board. Its one chart was already drawn better by the columns.
- Job-match passes moved off the page load onto an hourly scheduled job.
Fixed
- Matching was refused by the database. The table was owned by a different role and the app had no grant.
- The whole marketing site rendered on demand because the root 404 page read a cookie. 38 to 92 requests a second.
- Calendar dates showed a day early for every reader west of Greenwich.
- The API rejected a null applied date, which is the shape it returns itself.
- Review scores swung more than 20 points on an unchanged profile. Temperature is now zero and the rubric is a mechanical checklist.
- A corrupt upload answered 500. It now answers 400 with a next step.
Security
- Account takeover through provider linking: an unverified account's password and tokens are revoked before linking.
- Server-side request forgery through the custom AI endpoint.
- An open redirect: a backslash was read as a slash.
- Every per-address rate limit was defeated by one forged header.
- CSV exports wrote cells that spreadsheets execute as formulas, from third-party listing text.
Still open
- One deploy failed on a database address typed by hand, and the platform rolled back automatically. The site looked healthy from outside while shipping nothing.
Everything here is free to use
AI resumes and cover letters, a fit check against any posting, search across 12 job boards plus government sites, and a Board that tracks every application. No employer can pay to reach the top of your list.