Jul 2, 20267 min read

Building MemoryTree: What I Learned Shipping a Solo Full-Stack App

I built MemoryTree — a personal life-journaling app — as a way to go deep on full-stack fundamentals instead of just following tutorials. It organizes your life into Events (chapters like Travel, Career, Health), Memory Entries within those events, and Media attached to each memory. Along the way I hit a handful of problems that taught me more than any course would have.

Cross-Domain Auth Is Its Own Beast

My backend lives on Render, my frontend on Vercel — two different domains. Getting refresh-token cookies to actually persist across that boundary took longer than I expected.

The fix wasn't one setting, it was four that all had to be correct together:

res.cookie("refreshToken", token, {
  httpOnly: true,
  sameSite: "none",
  secure: true,
});

Plus CORS configured with the exact frontend origin and credentials: true on the server, and credentials: "include" on every frontend request. Miss any one of the four and cookies silently don't show up — no error, just a login that mysteriously doesn't stick.

Production Isn't Local, Even When the Code Is Identical

Email verification worked perfectly on my machine using Gmail SMTP. In production on Render's free tier, it just... didn't. No emails, no obvious error.

Turned out Render's free tier blocks outbound SMTP ports entirely. The fix was switching from Nodemailer to Brevo's HTTP API, which sidesteps SMTP altogether:

const brevo = new BrevoClient();
await brevo.transactionalEmails.sendTransacEmail({...});

Then Brevo's own security layer blocked requests from Render's IP until I explicitly whitelisted it. Two separate infrastructure quirks, stacked, that no amount of local testing would have surfaced.

A Reload Loop Taught Me About Interceptors

My Axios refresh-token interceptor used window.location.href to redirect on a failed refresh. On failure, it triggered a reload — which retried the request — which failed again — which reloaded again. A silent infinite loop.

The fix was to stop treating auth failure as a page-level event and treat it as an app-level one: dispatch a custom window event and let React Router handle the redirect. Small change, but it reframed how I think about side effects inside interceptors.

The Habit That Actually Helped: Three Questions, Three Files

Whenever I got stuck building a new route, I started forcing myself through the same ritual before writing code: what comes in, what happens, what goes out. Then I'd touch exactly three files — route, controller, query — starting with just the route definition. It sounds almost too simple, but it turned "I don't know where to start" into a repeatable checklist, every time.

What I'd Do Differently

  • Write the ownership-check logic (verifying a user owns a memory through its parent event) once, early, instead of duplicating checks across controllers
  • Set up Brevo and production email from day one instead of discovering the SMTP block after auth was "done"
  • Keep a running list of small inconsistencies (like a typo'd route path) instead of fixing them ad hoc — some are still flagged with a comment rather than corrected

MemoryTree is still evolving, but building it end-to-end — auth, uploads, deployment quirks and all — taught me more about how the pieces of a real app actually fit together than any single tutorial did.