Skip to content

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 ESLint
  • adapters/ implemented ports with explicit mappers
  • registry.ts wired adapters to ports
  • presentation/ 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 className string 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.

Markdown remains the source of truth. Run yarn docs:check before handoff.