v1 Learnings
Status: Archived
What we built in TwoMore v1 taught us. Captured to avoid repeating mistakes in v2.
Architecture — Keep
Hexagonal architecture worked
domain/had ZERO outer imports — enforced by ESLintadapters/implemented ports with explicit mappersregistry.tswired adapters to portspresentation/imported from@/registry, never adapters
Verdict: Port this verbatim to v2. The domain layer is the most valuable part of v1.
Signal system
75 signal types across 13 categories, pure resolver, PIPA-compliant signal_consent_log. Powered actionable home cards (dues, score verify, cancelled sessions).
Verdict: Port unchanged.
Tier system
7 tiers (Bronze → Grand Master). tier-display.ts as single source of truth for icons, colors, badge variants.
Verdict: Port unchanged.
createMutationHook factory
Wrapped useMutation with built-in haptic feedback, toast, optimistic update helpers. ESLint rule blocked raw useMutation.
Verdict: Port unchanged.
Supabase + RLS + Edge Functions
114 migrations. Every new table had RLS enabled + policies + trigger protection in the same migration. Rock solid.
Verdict: Port unchanged.
i18n split by domain
9 domain modules (ko/home.ts, ko/sessions.ts, etc.) instead of one monolith. All strings via t(). Code + comments in English.
Verdict: Port unchanged.
Architecture — Reconsider
14-state home classifier
Built an elaborate state machine (14 baseline states + 3 overrides + 20 modifiers + 7 relationships) to drive the home tab. Ended up forcing density — every state had to fill hero + supp + tertiary slots, even when there was nothing to say.
Fixed in final v1 week: replaced with a Toss-style feed of 10 independent cards. Each card decides its own visibility; returns null when nothing to say.
Verdict for v2: Never build a classifier for feed-style UI. Cards own their visibility.
Custom state engine (computeUserState)
3-dimensional state engine that was eventually replaced by the simpler feed pattern.
Verdict for v2: Keep computeGreeting (pure function for the greeting text), drop the rest.
Screen-level "zones"
HomeTemplate had 4 fixed zones (anchor/context/momentum/community). Again forced density.
Verdict for v2: Screen layout is just JSX in the screen component. No zone abstraction.
Styling — Why We're Rebuilding
NativeWind was the wrong anchor
- Token contract was weakly enforced —
className="p-[13px]"bypassed the system every day - Every session we invented new spacing values (4px, 6px, 8px, 12px, 14px, 16px, 20px, 24px) by feel
- No compile-time CSS extraction for web — would have been a separate effort for the web app
- Types on
classNamestring were loose (no autocomplete for tokens without extra plugins)
Verdict for v2: Tamagui with type-enforced createTokens(). No arbitrary values allowed.
Custom hand-rolled components
Built custom HomeCard, Card variants, Button variants by writing style dictionaries from scratch. Each component's variant logic lived in its own file.
Verdict for v2: Use Tamagui's styled() + variants pattern. One unified way to define variants.
Theme system via React context
useThemeColors() returned a nested object of colors. Worked, but required hooks in every styled component.
Verdict for v2: Tamagui handles theme switching via CSS variables on web and ShadowTree updates on native. No hooks required in leaf components.
Mixing of approaches
NativeWind className + inline style={{}} + useThemeColors() for dynamic values. Three ways to style. No enforcement on which to use when.
Verdict for v2: Tamagui styled() + $token inline props. Two APIs but with clear rules (styled for primitives, inline for one-offs).
UX — Korean Market Patterns Learned
Bottom sheet > modal
Everywhere we used a modal, users expected a bottom sheet. Korean apps use sheets for selection, detail, confirmation. Modals feel foreign.
Verdict for v2: @gorhom/bottom-sheet for all "reveal on tap" patterns. No <Modal> primitive usage.
Tab bar = flat + label + 5 items max
Tried a few variations. Ended up at: 5 items, labels always visible, filled icon on active, subtle top border. Matches Toss/당근.
Verdict for v2: Same, on day one.
FAB = rare
Tried FABs on multiple tabs. Most users missed them. Removed them eventually.
Verdict for v2: One FAB max (for the single most important creation action). Or zero — Toss has zero.
Greeting with name + context
Users loved the greeting belt that showed "안녕하세요, [name] · [context phrase]". Grounds them without demanding attention.
Verdict for v2: Port compute-greeting.ts verbatim.
Workflow — Keep
Pre-push hook
Ran typecheck + jest on every git push. Zero cost, instant feedback, replaced broken GitHub Actions.
Verdict for v2: Ship from day one.
Stop hook
Blocked session end without npm run check passing. Prevented a lot of "I forgot to verify" moments.
Verdict for v2: Ship from day one.
UserPromptSubmit hook
Injected token rules + terminology + blast radius + delegation rules every prompt. Kept behavior consistent across a long session.
Verdict for v2: Ship from day one.
Agent delegation rule
"4+ files = must delegate to Sonnet agent" was a late addition but made huge difference. Opus budget went 3x further.
Verdict for v2: Ship from day one.
Dev panel + seed scenarios
Being able to flip to any of 24 scenarios made visual QA fast.
Verdict for v2: Port unchanged.
Workflow — Mistakes
Not pinning library versions
Tamagui, RN, Expo all had surprise breaking changes at minor versions in v1. Pin exact versions.
Verdict for v2: Exact version pins on every dep. No carets.
Hand-wrote tokens instead of semantic tokens
v1 used spacing[4] (primitive, 16px) everywhere. No semantic layer. Every component decided "how much space?" independently.
Verdict for v2: Define semantic tokens (theme.gap.group, theme.gap.row, theme.gap.title). Type system forces you to pick from the menu.
Let dev scenarios drift from seed function
Scenario keys in dev-panel.tsx and seed-scenario/index.ts fell out of sync multiple times. Fixed by removing the VALID array (switch default handles unknown keys).
Verdict for v2: Same fix from day one.
Forgot to deploy edge functions
OTA only deploys client. Edge functions need supabase functions deploy. Forgot this multiple times.
Verdict for v2: /commit skill auto-deploys edge functions when supabase/functions/ files change. Ship this pattern from day one.
Long sessions without compaction
Some sessions went past 50 turns before compaction. Context quality degraded. Should have compacted proactively.
Verdict for v2: Compact after every phase boundary. Keep the compaction hook always-on.
Performance
No major RN perf issues in v1
114 migrations, 1100+ tests passing, 35+ adapters. Never hit a wall.
Verdict for v2: Same stack (Supabase, TanStack Query, Zustand) — don't optimize prematurely.
NativeWind had a theoretical perf tax
Community benchmarks showed ~4x slower than StyleSheet at scale. Never visible to users in our size, but would matter at 10x scale.
Verdict for v2: Tamagui's compiler-extracted CSS on web and flattened StyleSheet on native should be comparable to or faster than NativeWind.