유대선
프로젝트로
·기술 회고·3 ·리뷰 필요

Every Stack Decision: What, Why, Alternatives, and the Production Switch Point

A rundown of every tech-stack decision made in the 버팀목 MVP, with rationale, alternatives, and production switch points. The core pattern: 'put local defaults behind an interface, and swap in production keys by changing just one file.'

AI 버전

work.md §16 locked in the stack, and on top of that, to achieve "run instantly with 0 external accounts," each item was filled in with a local implementation behind an interface. The why and the production switch point of each decision:

Framework — Next.js 16 (App Router)

Full-stack (UI+API+RSC). A spec-locked item. React 19, Tailwind v4. Turbopack for both dev/build.

DB — Drizzle + PGlite (embedded PostgreSQL)

  • Why: The spec locks in PostgreSQL. PGlite is WASM PostgreSQL, so it runs instantly with 0 external DB servers while keeping "PostgreSQL." Drizzle has a drizzle-orm/pglite driver.
  • Alternatives: better-sqlite3 (spec deviation — SQLite), Docker Postgres (external dependency).
  • Switch point: Just replace lib/db/index.ts with drizzle-orm/node-postgres + DATABASE_URL. Schema/queries stay the same.
  • Caveat: As a single-connection WASM, it's vulnerable to concurrency/corruption → detailed in a separate entry (pglite-embedded-db). Migrations use runtime CREATE TABLE IF NOT EXISTS (lib/db/init.ts) instead of drizzle-kit, so it works immediately on first boot.
  • Why: The spec is Better Auth + Kakao/Apple. But social login needs real keys → per the "don't stop" guideline, an HMAC-signed session cookie + name/email dev login was placed behind the lib/auth interface. Session is 180 days (§15 "seamless re-login").
  • Alternatives: Better Auth from the start (no social without keys, setup friction), managed (Clerk etc. — outsourcing identity = trust concern).
  • Switch point: Replace while keeping the getCurrentUser/login/logout signatures. Later, integrated Kakao OAuth directly (see kakao-oauth-debug) — exactly the spec's "Kakao via generic OAuth."
  • Security: Dev login is fully disabled in production via a NODE_ENV guard (ed3600c).

Storage — local file driver (behind an interface)

  • Why: §13 "no public URLs, owner only." The StorageDriver interface in lib/storage + LocalStorageDriver (.data/media) + an auth-gated route (/api/items/[id]/media) control access via cookie sessions. Includes path-traversal prevention.
  • Switch point: Add S3StorageDriver (R2/S3 private bucket + signed URLs); as long as put/get/delete are honored, the route stays the same.

PWA — hand-written Service Worker (not Serwist)

  • Why: The spec is Serwist or manual. For Turbopack compatibility and full control, chose a manual public/sw.js (app-shell cache · push · notificationclick). Reliability first.
  • Alternatives: Serwist (Turbopack edge-case concerns), next-pwa (archived/unmaintained).
  • Includes the manifest + iOS 'Add to Home Screen' guidance.

Web Push — VAPID auto-generation

  • Why: VAPID keys can be self-generated (no external account needed). lib/env.ts auto-generates and stores them in .data/secrets.json → Android/desktop push actually works.
  • Switch point: Production fixes them via VAPID_* environment variables (applied per key individually — ed3600c).

Scheduler — node-cron + instrumentation

  • Why: On a persistent Node runtime, evaluate every minute → Web Push when a reminder time matches. Started once in instrumentation.register().
  • Caveat (future): In-memory dedupe is inaccurate across multiple instances/restarts → needs a persistent marker (recorded in PROGRESS).

Audio — best-effort server transcoding

  • Why: §12 webm→mp4 (guarantees iPhone playback). But ffmpeg isn't on this machine → convert if present, otherwise store the original + log it (lib/audio.ts). MIME is not hardcoded; candidates are selected in sequence via MediaRecorder.isTypeSupported().

Icons — pure Node PNG encoder

  • Why: Need to generate icons without external image tools (sharp/ffmpeg/imagemagick). scripts/gen-icons.mjs encodes PNG directly with zlib + its own CRC32 (a cream-colored heart inside a warm clay disc). 192/512/maskable/apple-touch.
  • The same encoder pattern was later reused for sample-photo generation (gen-sample-photo.mjs) and the data-export ZIP (lib/zip.ts, store mode).

One-line pattern

Put local defaults behind an interface, and production becomes a matter of plugging in keys and replacing just one implementation file. The spec's "don't stop — record and proceed" was realized through this pattern.

리뷰 필요

내 시각이 아직 안 들어간 entry.