Skip to content

Latest commit

 

History

History
78 lines (57 loc) · 27.1 KB

replit.md

File metadata and controls

78 lines (57 loc) · 27.1 KB

RealtimeAlgoChatApp Studio

RealtimeAlgoChatApp Studio is a real-time collaboration platform for developers with Build, Call, and Ship modes in a single room.

Run & Operate

  • pnpm --filter @workspace/api-server run dev — run the API server (port 5000)
  • pnpm run typecheck — full typecheck across all packages
  • pnpm run build — typecheck + build all packages
  • pnpm test:unit --run — root unit suite; it must pass on a fresh clone or new git worktree with no artifacts/api-server/test-results/ (gitignored Playwright output), so no step may require files from there — optional workspace files are guarded by an explicit, path-naming skip, and scripts/tests/check-native-large-text-evidence-fresh-checkout.test.sh enforces it
  • pnpm --filter @workspace/api-spec run codegen — regenerate API hooks and Zod schemas from the OpenAPI spec
  • pnpm validate:api-compatibility — fail on breaking OpenAPI changes vs. the target branch; CI rejects an intentional breaking-change override unless the pull request description follows the canonical instructions below
  • pnpm test:unit --run — root unit and contract suite; together with pnpm run typecheck and pnpm run validate:api-codegen it is the Root contract checks status check (.github/workflows/root-contract-checks.yml) that the GitHub ruleset requires before anything merges into main — see .github/branch-protection.md
  • pnpm --filter @workspace/db run push — push DB schema changes (dev only)
  • Required env: DATABASE_URL — Postgres connection string
  • Optional env: SENTRY_DSN — enables production error tracking and the /api/livez uptime monitor in artifacts/api-server (see Gotchas)

API compatibility override instructions

For an intentional versioned break, add both declarations to the pull request description.

Inline example

API_BREAKING_CHANGE_JUSTIFICATION: <why the existing contract must break>
API_BREAKING_CHANGE_MIGRATION_PLAN: <how existing consumers move to the new contract>

Multi-step migration plan example

API_BREAKING_CHANGE_MIGRATION_PLAN:
  - Release the updated client first.
  - Keep the old API available during migration.
  - Remove the old API after adoption is confirmed.

End the block with a blank line or the next API_BREAKING_CHANGE_* marker.

Stack

  • pnpm workspaces, Node.js 24, TypeScript 5.9
  • API: Express 5
  • DB: PostgreSQL + Drizzle ORM
  • Validation: Zod (zod/v4), drizzle-zod
  • API codegen: Orval (from OpenAPI spec)
  • Build: esbuild (CJS bundle)

Where things live

Populate as you build — short repo map plus pointers to the source-of-truth file for DB schema, API contracts, theme files, etc.

Architecture decisions

Populate as you build — non-obvious choices a reader couldn't infer from the code (3-5 bullets).

Product

Describe the high-level user-facing capabilities of this app once they exist.

User preferences

Populate as you build — explicit user instructions worth remembering across sessions.

Gotchas

  • Production error monitoring (Sentry) is wired into artifacts/api-server but needs SENTRY_DSN set to activate — without it, the server logs a warning and runs with no error reporting or uptime alerting. It captures uncaught exceptions/rejections, Express request errors, and reports a heartbeat check-in on /api/livez every 5 min (Sentry Crons "missed check-in" = the server is down). Database-backed readiness remains /api/healthz. Confirm in Sentry's dashboard that the api-server-healthz Cron Monitor and the project's issue alert rule actually have notifications turned on — that's dashboard-only config not settable from code.
  • Expo Go 57 on iOS (Replit's iPhone simulator preview) only loads a project from a dev server that is signed into the same Expo account Expo Go uses — the repl's private replit-private-… account. The Chat App dev script satisfies this: it first runs mkdir -p "${__UNSAFE_EXPO_HOME_DIRECTORY:-$HOME/.expo}" && create-launch login --session "$REPLIT_EXPO_SESSION_SECRET" (guarded: skipped silently when the secret is unset, || true so a failed login never blocks Metro), which writes the session to ~/.expo/state.json; Expo CLI then advertises the account in the served manifest as extra.expoGo.username. The mkdir -p is required: create-launch writes state.json atomically but never creates ~/.expo, and on a freshly booted container that directory does not exist until Metro starts, so without it every fresh boot logs ENOENT: no such file or directory, open '/home/runner/.expo/state.json…' instead of Logged in as … and serves an anonymous manifest. (extra.scopeKey stays @anonymous/… because Expo CLI only derives a scoped key from an EAS project, which this app does not have — that is expected.) Do not run npx expo login by hand; the secret is managed by the workspace and must never be echoed. Diagnostic order when the simulator sits on the iOS home screen or Expo Go shows account/sign-in steps: (1) the iOS manifest (curl -H 'expo-platform: ios' http://127.0.0.1:$PORT/) has no extra.expoGo.username → the sign-in step is missing or failed: check the workflow log for the Logged in as … line right after the script header, then restart the artifacts/chat-app: expo workflow (pnpm --filter @workspace/chat-app run validate:preview-startup reports this as dev_server_sign_in=… and fails when the secret is set but a served manifest is anonymous); (2) to see whether Expo Go reached Metro, do NOT rely on artifacts/chat-app/.expo/devices.json — Expo Go 57 on iOS sends no expo-dev-client-id header, so that file stays {"devices": []} even when the app is downloaded; instead set EXPO_DEV_REQUEST_LOG=1 (redacted request evidence in artifacts/chat-app/.expo/dev-request-evidence.log, lines carry platform=ios client=Expo Go) plus DEBUG=Metro:InspectorProxy,expo:start:server:middleware:manifest in the development environment, restart the Expo workflow, and read the workflow log: Expo Go shows up as Got new device connection: … app=host.exp.Exponent followed by an iOS Bundled line, and the bundle request lands in the evidence file (every Metro restart makes the simulator's Expo Go re-fetch the bundle, so no manual relaunch is needed for that check; remove both variables afterwards). Observed 2026-09-17 on Replit's iPhone simulator after the sign-in fix: manifest accepted, bundle downloaded (200, ~16 MB), then the inspector connection closed with code 1006 about 4 s later with no iOS LOG, asset, or API request — Expo Go quits while starting the app, which is why the simulator shows the iOS home screen; this is not a sign-in or launch-routing problem. Cause (2026-09-19): Expo Go iOS 57.0.5 — the build on Replit's simulator — embeds react-native-worklets 0.10.0 / react-native-reanimated 4.5.0, while SDK 57's bundledNativeModules.json (what expo install --check enforces) wants 0.10.1 / 4.5.1; worklets only checks major.minor at startup and 0.10.1 changed the arity of a JSI binding used during bundle evaluation, so the mismatched JavaScript crashes the native side before AppRegistry.runApplication (no red box, no client log). Expo Go 57.0.6+ (App Store 57.0.9) embeds 0.10.1 / 4.5.1 and the mismatch crashes in either direction, so the Chat App pins both packages to the targeted Expo Go build recorded in artifacts/chat-app/expo-go-native-modules.json (targetExpoGoIosVersion), lists them in expo.install.exclude, and pnpm --filter @workspace/chat-app run validate:preview-runtime fails when pins, exclusions, and target disagree. Keep the three in step: when the simulator's Expo Go build changes (Expo Go shows its version in its Settings tab; a probe result of BUNDLE_ONLY_THEN_CLOSED after a previously RUNNING setup is the tell), retarget, move the pins to that build's row, drop exclusions that no longer differ from the SDK default, pnpm install, restart the Expo workflow. A physical iPhone on the store build needs that retarget first. Never "fix" this with expo install --fix: it reinstates the crash. Native/EAS builds compile from node_modules, so they stay self-consistent at either version. The post-pin bundle was verified from the workspace (worklets 0.10.0 / reanimated 4.5.0, Hermes-compiles); the simulator launch itself still needs one armed probe run (probe:preview-launch -- --arm, expected RUNNING). Expo Go never runs native Sentry; that is only exercised in EAS builds.
  • Native secure storage (expo-secure-store) only accepts key names matching [A-Za-z0-9._-]; every keychain/keystore key must go through artifacts/chat-app/lib/secureStorageKey.ts, and the Jest mock in __tests__/CryptoContext.test.tsx enforces the same rule. Web (localStorage) accepts anything, so a native-only storage failure will not show up in the web preview.
  • The registered validation commands (typecheck, API/Chat App tests, banned-room E2E, mockup typecheck) are prefixed with flock -w 900 /tmp/replit-heavy-validation.lock on purpose: task-completion validation launches all of them at once, and on this 4-CPU workspace that slows tsc ~8x and trips fixed per-test timeouts that pass when the suites run one at a time. Keep the prefix when editing those commands.
  • Native crash reporting for the Chat App (artifacts/chat-app/lib/sentry.ts, .github/workflows/mobile-release.yml) is wired and gated, but the end-to-end proof from signed iOS/Android builds has not been run yet: it needs SENTRY_AUTH_TOKEN in the GitHub mobile-release environment and EAS secrets (never in the repo or Replit Secrets) plus one release-workflow run; see project task "Confirm crash reports from real iPhone and Android builds show readable code locations". The blockers were audited on 2026-09-14 (artifacts/chat-app/docs/sentry-native-source-map-check-20260914.md): the Chat App has no EAS project yet (eas.json, ios.bundleIdentifier, android.package, project ID all missing), GitHub has no mobile-release environment, secrets, variables, runners, or workflow runs, and GitHub development predates the Sentry pipeline. @sentry/react-native 7.11.0 already matches Expo SDK 57's bundled range, so no version alignment is pending.
  • Socket.IO auth-failure/disconnect-rate Sentry alerts (artifacts/api-server/src/lib/socketMonitoring.ts) ship with conservative default thresholds because the app has no real production traffic yet to baseline against. Once real usage exists, retune without a code change via optional env vars: SOCKET_AUTH_FAILURE_ALERT_THRESHOLD/_WINDOW_MS/_COOLDOWN_MS and SOCKET_DISCONNECT_ALERT_THRESHOLD/_WINDOW_MS/_COOLDOWN_MS (all default if unset or invalid).
  • Clerk retries for server account-access lookups (artifacts/api-server/src/lib/accountAccess.ts) and the Playwright browser checks (artifacts/api-server/e2e/clerk-retry.ts) share one policy in artifacts/api-server/src/lib/clerkRetry.ts: a wait between attempts never exceeds MAX_CLERK_RETRY_DELAY_MS (30 s), whether it comes from Clerk's retryAfter guidance (seconds) or the exponential fallback. Larger upstream hints are clamped, not rejected; malformed guidance falls back to the exponential delay. A server lookup additionally has a hard wall-clock deadline, ACCOUNT_ACCESS_RETRY_BUDGET_MS (15 s), covering waits and Clerk requests alike: it keeps the usual four attempts and delays while they fit, gives up as soon as the next wait would end past the deadline (so a capped 30 s hint is never waited out server-side), and races every Clerk request against the time left because the Clerk SDK exposes no abort signal (a hung request fails at the deadline with AccountAccessDeadlineError as the cause and its late result is discarded). The deadline exists because Socket.IO closes a handshake that has not finished within its 45 s connect timeout and proxies time out HTTP requests. When it gives up, getAccountAccess rejects with AccountAccessUnavailableError (artifacts/api-server/src/lib/accountAccessUnavailable.ts) carrying Clerk's capped guidance for the next attempt; requireAuthorizedUser turns that into a 503 with a Retry-After header and { code: "ACCOUNT_ACCESS_UNAVAILABLE", retryAfterSeconds }, and the socket handshake rejects with the same message plus connect_error data: { code, retryAfterSeconds } for both the Clerk-token and room-capability branches. The Chat App and the call/sandbox WebView pages do not act on the hint yet; they still show a generic connection error.
  • The Chat App is always dark (both palettes in artifacts/chat-app/constants/colors.ts are identical), so app.json pins userInterfaceStyle: "dark" and the root layout renders one <StatusBar style="light" />; Android draws its edge-to-edge status/navigation bars in the device theme otherwise and they vanish over the dark UI. Keyboard handling on native has one strategy: forms use KeyboardAwareScrollViewCompat, everything else uses KeyboardAvoidingView from react-native-keyboard-controller with behavior="padding" on both platforms (React Native's own KeyboardAvoidingView does not react on Android once the keyboard-controller provider owns the insets). Jest cannot see a drift back because every suite mocks react-native-keyboard-controller, so artifacts/chat-app/scripts/validate-keyboard-strategy.mjs (first step of the package test script, pnpm run validate:keyboard-strategy by hand) parses every top-level directory of artifacts/chat-app except the known non-source ones (NON_SOURCE_DIRECTORIES in the script: node_modules/, assets/, the test trees __tests__/, __mocks__/, test-utils/, test-results/, coverage/ and e2e/, the package's own scripts/, docs/, and the build outputs dist/, web-build/, static-build/, build/, ios/ and android/) and dot-directories, so today app/**, components/**, constants/**, contexts/**, hooks/**, lib/**, plugins/**, server/**, types/** and utils/** are read at module level and a source folder added later (services/, store/, features/, …) is scanned without editing a list — a helper there that re-exports the wrong component or defines a platform-split behavior consumed only through a props spread is reported where it is defined (app/ and components/ must exist; root-level files are Metro/Babel/Jest configuration and are not scanned, though a value or tag a screen imports from one is still followed one hop; the pass message lists the directories read and the ones skipped, so a source folder that collides with a non-source name is visible in the run output) and fails when a file imports KeyboardAvoidingView from react-native (including a shared module that re-exports it under another name and the screens that render that alias), chooses behavior by Platform.OS/Platform.select/process.env.EXPO_OS or by .ios/.android module files, or uses KeyboardAwareScrollView directly outside the compat component. Same-file indirection and one import hop into another Chat App file are followed (behavior={useKeyboardBehavior()} is traced into the hook); a value further away, behind export *, or in a module the check cannot find is rejected as "comes from another file", and the controller KeyboardAvoidingView still needs the literal "padding" at its own call site. The same script's whole-tree rule (root-keyboard-provider) reads app/_layout.tsx and fails unless the tree its default export renders nests expo-router's navigator (Stack, Slot, Tabs, NativeTabs, Drawer or Navigator) inside KeyboardProvider from react-native-keyboard-controller — the controller's KeyboardAvoidingView and the compat component do nothing on devices without that provider, and every suite mocks it as a pass-through (__tests__/EntryBranding.test.tsx mocks KeyboardProvider itself), so a layout refactor or SDK upgrade that drops or moves the provider is otherwise invisible. The rule reads only what the layout renders — the expressions each component returns (plus conditionals, &&/||, arrays, JSX-valued variables and calls in rendered position), following same-file components and one import hop — so JSX in an effect, an unused variable, a prop or a nested callback neither satisfies nor trips it, and the navigator element must be lexically nested inside the provider element on that rendered path; a provider hidden in a children-passing wrapper or a render prop, a navigator two hops away, a re-exported default export, a missing root layout or a platform-specific _layout.<platform>.tsx are reported, never assumed fine. Change the strategy here and in that check together. artifacts/chat-app/__tests__/KeyboardAwareScrollViewCompat.test.tsx is the one suite that renders the real compat component: it runs under both Jest projects and fails if phones stop getting the controller's KeyboardAwareScrollView, web stops getting React Native's plain ScrollView, or the keyboardShouldPersistTaps="handled" default is lost. The form-screen suites (SignIn, SignUp, Setup, NewRoom) replace the component with the shared stand-in in artifacts/chat-app/test-utils/keyboardAwareScrollViewCompatMock.tsx, which wraps its ScrollView in a tagged host element, and look their inputs and submit controls up through withinKeyboardAwareScrollViewCompat(view); nothing else proves a form still uses the component (the source check only forbids the wrong keyboard components, the screens pass keyboardShouldPersistTaps="handled" and their testID themselves, and the app tsconfig does not flag an unused import), so a form rewritten onto a plain ScrollView or an input moved outside the component fails those suites under both Jest projects. A new form screen needs the same mock and assertion in its suite. The classic tab bar differs per platform: a BlurView over a clear bar on iOS (while high contrast is on, iOS draws the palette's denser tabBarBackground panel instead of the blur — the same surface web draws — because the blur is the busiest surface and the high-contrast palette exists to take visual noise away from low-vision users; the panel token must stay denser than the default palettes' and the layout suite checks that); an opaque background bar with no background element on Android (owner decision 2026-09-17 — a translucent panel was tried and taken back; expo-blur's Android blur needs a BlurTargetView around each screen and re-blurs every frame below Android 12, so no blur ships until a device pass justifies it); the palette's translucent tabBarBackground panel on a fixed 84pt bar on web. The Profile → Accessibility "Reduce transparency" toggle (reduceTransparency in contexts/AccessibilityContext.tsx, persisted with the other preferences and following iOS's system Reduce Transparency setting until the user sets it — Android has no such setting and react-native-web has no isReduceTransparencyEnabled, so the query is guarded) makes the iOS blur (or its high-contrast panel) and web panel opaque with no background element while preserving the border and absolute position; Android is already opaque. High contrast alone never makes a surface opaque — that is Reduce transparency's job, and the two settings stay distinct. On iOS 26 (isLiquidGlassAvailable() from expo-glass-effect) the layout renders expo-router's NativeTabs instead, and UIKit draws the Liquid Glass bar; the in-app toggles are preferences the system never sees, so the layout maps both onto NativeTabs props: with Reduce transparency on it passes backgroundColor (palette background), blurEffect="none", disableTransparentOnScrollEdge and shadowColor (palette border) so UIKit draws an opaque bar in place of the glass; with high contrast alone it passes the same four props with the denser, still see-through tabBarBackground panel (never opaque — Reduce transparency wins when both are on) plus tintColor, iconColor { default, selected } and labelStyle { default, selected } so the items take the high-contrast palette's primary / mutedForeground instead of system blue and gray (Reduce transparency alone keeps the system tints); and it passes none of them with both off so the default glass stays (iOS's own system setting solidifies it by itself). No iOS 26 device is reachable from the workspace: artifacts/chat-app/docs/ios-liquid-glass-accessibility-device-check-20260920.md records what is verified and what UIKit's rendering still owes (full-width bar vs. capsule, and whether unselected icons take iconColor.default once the bar leaves the glass — react-native-screens documents that on the glass bar iOS 26 tints only the selected item's icon). __tests__/TabLayout.test.tsx runs under both Jest projects and fails if the Android background is dropped, made translucent, or joined by a panel or blur, checks the web panel keeps the tab tints at 4.5:1 over white, and pins the native tab bar's prop set for default / high contrast / Reduce transparency / both, with the high-contrast tints at 4.5:1 over both native surfaces; __tests__/NativeTabBarAppearance.test.tsx renders the real layout and its real public NativeTabs through expo-router's whole vendored chain (wrapper, withLayoutContext, navigator, trigger conversion, iOS appearance builders in build/native-tabs/appearance.ios, native tabs view) inside the minimum of expo-router's root the chain reads (a BaseNavigationContainer with the theme as its prop, a Route node for app/(tabs), a stub router store), for each toggle state and across a toggle on the mounted bar, and pins what reaches react-native-screens' Tabs.Screen / Tabs.Host, so an Expo upgrade that renames or drops an option anywhere in the chain, drops a fallback, derives options once, changes the scroll-edge gating or the appearance shape fails in Jest naming the vendored file. Screens that scroll under the classic tab bar reserve its measured height through hooks/useTabBarContentInset.ts, which reads expo-router's vendored BottomTabBarHeightContext (expo-router/js-tabs) — do not add @react-navigation/bottom-tabs, a second copy of the context never receives the height. Inside the iOS 26 native tabs no height is published and the hook reserves only the bottom safe-area inset, which is enough because expo-router's vendored NativeTabsView.ios.js renders each tab inside its own SafeAreaProvider, where UIKit's safe area already includes the Liquid Glass bar. __tests__/VendoredBottomTabBar.test.tsx and __tests__/VendoredNativeTabsView.test.tsx render the real vendored views (iOS Jest project) and fail, naming the vendored file and the assumption that broke, when an Expo upgrade changes either; re-verify the tab bars on a phone before adjusting the hook. Multiline inputs set textAlignVertical: "top" because Android centers multiline text. No Android emulator is reachable from this workspace; Android-only appearance has to be confirmed by the user. The only early signal is artifacts/chat-app/jest.config.js: it runs the whole suite as the iOS project (jest-expo's default preset, where Platform.OS is "ios") and the screen/layout suites listed in androidLayoutSuites a second time as the Android project (jest-expo/android, guarded by jest.setup-android.js). Add a new layout suite to that list; key expectations that legitimately differ by platform with test-utils/platform.ts (onTestPlatform({ ios, android })) rather than skipping; run one platform with pnpm exec jest --selectProjects Android.
  • @clerk/expo must stay in the plugins array of artifacts/chat-app/app.json (currently with appleSignIn: false because the app offers no Sign in with Apple button). Its config plugin raises the iOS deployment target to 17.0, which the Clerk native module (ClerkExpo pod, Swift package clerk-ios) requires; on the SDK default (16.4) Expo autolinking skips the pod after its podspec has already registered the Swift package, and the App Store build dies in pod install with undefined method 'package_product_dependencies' for nil from React Native's SPM post-install hook. The plugin's Android edits (packaging excludes, Kotlin flag, hosted-auth intent filter) are Clerk's documented defaults. Keep the config static (app.json, no app.config.*).
  • Sentry native uploads are conditional on a credential: artifacts/chat-app/app.json routes @sentry/react-native through artifacts/chat-app/plugins/withSentryNativeUpload.js, which applies the official Expo plugin (Xcode phases, sentry.gradle, sentry.properties) only when SENTRY_AUTH_TOKEN is set at prebuild (environment variable, or a non-blank entry in an ignored .env.sentry-build-plugin file) and otherwise omits the upload steps with a prebuild warning; the authToken plugin option is rejected because app.json is committed. Reason: App Store publishes from Replit's Publish pane (Expo Launch) never carry the token, and sentry-cli react-native xcode fails the Release archive without one (SENTRY_ALLOW_FAILURE is not honoured by the iOS script; only SENTRY_DISABLE_AUTO_UPLOAD=true skips it). Never list @sentry/react-native / @sentry/react-native/expo directly in plugins again — that bypasses the policy (pnpm --filter @workspace/chat-app run test:sentry-upload-policy guards it). @sentry/cli is also a direct Chat App devDependency pinned to the exact version @sentry/react-native depends on: Sentry's Xcode/Gradle scripts resolve it with require.resolve('@sentry/cli/package.json') from the app directory, which pnpm's isolated layout only satisfies for declared dependencies (the scripts' pnpm shim fallback produced an unusable path and the archive died with "Cannot find module" in the "Bundle React Native code and images" phase). Keep the two versions in lockstep on every @sentry/react-native upgrade. Expo Launch builds do not run the app's eas-build-pre-install hook (node scripts/build.js --native-build-preflight, which fails closed without the token, SENTRY_RELEASE, and SENTRY_DIST): the 2026-09-16 archive reached Xcode compilation, which that preflight would have prevented. The strict hook stays as the GitHub/EAS release gate; if a Publish-pane failure ever shows the preflight's "Native mobile release build requires…" message, Launch has started running hooks and the two build channels need an explicit, non-secret mode switch before retrying.
  • GitHub masks a secret in logs and step summaries only for the exact value registered in the same job, so release IDs and smoke-account values must stay out of summaries by construction. scripts/tests/mobile-release-summary-contract.test.mjs (part of pnpm test:unit) discovers every script the mobile release workflow invokes (directly or through package scripts) that writes GITHUB_STEP_SUMMARY, requires each to be listed in its scriptContracts inventory, and fails if the workflow, the native large-text gate, or the Android preflight interpolates a secret-backed variable into a summary, a diagnostic, or a log. Candidate IDs belong only in uploaded evidence files under the results directory; a file the workflow copies into the summary (the native-branding-summary.md fragment) counts as a summary and must stay identifier-free, while the detailed native-branding-check.md report keeps the ID; summary steps may reference step outcomes and non-secret env, nothing else; diagnostics may interpolate only allowlisted runtime values and literal script constants.

Pointers

  • See the pnpm-workspace skill for workspace structure, TypeScript setup, and package details