JOBBL
Overview
A marketplace for short practical work, open from thirteen, designed around what it refuses to know.
Details
At a glance
- Project type
- Marketplace, mobile-first web
- My role
- Defined the product and its safety rules, designed it, and implemented the platform and the app.
- Category
- Products
- Status
- In development2026
What I did
- Set the rules that let a thirteen-year-old use it lawfully
- Designed the flows for finding work, posting a job and talking about it
- Built the domain layer, the versioned API and the authorisation suite that tests it
- Decided what the platform must not collect, and removed what already collected it
Built with
- TypeScript: The whole stack, with the rules in their own package
- Next.js: The web client and the versioned REST API
- PostgreSQL: The record, hosted in the EU beside the application
- Drizzle: Schema and queries
- Zod: One set of schemas shared by the client, the API and the services
- Cloudflare R2: Photo storage, re-encoded and stripped before it arrives
- Twilio: The one-time codes that are the only way an account exists
Next project
Wisr
The need
Hedges, snow, raking, carrying. The short practical jobs that used to be arranged between neighbours, and that are still the first work most Norwegians ever did.
They sit badly on the platforms that exist. Those are built for tradespeople quoting on projects, and they assume an adult with an organisation number. So the small job goes unposted, and the fifteen-year-old who would gladly do it has nowhere to be found.
One account, no bidding
The same person works on Saturday and posts a job on Sunday, so there is one kind of account and signup never asks which one you are. The two extra things a poster needs are asked at the moment of their first posting, and not before.
The person posting sets the rate. There is no bidding, because a bidding market for four hours of raking pushes the price down and the youngest people to the bottom of it.


Decision
Delete the timer, because knowing was the liability
- Context
- There was a work timer: a server-side start and stop, a frozen rate, a computed amount. It worked. Then JOBBL opened to thirteen-year-olds. Under the Norwegian tax authority's platform-reporting rules, a platform that knows what a worker is owed, or could reasonably work it out, must collect and verify every worker's national identity number and report their income each year.
- What I decided
- The timer was removed. Work now ends when both sides say it is done, and JOBBL records who said so and when. Never how long, never how much.
- Trade-off
- The product lost a genuinely useful feature and the trust it would have carried. What it bought was not having to hold a national identity number for a thirteen-year-old, which is the last thing a small marketplace should be storing.
Open from thirteen
Permission is a table, not a warning. A thirteen-year-old may be offered gardening and clearing, with a parent's confirmation. From sixteen, painting, moving, assembly and carrying open up. Demolition is closed to anyone under eighteen at any time. Choose demolition when posting and the youth option is simply not there; set the start between eight in the evening and six in the morning and the youngest bands disappear from that job.
A parent confirms on their own handset, and never with a signature. A finger drawn on a child's own screen proves nothing, the child can draw it, and evidence that looks like evidence and is not is worse than none. So the young person types a parent's number, a code goes to that phone, and what is recorded is a number, a time and an act on a second device. It does not prove parenthood. Nothing short of national identity would.
Age is stored as a band, not a birth date. JOBBL cannot verify an age, so a birth date would collect more about a child and prove nothing extra. A band can be corrected upwards, which is a birthday, and never downwards, which is what quietly ends a parent's involvement at sixteen without the platform holding a date to count towards.
Decision
No position, no number
- Context
- The distance under a job card used to fall back to the centre of Oslo when someone declined the location prompt. Somebody in Bergen then read a confident distance measured from a city they were not in.
- What I decided
- A distance JOBBL cannot stand behind is not printed at all. The card shows the place instead, and the distance filter disappears with it. The position itself is asked for once, kept in the browser and never stored on the server.
- Trade-off
- Some cards carry less information than they could. A filter that says within twenty-five kilometres of a guess is a coin toss that hides real work, and a number nobody can stand behind is worse than no number.
How it works
- The web app is a client of the platform, not the platform. The rules live in their own package behind a versioned API, so a native app later inherits every one of them rather than reimplementing them.
- Identity comes from a signed http-only cookie and from nowhere else, never a body, a query or a header. Every service function takes an actor, so there is no unchecked read.
- Database rows never leave the server. Responses are rebuilt field by field, so a column added later cannot leak by accident.
- One endpoint an anonymous caller can make cost money, the one that sends a code, so its limits are a budget, not politeness: Norwegian numbers only, one code a minute, three an hour, ten a day, a per-address limit and a hard daily ceiling the app refuses to pass. Codes are stored only as a keyed hash, expire in minutes, die after five wrong guesses, and work once.
- A code knows what it is for. One sent to a parent to confirm their child can never be used to sign anybody in as that parent.
- Chat exists only inside a job, only between its two sides, and only once the worker has applied. There is no contact list and no way to reach a stranger, which removes a category of abuse before it can exist.
- Photos are re-encoded on the server before storage: orientation applied, every piece of camera metadata stripped, and resized to a display size. A phone photo of somebody's house carries the coordinates of that house, and none of that belongs in a public feed. Decoding is the validation, so a file that only claims to be an image never reaches storage.
- A report is never readable by the person it names, and you can only report what you could already reach, otherwise asking to report a conversation would answer whether it exists.
- Money is integer øre, never a float and never a string, and pay is only ever computed on the server.
- A per-request nonce and a content security policy mean a script that arrives any other way does not run. It ships in report-only first, because the difference between a policy that protects and one that takes the site down on a Saturday is which order you do that in.
Where it stands
The loop runs: sign in by phone, find a job, apply, talk about it, agree it is done, rate each other. The authorisation suite signs in as three different people and tries to read and write each other's data through the real API, and that is the test kept green above all others.
It is not launched. Push notifications, payments and a map view are not built, and neither is a way for somebody whose content was removed to answer the decision, which the terms invite, so it is written down as owed rather than left out.
What the project turned out to be about was not features. It was a series of decisions about what a platform should refuse to know: not the hours worked, not a child's birth date, not where anybody is standing.