← Blog
8 min read

Building Mirror, Part 2: Engineering

Spring Boot on AWS, MongoDB Atlas, Auth0, Terraform infrastructure, OpenAPI contracts, and the engineering choices behind Mirror's backend.

mirrorengineeringspring-bootawsseries

Part 2 of the Mirror series. Part 1 covered the company. This one is the stack - what I actually built and why.

Mirror's mobile client talks to a Spring Boot backend on AWS. I'm a Java developer by trade, so reaching for Spring wasn't nostalgia - it was speed with tools I trust.

Architecture at a glance

Mobile app (React Native / Expo)
        ↓ Auth0 login              ↓ HTTPS API calls
                                    API Gateway (only public entry)
                                          ↓ VPC link
                                    Internal NLB → ECS (Spring Boot)
                                          ↓              ↓
                                    MongoDB Atlas    S3 ← CloudFront (signed URLs)

Infrastructure defined in Terraform. Containers built with Dockerfiles, deployed through CI.

The goal: boring, observable, secure enough for wardrobe photos and personal data without ops becoming a second job.

Spring Boot: the backend core

Spring Boot is the spine of Mirror's API layer.

Why it fit:

  • Mature ecosystem for validation, data access, and HTTP APIs - plus solid JWT integration for Auth0.
  • Fast to ship - I know the patterns; I'm not learning a framework while learning a market.
  • Security libraries I already trust from enterprise work.
  • Testability - integration tests against real-ish components without theater.

The API handles wardrobe entities, outfit composition, image upload orchestration, and sync. Auth0 owns login and token issuance; Spring validates JWTs and enforces authorization on every request. Standard REST JSON for now - debuggable at 7am when something breaks.

Spring HATEOAS - tried it, disabled it

I initially added Spring HATEOAS for hypermedia-driven responses. In theory, clients discover actions through links. In practice, for a mobile app with a known contract, it was overkill.

The mobile client doesn't need to crawl links. It needs predictable endpoints. HATEOAS added payload complexity and mental overhead without buying us flexibility we were using.

So I disabled it. Good decision. Not every enterprise pattern earns its place in a startup API.

Auth0: login without building identity from scratch

Mirror uses Auth0 for user authentication. The mobile app runs the Auth0 login flow; the backend never sees passwords.

Why Auth0:

  • Ship login fast - social providers, email/password, password reset, MFA hooks without writing auth plumbing.
  • Security defaults - token lifetimes, refresh flows, and breach response handled by people who do this full time.
  • Spring integration - JWT validation in the API layer; claims map cleanly to user identity for AuthZ checks.

I'm a security engineer. I still didn't want to be the person maintaining login UX and credential storage for a consumer app. Auth0 keeps identity out of the critical path I have to babysit.

MongoDB Atlas: wardrobe data as documents

Wardrobe data is naturally nested - items, tags, outfits, references between them. MongoDB Atlas fits that shape without forcing everything into relational joins on day one.

Why MongoDB Atlas:

  • Managed service - backups, patching, and scaling without me running a database.
  • Flexible schema - product iteration doesn't mean migration Fridays every week.
  • Spring Data MongoDB - familiar repository patterns from my Java background.
  • Network isolation - IP allowlists and VPC peering options so the cluster doesn't greet the public internet.

Images still live in S3. MongoDB stores metadata, relationships, and the pointers - not the binary blobs.

OpenAPI: contract first, then code

I generate and maintain OpenAPI specs from the Spring implementation (and treat the spec as the contract mobile depends on).

Why this matters:

  • Mobile and backend can evolve in parallel without silent drift.
  • Request/response shapes are documented where both sides can see them.
  • Future clients (web, integrations) get a head start.
  • Reviewers - including future me - can understand the API without reading every controller.

For a solo-ish backend with a mobile client, the spec is the handshake. Break it knowingly, not accidentally.

AWS infrastructure

The backend runs on AWS, provisioned with Terraform (split into core and services stacks so long-lived resources like storage and secrets stay isolated from disposable compute).

Why Terraform:

  • Infrastructure as code, reviewable in PRs.
  • Reproducible environments - dev/staging/prod don't drift into snowflakes.
  • I'm one person; I can't manually click-console my way to consistency.

How traffic and access are wired

The design principle is simple: one public door for the API, everything else private.

  • API Gateway is the only internet-facing entry point for backend traffic. Requests reach the app through a VPC link - not by exposing containers directly.
  • NLB and ECS live in private subnets. Tasks have no public IPs; outbound traffic (image pulls, logs, calls to Auth0 or MongoDB Atlas) goes through a NAT gateway.
  • Security groups sit between each hop - API Gateway to load balancer, load balancer to containers - so traffic follows a narrow path instead of wandering the VPC.
  • S3 holds wardrobe photos with public access blocked. A VPC endpoint lets containers reach the bucket without routing storage traffic over the public internet.
  • CloudFront sits in front of S3 with origin access control - the bucket is not browsable directly. The API hands out short-lived signed URLs when the app needs to display or upload an image.
  • SSM Parameter Store holds application secrets (Auth0, database connection strings, signing keys) as encrypted parameters injected at container startup - not baked into images or git.
  • IAM task roles grant the running service only what it needs for S3, logging, and parameter access.

MongoDB Atlas and Auth0 sit outside AWS but follow the same idea: managed services, network restrictions, no credentials in source control.

Terraform state is treated like production data. Because it is.

Dockerfiles and deployment

The Spring Boot app ships as a Docker image - multi-stage Dockerfile, non-root user where possible, minimal base image, health check endpoint exposed.

Build pipeline:

  1. Test + lint.
  2. Build JAR.
  3. Build and push image.
  4. Deploy to AWS environment.

No Kubernetes cosplay. At our scale, managed container hosting beats running a cluster I have to babysit.

Mobile client (briefly)

The app is React Native with Expo - single codebase for iOS and Android while we prove the product loop. Part 2 is backend-focused, but the split is clean: mobile owns UX and local caching; server owns authoritative data and media URLs.

Security habits that carried over

Building a closet app doesn't mean turning off my security brain:

  • Auth0 for identity; Spring validates every JWT before business logic runs.
  • Layered network access - public API Gateway, private compute, no direct path from the internet to containers or storage.
  • Signed CloudFront URLs with tight expiry for media access - the bucket itself stays closed.
  • AuthZ checks on every object reference - no security-through-obscurity IDs.
  • Input validation on uploads - MIME checks, size limits.
  • Secrets in SSM Parameter Store, not in git or container images.
  • Dependency scanning in CI.

I threat-modeled wardrobe photos the same way I'd threat-model any PII-adjacent data. Bedrooms appear in backgrounds. Treat it seriously.

What I'd do differently

Honest postmortem notes:

  • HATEOAS - already mentioned. Shipped curiosity, removed weight.
  • Almost over-engineered sync before users existed. Simpler sync first would have been fine.
  • Underestimated image retry UX on mobile - backend was ready before client failure states felt good.

What's next (engineering)

Near-term:

  • Wear logging and smarter repetition avoidance.
  • Weather-aware suggestions with respectful location permissions.
  • Improved upload pipeline resilience.

Medium-term:

  • On-device tagging assistance.
  • Export/portability - your wardrobe data is yours.

The bill nobody puts in architecture diagrams

I worried about AWS costs going in. NAT gateways, load balancers, container hours - the classic "your side project is $40/month because you forgot the NAT exists" trap.

Turns out AWS was the cheap part. The whole stack - API Gateway, private networking, ECS, S3, CloudFront, logs - runs me somewhere between $50 and $100 per month. Annoying for a pre-revenue startup, but predictable. I can model it. I can turn things down.

Auth0 is what kept me up at night.

Auth0 charges per monthly active user - roughly $0.07 per MAU on the tier we're on. That doesn't sound scary until you do the math for a consumer app. AWS scales with traffic and storage. Auth0 scales with every person who logs in, whether they upload one shirt or fifty.

Rough monthly costWhat drives it
AWS (full stack)$50-$100Fixed infra + modest usage
Auth0$0.07 × MAUsEvery active user, every month

At 1,000 active users, Auth0 alone is ~$70 - already in the same ballpark as the entire AWS bill. At 10,000, it's ~$700. The backend could stay cheap while identity eats the budget.

I still chose Auth0. Building login, password reset, social providers, and breach response myself would cost more in time and risk than the MAU line item - at least at our current scale. But if you're copying this stack, price Auth0 before you price ECS.

Why this stack

Mirror engineering isn't about impressing anyone on Hacker News.

It's Spring Boot + AWS + MongoDB Atlas + Auth0 + Terraform + OpenAPI + Docker - tools I can ship with, observe, and secure - supporting a mobile app that needs to feel fast and trustworthy on Tuesday mornings.

Part 3 covers marketing - the part no Dockerfile solves.

If you're an engineer reading this: the stack is conventional on purpose. Startups die from novelty, not from Spring.

Same as every good product.