PROJECTSVISIT LIVE SITE
CASE STUDY — 2025

FILEDROP

CLIENTPersonal project
ROLEMaintainer — audit, security & ops
YEAR2025
STACKNext.js 14 · Firebase · Clerk · Vitest · GitHub Actions
FILEDROP
01 — The problem

FileDrop started as a tutorial project (Josh tried coding, late 2023) — a personal file storage app: drag-and-drop upload, list, rename, download, delete. This case study is not about that initial build. It is about auditing it in July–August 2026 the way I would inherit someone else's product at a new job: reconstruct the decisions baked into the code, judge which ones still hold, and fix what is actually broken.

The most serious thing that audit found was a security gap I had created myself in an earlier certification pass: Firebase security rules required `request.auth.uid`, but authentication ran through Clerk. Nothing bridged the two — every Firestore request arrived as anonymous. Neither `firebase.json` nor `.firebaserc` existed in the repo, so the rules were files sitting in the codebase, not a deployed protection. And my own certification writeup had stated, in writing, that access control (OWASP A01) was handled.

Who this was for

Anyone reviewing whether I can find and close a real security gap in code I wrote myself under a false sense of confidence — not just ship a feature. Also a direct answer to a piece of feedback from a scale-up engineer who screens candidates: side projects matter less than proving you can read decisions you inherited, justify priorities, and stay pragmatic — which is exactly what auditing my own past work forced me to do.

02 — Decisions made — and why
01

A server-side Clerk → Firebase token bridge (`POST /api/firebase-token`), not Clerk's official Firebase JWT template.

Clerk's built-in `integration_firebase` JWT template is deprecated and can no longer be activated on a new application — building on it would have shipped a dead end. Instead, a route reads the Clerk session server-side (`auth()`), and only then asks the Firebase Admin SDK to mint a custom token with that Clerk user ID as the Firebase `uid`. The client never sends an identifier, so it cannot request a token for someone else, and the existing `users/{userId}/files` paths stay valid — zero data migration.

02

7 isolation tests against the Firestore emulator, validated by deliberately replacing the ownership rule with `allow read, write: if true` and watching 5 of 7 turn red.

A security test suite that has never seen its own tests fail is unproven — mine had been read, judged correct, and never once executed against a real check. The seven scenarios cover both directions (user A can read/write their own space; user A is refused on user B's files; an unauthenticated request is refused entirely). Deliberately reintroducing the bug and confirming the tests catch it — 5 failed, 2 passed, the two that still passed being unauthenticated-request checks unaffected by an ownership rule — is what makes the suite trustworthy rather than decorative.

03

A `/api/health` probe checking three critical dependencies in parallel (config, Firebase identity signing, Storage bucket reachability), each capped at 5s, returning 200 or 503 — plus a scheduled active probe that calls it and fires an alert.

The identity check does not just ping Firebase — it actually signs a token for a reserved test UID, because a network-reachable Firebase can still fail to authenticate if the service key is revoked; only the real operation catches that. A 401/403 from the Storage bucket is treated as healthy (the service answered, it just refused an anonymous request) while only a 404 or 5xx counts as a failure — conflating "refused" with "broken" would page someone on every normal day. The whole chain was verified end-to-end by actually pulling `FIREBASE_PRIVATE_KEY` from the environment and watching the probe go from 200 to 503 and the alert fire with the exact missing variable named.

GALLERY
FILEDROP — 01
FILEDROP — 02
FILEDROP — 03
03 — What I chose not to do

No data migration alongside the auth bridge fix.

Migrating existing records while also changing the authorization model doubles the blast radius of a security fix. Bridging the identity systems at the token level fixed the isolation gap without needing to touch a single stored record.

Did not apply `npm audit fix --force` on a proposed downgrade of `firebase-admin` from v14 to v10 to silence a moderate `uuid` advisory.

The automated fix would have rolled back four major versions of the SDK that issues the identity tokens — the single most security-critical piece of the whole system — to resolve a minor transitive advisory. Applying it blindly would have traded a real regression for a cosmetic fix. The lesson kept from this: a proposed fix flagged `isSemVerMajor` must be read before it is applied, because the flag can hide a regression, not just a breaking change.

Tested the upgrade path to Next.js 16 / React 19 in an isolated copy of the repo, confirmed it fixes 21 of 26 known vulnerabilities — and did not ship it.

Typing, the full test suite and the production build all passed clean on the upgrade. What none of that covers is React 19's runtime behavior on the real user flows (drag-drop upload, modals) — and this project has no staging environment, so the only place to validate that would be production itself. The vulnerabilities left unpatched target request rewriting, middleware and Server Actions — features this project does not use — so the real exposure is far lower than the raw count suggests. The upgrade stays queued behind setting up a staging environment first, not silently dropped.

04 — What I'd measure
5/7ISOLATION TESTS CAUGHT THE REGRESSION
42AUTOMATED TESTS IN CI (UP FROM 26)
3/3DEPENDENCIES UNDER 200/503 CONTRACT

This is a zero-user personal project, so the proof here is not usage data — it's deliberate verification: pulling the service key to confirm the alert chain actually fires, breaking the ownership rule on purpose to confirm 5 of 7 tests catch it. The product north star I did define — the number of users who come back on a later day to retrieve a file dropped in a previous session, over a trailing 7 days — has an instrumented event (`file_downloaded` with `sameSession` and `fileAgeHours` properties) and a pre-set kill threshold: fewer than 5 distinct users doing that over 30 days means the product solves no real problem. I have not looked at that number yet, because there is no real traffic to look at.

05 — What I'd redo differently

If I started this project over, I'd pick a single identity system for both auth and data authorization from day one — Firebase Auth alone, or Clerk with a database that reads its session directly — instead of Clerk for auth and Firebase for authorization. The token bridge is a solid fix, but it exists only because two systems were bolted together after the fact. I'd also not have shipped "share" as a missing feature from day one: the audit of the tutorial's own decisions found no sharing at all, on a storage product whose homepage literally promised "securely collaborate" — sharing is the one growth channel a storage product has, and its absence was the single most consequential inherited decision I found.

NEXT PROJECT
WATTWILLER
KEEP SCROLLING