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 version
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/pglitedriver. - Alternatives: better-sqlite3 (spec deviation — SQLite), Docker Postgres (external dependency).
- Switch point: Just replace
lib/db/index.tswithdrizzle-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 runtimeCREATE TABLE IF NOT EXISTS(lib/db/init.ts) instead of drizzle-kit, so it works immediately on first boot.
Auth — session-cookie dev login (behind an interface)
- 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/authinterface. 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/logoutsignatures. Later, integrated Kakao OAuth directly (seekakao-oauth-debug) — exactly the spec's "Kakao via generic OAuth." - Security: Dev login is fully disabled in production via a
NODE_ENVguard (ed3600c).
Storage — local file driver (behind an interface)
- Why: §13 "no public URLs, owner only." The
StorageDriverinterface inlib/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 asput/get/deleteare 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.tsauto-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 viaMediaRecorder.isTypeSupported().
Icons — pure Node PNG encoder
- Why: Need to generate icons without external image tools (sharp/ffmpeg/imagemagick).
scripts/gen-icons.mjsencodes 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.
Review needed
No human review on this entry yet.