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.