Since 2021 · 369 commits

@unicorn: an old app, a new stack, and two AI tools

@unicorn calls itself “a place to express your uniqueness”. I started it in 2021. In 2026 Codex and Claude moved its images, tightened it and rewrote it in TypeScript. Counted from the logs, which only cover this year.

Light grey cover with soft rainbow glows in the corners. On the left, the @unicorn logo, a white unicorn leaping over a rainbow, and the title "An old app, a new stack", above the line "@unicorn since 2021, rebuilt in 2026 with Codex and Claude." On the right, a screenshot of the live About page: the unicorn avatar, the line "A place to express your uniqueness", and "Developed using Next.js, React, TypeScript, Tailwind, Firebase".

@unicorn’s About page on the live site, after the TypeScript rewrite. Image and site: Edgaras Neverdauskas.

Download
Alt text · 434 characters

Light grey cover with soft rainbow glows in the corners. On the left, the @unicorn logo, a white unicorn leaping over a rainbow, and the title "An old app, a new stack", above the line "@unicorn since 2021, rebuilt in 2026 with Codex and Claude." On the right, a screenshot of the live About page: the unicorn avatar, the line "A place to express your uniqueness", and "Developed using Next.js, React, TypeScript, Tailwind, Firebase".

On a Saturday afternoon in June I sent Codex a screenshot of an error and four words: "need to fix this". Twenty minutes later I asked it to check the packages for security issues. Then came the bigger job: move the images off Firebase to "somethign that is free and witohut pay to go".

A few messages later I wrote down the one rule that shaped the rest of the evening: "i need storage wehre i know that it will not charge but will rather stop working".

That was the first logged conversation about @unicorn. The site itself is much older. This is the account of what happened to it in 2026, with a short look at the five years before. As with my other build stories, I went back through the session logs and the full git history, so the numbers are counted, not remembered. One limit up front: the AI logs start in June 2026. For 2021 to 2025, git is the only record.

What @unicorn is

A small image-sharing site at atunicorn.io. Its own about page calls it "a place to express your uniqueness". You sign up, pick a username, write an about page, and post pictures. Everyone's posts land in one shared feed. Posts marked NSFW stay covered until you tap them. New accounts are approved before they can post.

It runs on Next.js as a static site, with Firebase for accounts and data. Images live on Cloudinary, behind a small Cloudflare Worker. Since September it is TypeScript with Tailwind.

Before the logs: 2021 to 2025

The first commit landed at 12:34 on 6 March 2021, in a repository then called "unicorn". Two of us started it. Another developer set up the base: the project structure, loading users and posts, file upload, and later email sign-in. That was 17 commits. I did the screens and the look, and most of what came after.

The busiest month by far was March 2022, with 172 commits, most of them with "update" in the name, like "updated post", "updated radio button" or "small update". It began that month on Next.js 10 and React 16 (Next.js 12 and React 17 by the end), with react-native-web and two small styling and state libraries built for that setup. When I dropped react-native-web in 2026, I said what I had used it for: "i ued that for automatiucally loading the post on the scroll". It went quiet after a last fix on 1 May 2022.

In October 2025 I came back and flattened the project. In December I moved it to Next.js 16, React 19 and a current Firebase. Eight of those nine commits are called "updated", the ninth is a merge, and no AI logs survive from then, so I can't say who or what helped.

How it happened in 2026

Times below are the local time on the machine that did the work: UTC+3 in June and July, UTC+7 from August.

6 June: the images move. The session began with an audit of the packages and a page error left over from the framework update. Then came the storage. Codex first built the move to Cloudflare R2, which has a generous free tier. When I read the fine print on the sign-up screen, I saw that R2 bills if you go over. I asked for something that stops instead. Codex suggested Cloudinary's free plan, which needs no card, and rebuilt the change for it within minutes. A small Worker keeps the Cloudinary key off the browser, and the app keeps Firebase for accounts and data.

Moving the old pictures hit a wall. The first copy failed with "402 Payment Required": the old store would not serve a single file without a billing plan. So I switched billing on, copied all 233 images across, and at 18:48 wrote "OK EVERYTHINNG LOADS". Four minutes later: "ok disabled". The change was committed at 19:15.

Along the way I pressed "y" at a terminal prompt I didn't mean to answer: "shit i pres y, how to revert". Codex asked me to paste the git status, read it, and told me nothing needed reverting.

7 June: small things. The header icons weren't centred. I wanted to watch the fixes happen, and when Codex kept closing its browser I asked it to stop: "i want to se chagnes live". In the afternoon I asked for simpler routing, "remove the modal based routing an have only pGE ROUTING". Every post and profile is now its own page. Of the ten commits over the two days, only two came from a logged Codex session.

17 July: everything at once. I asked Codex to check the whole app and find anything worth optimising. The report listed about 1.4 MB of compressed JavaScript on the home page, the whole icon library bundled, and the whole database downloaded at start. I asked: "can you fix everything at once?" It did, in one large pass, and measured the JavaScript down to 306 KB. None of it was ever committed. When I came back three days later, the working copy was clean.

20 July: one step at a time. This time I asked for a plan first. Codex finished its first phase in about 50 minutes, and I threw it away: "i dont liek that there was som any fils changes and added that i dont udnerstand why". We started again with the dependencies alone, in two files. Fifteen pinned package overrides came down to one that was still needed.

After that the day was a loop. Codex finished a small phase, reported what it had checked in a real browser, and I answered. 32 of my 70 messages that day were some version of "continue" or "whats next". The phases covered cleaner loading states, feed images down from about 7.4 MB to 2.2 MB on load, a keyboard and screen-reader pass that took the Lighthouse accessibility score from 90 to 100, and a round of security tightening, tested locally before it went live.

Two of my messages mattered more than the rest. When a tidy-up touched the logo animation: "hey hey gif id improtant w i need that nimation it is the main point". And when the feed began loading every post at once instead of as you scroll, I guessed where it broke: "ithink something happened when we wrapped component in side main". Codex had been working around it. It agreed, dropped the workaround and fixed the wrapper instead. At 17:09 Codex's own meter showed its weekly allowance at 100%. Three minutes later it showed a new week starting from 0%, and the work carried on. At 19:53 the changes were pushed and live.

On 26 July I reported the scroll problem again. That message has no reply in the log. The last meter reading, on 20 July, showed 60% of the new week used.

12 August: the audit that went too far. "seemsl ike we havw security vurnaliblieties in this repo pelase audit". Codex found real gaps and fixed them all in one pass. It took the evening to test, and once it went live, the live site showed an error. At 22:58: "ok oyu know what rever all the changes ew made today and only fix the dependbot issues and redeploy everytihng". Then I asked what risk was left, and we put back only the protections that mattered most, one at a time, testing each.

I also made a product call that night. Codex walked me through what would happen if I removed the approval step and let anyone post. My answer: "yeahi am afraid of illigeal content". Approval stays. One more change went in and came out: a cleanup for images from posts you start and then abandon ended up keeping them in storage instead. I reverted it, and the final fix was simpler: an image is only uploaded when you publish the post.

8 September: the late-night cleanup. At 22:29 I asked Claude to give all my repositories proper descriptions. @unicorn's read "make stuff happen". Its README described an app that no longer existed, so Claude rewrote it from the source. It deleted a release workflow that could never have run, and fixed a dependency alert whose cause was one of my own version pins.

It also noticed that dark mode was fully built and nobody could reach it. I asked for a switch. Turning it on showed four bugs within the hour: icons painted dark on dark, a white box around the loading animation, profile text you couldn't read, and an author name stuck in black. I sent screenshots, Claude fixed them one by one, and each fix went live within minutes. At 00:21 I simplified it: "is use is nto connect we don disaply the theme mode switch at all". Just before that, I had left a note for the morning: @unicorn was "only one prejctn ow that is not i nsnc" with my other active repositories, which are TypeScript.

9 September: the rewrite. At 11:15 I laid out what I had in mind: TypeScript, Next.js, Vitest and Tailwind, dropping react-native-web, maybe GitHub Pages instead of Firebase hosting, maybe the database on Cloudflare too. Claude agreed with two ideas and argued against the other two. Dropping react-native-web was right, and the old styling library should go with it. GitHub Pages would add a host without removing one, and the thing tying the app to Firebase is the accounts, not the data.

At 12:35 the rewrite was merged, 80 minutes after my message. 93 files changed, and react-native-web was gone. Then I used it, and found three bugs in 20 minutes: the page crashed when you scrolled past the point where the header shrinks, the unicorn logo was squashed, and a refresh showed an empty feed before the loading animation. All three came from the rewrite, and all three were fixed and live by 12:57.

At 13:01 I asked: "shoudlwe have moretest to amke sure eveyrtihng works well". Claude wrote tests around the bugs that had actually shipped, and proved each one by putting its bug back and watching the test fail. The project went from 3 tests to 38 unit tests and 11 browser tests. The search field got fixed next, then the tests started running on GitHub on every push. The last commit of the day landed at 13:45.

The bug class, not the bug

The four dark-mode bugs of 8 September looked like four problems. Three of them were one. The old styling library worked out each colour in JavaScript, and in several places it did so without knowing which theme was on, so it fell back to the light one. The fourth, the author name, had never been given a colour at all. Each fix patched one more place. The next morning I found a link in the login box you could click but not see. It was the same bug again, in a place nobody had found yet.

The rewrite made that bug impossible instead of fixing it again. Colours are now plain CSS: one set of names, defined twice, and a class on the page picks which set applies. A component asks for "the surface colour" and never knows which theme is on. Claude put it this way: "That failure mode no longer exists."

Two things made that the right call rather than just a bigger change. The job was small enough to check: a few dozen files and a site I could click through in minutes. And I checked it by using it, not by reading the diff. The rewrite brought its own new bugs, and I found all three that way.

What we got wrong

  • The old image store had stopped serving files without a billing plan. The logs first show it when the migration's first copy failed.
  • Codex said early on that R2 can bill beyond its free tier, and built on it anyway. I only took that in when the sign-up screen said so.
  • "Fix everything at once" produced a large change I never committed. The next attempt's first phase added files I couldn't explain, and I reverted that too.
  • I asked about missing borders on every card. It was mostly my screen: "oh dont mid it was the screen hat i used". We made the borders stronger anyway, and that change shipped the same day.
  • A tidy-up squashed the logo animation, and wrapping the page in a new container made the feed load every post at once.
  • A security pass done in one go broke the live site, so everything from that evening was reverted.
  • An image cleanup kept unposted images instead of removing them. Reverted.
  • Dark mode shipped with invisible icons, unreadable profiles and a white box on a dark page.
  • The TypeScript rewrite crashed on scroll, squashed the logo, and flashed an empty feed before loading.
  • Claude's own new focus style drew a box inside the search field, the thing its previous fix had just removed.

Who did what

Codex did most of the year's work: 7 sessions from June to August, and 19 commits made inside them. Eight more commits from June have no session on record, so I don't count them for either tool. I sent it 203 messages and 33 screenshots. It replied 786 times: 291 from GPT-5.5 in June, 378 from GPT-5.6 Sol in July and 117 from GPT-5.6 Terra in August. It made 1,996 tool calls, not counting 1,142 waits on commands that were still running, and worked for about 15 hours and 45 minutes. Codex's automatic reviewer also checked its riskier actions 107 times. It moved the images, did the July modernisation, and the August security work.

Claude Code had one session over two days in September, all on Claude Opus 5. I sent it 25 messages and 11 screenshots. It replied 509 times and made 484 tool calls: 284 commands, 76 file edits and 118 actions in a browser, in about 3 hours and 45 minutes. That includes the first 25 minutes, spent on the other repositories' descriptions. It made 16 commits: the README, dark mode, the TypeScript rewrite, the tests and CI. Three more commits carry Claude's name, on 29 August and 12 September, from sessions this count doesn't include.

@unicorn by the numbers. Since March 2021: 369 commits on 81 days. In 2026, the only part with AI logs: 46 commits on 9 days (UTC+7). The TypeScript rewrite was merged 80 minutes after I asked, and tests went from 3 to 38 unit and 11 browser tests. Codex: 203 messages, 786 replies, 1,996 tool calls, 19 commits in logged sessions, plus 8 June commits with no session on record. Claude Code: 25 messages, 509 replies, 484 tool calls, 16 commits. Me: 228 messages, typically 9 words, 44 screenshots.

Counted from git and the 2026 Codex and Claude Code session logs. 2021 to 2025 has git only.

Download
Alt text · 498 characters

@unicorn by the numbers. Since March 2021: 369 commits on 81 days. In 2026, the only part with AI logs: 46 commits on 9 days (UTC+7). The TypeScript rewrite was merged 80 minutes after I asked, and tests went from 3 to 38 unit and 11 browser tests. Codex: 203 messages, 786 replies, 1,996 tool calls, 19 commits in logged sessions, plus 8 June commits with no session on record. Claude Code: 25 messages, 509 replies, 484 tool calls, 16 commits. Me: 228 messages, typically 9 words, 44 screenshots.

My part was deciding and looking. 228 messages across both tools, a typical one 9 words long, and 44 screenshots. I chose storage that stops over storage that bills, kept the approval step, and threw away work I didn't understand. I also asked for tests and noticed what looked wrong. Most of the bugs in the list above were found by me using the site.

Treat the numbers as a record, not a benchmark. The two tools worked in different months, on different jobs, and log their work differently.

What it cost

Money was the reason the storage moved, so it is the one cost I watched. The images now sit on Cloudinary's free plan. As Codex read the terms, it asks you to upgrade rather than charging, and a free Cloudflare Worker stops at its daily limit instead of billing. Billing on the old image store was on for 13 minutes on 6 June, long enough to copy 233 files, and then switched off.

The tools ran on subscriptions. Codex's meter showed 95% of its five-hour allowance on 6 June, and 100% of its weekly allowance at 17:09 on 20 July, before a new week started three minutes later. The Claude session shows no limit being reached.

What I would tell someone starting

  • Decide what failure you can live with. "Stop working" instead of "charge me" picked the storage in one sentence.
  • Ask for a plan, then take it in small steps. One large pass gave me a change I couldn't review. Small phases gave me one I could.
  • Throw away work you don't understand, or that breaks. Three times I reverted AI work the same day: once because I couldn't follow it, once because the live site broke, once because a fix backfired. Each redo was smaller.
  • When the same bug keeps coming back, fix the cause. Four dark-mode fixes in one night showed why the old styling library had to go.
  • Use it after every change. The crash, the squashed logo and the empty-feed flash were all found by clicking around, not by the checks.
  • Make tests prove they work. Put the bug back and watch the test fail.
  • Old projects are good for this work. A small, real app with real history is an honest test of what these tools can and can't do.

In one paragraph

@unicorn started in March 2021 with two developers and a lot of commits called "updated". In 2026 two AI tools picked it up. Codex moved its images to a free plan that, as we read the terms, stops rather than bills. It modernised the app step by step, and did a security pass that I reverted and redid one change at a time. Claude found dark mode hidden in the code, switched it on, and the next day rewrote the app in TypeScript in 80 minutes, with tests built around the bugs I found. That was 46 commits on nine days (UTC+7), about 19 and a half hours of counted AI work, and 228 messages from me. The tools wrote the code. What it should cost, who may post, and which changes to throw away were my calls.

See it at atunicorn.io. The code is private. Built with OpenAI Codex (GPT-5.5, GPT-5.6 Sol and GPT-5.6 Terra) and Claude Code with Claude Opus 5.