This file provides guidance to Codex and Claude Code when working with code in this repository.
Seen is an iOS-only app — a Letterboxd-style app. There is no web target and no Android target for now. Do not add web-only code, .web.* files, or Android-specific branches.
The UI is built primarily with Expo UI + SwiftUI (@expo/ui/swift-ui) to be fully native, falling back to plain React Native views only when no Expo UI equivalent exists.
This is an Expo SDK 56 project (React Native 0.85, React 19.2). SDK 56 introduced breaking changes. Read the exact versioned docs before writing any code — do not rely on memory of older Expo APIs:
- Expo SDK: https://docs.expo.dev/versions/v56.0.0/
- Expo UI SwiftUI: https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/
Mandatory: when building or editing any UI, follow the rules in the expo-ui-swiftui skill (.agents/skills/expo-ui-swiftui/SKILL.md), which encode the required Expo UI conventions from https://codewithbeto.dev/blog/expo-ui-tips (e.g. always pass matchContents and an explicit ignoreSafeArea on <Host>, centralize component wrappers, use platform colors).
Package manager is bun (bun.lock). Use bunx/bun rather than npm/npx.
bun start/bunx expo start— start the Metro dev server (loads.env.development)bun ios— start targeting the iOS simulator (the only supported target)bun run lint—expo lint(ESLint)bunx tsc --noEmit— typecheck (strict mode is on)bun run reset-project— moves starter code aside and scaffolds a blankapp/(rarely needed)
There is no test framework wired up.
The app talks to the local Seen API, selected by environment file (loaded automatically by Expo):
.env.development→ local Bun/Elysia API (bun run dev:api) and local Postgres/Redis dependencies..env.production→ hosted API endpoint used for production builds.
Client-visible values use EXPO_PUBLIC_*; never put provider secrets or database credentials there.
The backend is apps/api — a Bun/Elysia service using Better Auth, Drizzle, Postgres, Redis, and S3. Database schema lives in packages/db/drizzle/ and packages/db/src/schema/; generate and run migrations through the root db:* scripts. Provider secrets stay server-side in the API environment.
Movie data comes from TMDB via the API module in apps/api/src/modules/tmdb/, which proxies TMDB and keeps TMDB tokens server-side. Follow the tmdb skill for any TMDB work (search/discover/find/detail, images, auth, cache).
iOS-only Expo Router (file-based routing) app. Source lives under src/ and is imported via the @/* alias (→ src/*), with @/assets/* → assets/*. Typed routes and the React Compiler are both enabled (app.json → experiments).
Authentication state drives routing through a single context, not imperative redirects:
src/lib/auth-client.tscreates the Better Auth Expo client and configures secure client-side session storage.src/providers/auth-provider.tsxreads the Better Auth session and exposes{ session, user, isLoading, isLoggedIn }viaAuthContext(src/hooks/use-auth-context.tsx).src/app/_layout.tsxreadsuseAuthContext()and uses<Stack.Protected guard={...}>to gate route groups:(tabs)when logged in,(auth)/loginwhen not. Signing in/out flipsisLoggedInand the guard handles the redirect — screens should not navigate manually on auth change.src/components/splash-screen-controller.tsxholds the native splash screen untilisLoadingis false (session restore finished).
When adding screens that require auth, place them under a group guarded in _layout.tsx rather than checking auth inside the screen.
Route files under src/app/ hold no UI — they only map a URL to a screen and own route-level concerns. The actual screen (UI + logic + StyleSheet) lives in a flat file src/components/screens/<screen>.tsx (kebab-case) as a named export with the bare feature name (Home, Explore, Login — not *Screen).
The route file is a thin default-export component that renders the screen:
// src/app/(tabs)/index.tsx
import { Home } from "@/components/screens/home";
export default function HomeScreen() {
return <Home />;
}The wrapper (rather than a bare export { default } from) is deliberate: it lets the route add route-only config around the screen — e.g. <Stack.Title> / <Stack.SearchBar>, or wiring a route param into a store. When adding a screen: create src/components/screens/<name>.tsx exporting export function <Name>(), then add the thin route file under the appropriate (guarded) group in src/app/. Keep all components, hooks, and styles in the screen file, not the route.
Client state uses Zustand, split into small feature stores under src/store/ (kebab-case use-<feature>-store.ts, actions suffixed Action). Persisted stores use the shared expo-sqlite/kv-store adapter in src/store/storage.ts (createJSONStorage(() => Storage)); transient UI/form state uses plain create(...) with no persist. See the state-management skill for the full conventions.
Boundary rule: the API/database is the source of truth for server data, and Better Auth owns the auth session. Zustand persistence is for client/UI state and offline cache only — never re-store auth sessions, tokens, or secrets in a Zustand store.
Principal actions must give haptic feedback (expo-haptics). Always go through the semantic helpers in src/lib/haptics.ts (hapticTap, hapticSuccess, hapticError, hapticDelete, hapticSelection, hapticWarning) — never call expo-haptics directly in components. Decide by intent: primary button/confirm → hapticTap; delete → hapticDelete; tab/page switch, toggle, picker → hapticSelection; async result → hapticSuccess/hapticError. Tab switches are already wired via NativeTabs screenListeners in src/components/app-tabs.tsx. Helpers are iOS-guarded and fire-and-forget; haptics don't fire on the simulator. See the haptics skill.
src/constants/theme.tsdefinesColors(light/dark),Fonts(per-platform), and aSpacingscale (half=2 …six=64). PreferSpacing.*tokens over raw pixel values.useTheme()(src/hooks/use-theme.ts) returns the active color set;useColorScheme()has a.webvariant. Build UI withThemedText/ThemedViewand passthemeColor/typeprops instead of hardcoding colors.- Tabs use the new
expo-router/unstable-native-tabs(NativeTabs) insrc/components/app-tabs.tsx. - For the broader design system (design tokens, a Tailwind-style color palette with shades, and a
variant/color/sizecomponent API viauseVariantConfig), follow thedesign-systemskill. Reusable components are SwiftUI-first (built on@expo/ui/swift-ui, perexpo-ui-swiftui) but consume the shared tokens. Note: the design-systemSPACINGscale (XS..XXL) andtheme.ts'sSpacing(half..six) overlap — pick one before scaling the UI up.
The starter left behind some *.web.tsx/*.web.ts files (e.g. app-tabs.web.tsx, animated-icon.web.tsx, use-color-scheme.web.ts) plus animated-icon.module.css. Web is not a supported target — do not add new .web.* files and do not spend effort maintaining the existing ones. Prefer .ios.tsx platform extensions when a native-only variant is genuinely needed.
- Naming is governed by the
namingskill (.agents/skills/naming/SKILL.md) — the single source of truth for which word to use per concept (media/movie/tv,rating/review,profile/user;noteandfilmare banned) and where each kind of file lives. Read it before naming or renaming anything. - Files are named kebab-case; the path alias
@/is used for all internal imports. - User-facing screen text is in English. (The legacy login screen is still in French and is being migrated.)
- Build UI with Expo UI SwiftUI per the
expo-ui-swiftuiskill; reach for plain RN views only when there's no native equivalent. - Source layout under
src/:app/(routes),components/(+components/ui/),hooks/,providers/,lib/,constants/, andstore/(Zustand). Addtypes/andutils/when those layers are introduced (typed API client, shared helpers).
Follow the global File structure & complexity rules: one unit per file, extract reusables, analyze before writing, avoid duplication, keep files short and flat, and group siblings into folders. The codebase has accumulated files that are too long, complex, and duplicated — keep it small, flat, and DRY.
Seen-specific concretions of those rules:
- Files are kebab-case; a single primary export per file.
- Reusable UI/logic goes in
components/,components/ui/, orhooks/. - When a feature has 2+ siblings, co-locate them: several hooks →
hooks/<feature>/(use-*.ts), a split screen →components/screens/<screen>/(withindex.tsx), backend client code →services/<feature>/handlers/.