Currently building: himasjid · Week 16
Perth, AU · GMT+8 00:00:00
Back to work
Project: Self-initiated · Open source· Role: Solo builder· Timeframe: Mar 2026 — Present

Caretbox.

A personal knowledge hub for developers. One place for snippets, commands, notes, prompts, files, and images — organised into collections, AI-tagged on save, open on the second monitor.

Status

Live on Vercel · caretbox.vercel.app ↗

Stack

Next.js 16 · React 19 · Tailwind v4 · Prisma 7 · Postgres · NextAuth v5 · R2 · OpenAI

Snippets/signed-upload-url.ts
typescript ★ pinned + New item
signed-upload-url.ts ⧉ Copy
// presigned R2 upload · 15 min expiry
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

export async function presign(key: string, type: string) {
  const cmd = new PutObjectCommand({
    Bucket: env.R2_BUCKET,
    Key: key,
    ContentType: type,
  });
  return getSignedUrl(s3, cmd, { expiresIn: 900 });
}
#upload #r2 #s3 Added 6 Apr · edited 2 days ago
Recently added
⟨⟩tanstack-query-retry.ts2h
shipping notes · week 141d
›_psql reset sequence3d
Pinned
code-review prompt · senior
⟨⟩signed-upload-url.ts
prisma adapter for nextauth
§ 01 — Context

I kept losing my own code. The snippet I wrote in January to sign an R2 upload URL was buried three branches deep in a client repo by March. The prompt I spent a week refining was in a draft message I couldn’t find. The command I use to reset a Postgres sequence lived in shell history until I opened a new terminal.

The obvious answer was an existing tool. I tried a handful — the note apps are the wrong shape for code, the snippet managers don’t handle notes, the bookmark tools don’t handle files, and none of them let me drop a prompt and a snippet and a screenshot into the same collection. I decided to build the thing I was describing.

caretbox is that thing. A personal knowledge hub with seven item types — snippets, commands, notes, prompts, files, images, URLs — grouped into collections, AI-tagged on save, searched by tag, pinned when I actually reach for them weekly.

A tool I use myself, written to the standard I'd use to review someone else.
§ 02 — Constraints

This is an open-source side project that exists partly to solve a real problem and partly as a showcase of how I write code when I am the only reviewer. That second constraint is the one that shaped most of the decisions — it means no shortcut I wouldn’t ship into a client repo, no half-finished feature on the main branch, and a test suite that exists even though nobody is going to run it in CI but me.

Team size
One. Weeknights.
Budget
Free tiers only — Vercel, Neon, R2, Resend, OpenAI.
Intended user
Me. If it’s useful to anyone else, that’s upside.
Quality bar
Readable by a senior engineer opening the repo cold. No commented-out code, no TODOs on main.
Scope
Seven item types. Collections. Tags. Search. Auth. AI tagging and summaries. No collaboration, no sharing, no public links — on purpose.
§ 03 — Architecture

The interesting shape here is how little there is of it. One Next.js app, one Postgres database, one object store, one email provider. Every page is a server component that loads its own data. Every mutation is a server action. There is no API layer because there is no second client.

Auth is NextAuth v5 with the Prisma adapter, supporting GitHub OAuth and email-password credentials. The password-reset flow reuses the VerificationToken model with a pwd-reset: prefix and a one-hour expiry — which was one of those decisions I made and then talked myself out of twice before coming back to it because the simpler model was already correct.

File and image uploads go to Cloudflare R2 through an S3-compatible presigned URL, so the Next.js app never proxies file bytes. A download proxy route streams the object back through the app only when access control matters, so most reads go direct from R2 to the browser. It’s the kind of thing that doesn’t matter at my traffic level and won’t matter for a long time, but it was cheap to do right the first time.

OpenAI sits alongside these as a thin service layer. Item creation can optionally trigger a completion request that proposes tags and writes a one-line summary back before returning. Code items get an explanation endpoint that runs on demand. Neither is on the critical path — the library works without a network call to OpenAI, and the AI features degrade gracefully when the key is missing or the request fails.

CLIENT Browser DESKTOP · ONE USER AT A TIME APP Next.js 16 · App Router SERVER ACTIONS · RSC · NO SEPARATE API DATA · AUTH · STORAGE · AI Postgres PRISMA 7 · NEON NextAuth v5 GITHUB OAUTH · CREDS R2 FILES · IMAGES · PRESIGNED Resend RESET MAIL OpenAI TAGGING · EXPLAIN LIME = CRITICAL PATH · PRISMA + RSC
Fig. 01 — System diagram. Single-user app, single database, no API layer to maintain.
§ 04 — Stack & reasoning
Next.js 16
App Router with server components and server actions removed the need for a separate API layer. Every mutation is a typed function call from the component that triggers it.
React 19
Most pages are server components with no client-side state. The few that need interactivity use the new Actions API rather than useState + fetch — fewer round-trips, no loading spinners for most mutations.
Tailwind CSS v4
The v4 rewrite dropped the config file entirely — CSS-native variables, no PostCSS pipeline. Utility classes keep styles co-located with markup and make component-level overrides obvious at a glance.
shadcn/ui
Pre-built component primitives (dialogs, drawers, command palette) that live in the repo as editable source, not a black-box dependency. Each component is owned code — modified to match the design without fighting a theme system.
Prisma 7 · Postgres
End-to-end typed queries. Migrations I can read in git log. Neon’s branching made schema iteration cheap enough to keep doing.
NextAuth v5
GitHub OAuth and credentials in one library. The Prisma adapter gives me the account, session, and verification-token models without writing them myself.
Monaco · react-markdown
Code items get Monaco with a language label and a copy button; notes and prompts get a markdown editor with a preview tab. The right editor for the right content type, not a single compromise box.
Cloudflare R2
S3-compatible, no egress fees, presigned URLs so the Next.js app never proxies upload bytes. Cheap enough to not think about at my scale.
OpenAI
Powers the optional AI layer. Item save can auto-propose tags and write a one-line summary; code items can request an explanation on demand. Wrapped behind a single service module so the SDK never leaks into application code, and every call degrades gracefully if the key is missing.
Vitest
32 tests focused on server actions and auth boundaries. The class of bugs I can’t catch alone — ownership checks, Zod validation, typeId tampering — have tests. UI presentational code does not.
§ 05 — Status

Live on Vercel, used daily, still under active build. The core loop — create an item, tag it, find it later — works. Collections, item drawers, inline edit, favourite and pin, image gallery, file upload, profile and usage stats, password reset: all shipped. AI tagging and summaries are live on item creation; code explanation is available on demand. Search is the next piece; right now the type-filtered views and tag chips do most of the finding.

It is also the repo I hand to anyone who asks for a code sample. Commits are squashed around shipped features, each with a paragraph describing what changed and why. There is a security-audit commit in the history that Zod-validated the registration route, added ownership checks to item creation, and capped a query at twenty rows — not because anyone asked for it, but because I’d review my own code and flag those.

§ 06 — Reflection

The thing I’m most happy with is the discipline around scope. Every feature starts as a one-line entry in a current-feature.md file I keep at the repo root. I work on exactly that one thing until it ships, then I reset the file. The commit history shows it — feat: add item drawer edit mode, chore: reset current-feature.md after completing item-drawer-edit — because the file is part of the workflow, not a document I’m pretending matters. It’s the closest thing I have to a product manager when I’m building alone.

The thing I’d do differently is pick a single editor primitive on day one. I shipped Monaco for code and a custom textarea-plus-preview for markdown and they’re now two slightly different mental models of what an editor is. Next refactor merges them behind one <ItemEditor> and stops pretending the two paths are unrelated.

Next case study

Copypara

A freemium SaaS that strips AI tells from generated text so it reads like a person wrote it.