In this chapter
Anna makes the waitlist safe and solid: organiser logins with hashed passwords and sessions, permission checks, a proper Git workflow even when working alone, and unit and integration tests that run automatically — reviewing Acts 15, 17 and 22 in a real project.
The Problem in Real Life
2:30 PM. Liam wanders over and, as a joke, sends DELETE /events/1/waitlist/2 from his laptop. It works. Someone's place in the queue disappears. He looks guilty; Anna laughs. "Point taken. Logins next."
John adds a note on his clipboard and says only: "Remember Act 22. And Act 17. Hack Day projects still get attacked and still break."
Working alone is no excuse to skip the safety nets.
John
"It's Just a Hack Day Project" vs. Building It Like Real Software
Anyone can do anything
Without logins and permission checks, any visitor can delete anyone's place in line.
One big folder of changes
Without commits and branches, one bad change can break everything with no way back.
Does it still work?
Every new feature can quietly break an old one, unless tests check it automatically.
Adding Authentication, a Git Workflow and Tests
The shop analogy, continued: the shop now needs staff badges (only staff go behind the counter), a logbook of every change to the stock (so mistakes can be undone), and a daily stock check that confirms everything is where it should be. In software: authentication and authorization, Git, and tests.
- Authentication for organisers (Act 22): organisers sign up with email and password; passwords are stored only as bcrypt hashes. After login, the server creates a session and sends an
HttpOnly,Secure,SameSitecookie. Fans don't need accounts — they join with just an email, which keeps joining easy. - Authorization — who may do what (Act 22): a small middleware
requireOrganiserchecks the session. Then each organiser endpoint checks ownership: you can only see or edit the waitlist of an event you created — otherwise404(Act 25's pattern). Liam's joke now returns401. - Basic protections: rate-limit the join and login endpoints (bots, Act 22), validate every input, never log passwords or full emails in plain text, and keep the session secret in an environment variable (Act 16).
- Git workflow, even alone (Act 15):
git initin the first minute, a.gitignorefornode_modulesand.env(Act 16) before the first commit, small commits with clear messages (Act 25's Conventional Commits), and a branch per feature (feature/auth) merged intomainonly when it works. When she breaks something at 4 PM,git diffandgit restore(Act 15) get her back in seconds. - Unit tests (Act 17): test small pieces alone: email validation, position calculation. Fast and precise.
- Integration tests (Act 17): test the real API against a test database: join returns 201; joining twice returns 409; an organiser can't see another organiser's list; a removed fan's position disappears and everyone moves up. The
supertestlibrary sends real HTTP requests to the app without starting a server. - Run tests on every push (Act 19): a small GitHub Actions workflow runs
npm ci, the linter and the tests on every push and pull request — with a PostgreSQL service container for the integration tests. Even working alone, hermainbranch can only receive green code.
| Test | Expected |
|---|---|
| Fan joins with a valid email | 201 Created |
| Same email joins again | 409 Conflict |
| Invalid email | 400 Bad Request |
| Position after someone ahead is removed | Moves up by one |
| Delete an entry without logging in | 401 Unauthorized |
| Organiser B reads organiser A's list | 404 Not Found |
| Commit | What it did |
|---|---|
| chore: initial project with .gitignore | node_modules and .env ignored from the start |
| feat: create events | Migration + POST /events |
| feat: join waitlist and get position | Core feature |
| feat(auth): organiser signup and login | bcrypt + sessions |
| test: integration tests for waitlist | 14 tests |
| fix: positions update after removal | Bug found by a test |
async function requireEventOwner(req, res, next) {if (!req.session.organiserId) {return res.status(401).json({ error: { code: "login_required" } });}const { rows } = await db.query("SELECT id FROM events WHERE id = $1 AND organiser_id = $2",[req.params.id, req.session.organiserId]);if (!rows.length) return res.status(404).json({ error: { code: "event_not_found" } });next();}app.get("/events/:id/waitlist", requireEventOwner, listWaitlist);app.delete("/events/:id/waitlist/:entryId", requireEventOwner, removeEntry);
it("does not let one organiser read another organiser's waitlist", async () => {const eventId = await createEventAs("organiser-a@test.example");const agentB = await loggedInAgent("organiser-b@test.example");const res = await agentB.get(`/events/${eventId}/waitlist`);expect(res.status).toBe(404);});
By 6 PM on day one: organisers can sign up and log in; only they can manage their own events; fans join with an email; 14 tests pass locally and in CI; and the Git history reads like a story: feat: create events, feat: join waitlist, feat(auth): organiser signup and login, test: integration tests for waitlist, fix: positions update after removal. Liam tries his joke again: 401 Unauthorized. He gives a thumbs up.
Key Takeaway
Build a hack project like real software: organisers authenticate with bcrypt-hashed passwords and secure session cookies; every organiser action checks ownership on the server; rate limits and validation protect public endpoints. Use Git from the first minute — .gitignore before the first commit, small clear commits, a branch per feature. Write unit tests for small pieces and integration tests against a test database, and run them in CI on every push.
Why This Matters
Interviewers who look at your portfolio projects check exactly these things: are passwords hashed, are permissions checked on the server, is there a real Git history, and are there tests that run in CI? A small project that does all four says far more about you than a big one that does none.
Day one ends with a solid app — on Anna's laptop. Day two's goal: put it on the internet, at a real address, with a padlock in the browser.
