RealtimeAlgoChatApp Studio is a real-time collaboration platform for developers with Build, Call, and Ship modes in a single room.
pnpm --filter @workspace/api-server run dev— run the API server (port 5000)pnpm run typecheck— full typecheck across all packagespnpm run build— typecheck + build all packagespnpm test:unit --run— root unit suite; it must pass on a fresh clone or new git worktree with noartifacts/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, andscripts/tests/check-native-large-text-evidence-fresh-checkout.test.shenforces itpnpm --filter @workspace/api-spec run codegen— regenerate API hooks and Zod schemas from the OpenAPI specpnpm 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 belowpnpm test:unit --run— root unit and contract suite; together withpnpm run typecheckandpnpm run validate:api-codegenit is theRoot contract checksstatus check (.github/workflows/root-contract-checks.yml) that the GitHub ruleset requires before anything merges intomain— see.github/branch-protection.mdpnpm --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/livezuptime monitor inartifacts/api-server(see Gotchas)
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.
- 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)
Populate as you build — short repo map plus pointers to the source-of-truth file for DB schema, API contracts, theme files, etc.
Populate as you build — non-obvious choices a reader couldn't infer from the code (3-5 bullets).
Describe the high-level user-facing capabilities of this app once they exist.
Populate as you build — explicit user instructions worth remembering across sessions.
- Production error monitoring (Sentry) is wired into
artifacts/api-serverbut needsSENTRY_DSNset 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/livezevery 5 min (Sentry Crons "missed check-in" = the server is down). Database-backed readiness remains/api/healthz. Confirm in Sentry's dashboard that theapi-server-healthzCron 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 Appdevscript satisfies this: it first runsmkdir -p "${__UNSAFE_EXPO_HOME_DIRECTORY:-$HOME/.expo}" && create-launch login --session "$REPLIT_EXPO_SESSION_SECRET"(guarded: skipped silently when the secret is unset,|| trueso a failed login never blocks Metro), which writes the session to~/.expo/state.json; Expo CLI then advertises the account in the served manifest asextra.expoGo.username. Themkdir -pis required:create-launchwritesstate.jsonatomically but never creates~/.expo, and on a freshly booted container that directory does not exist until Metro starts, so without it every fresh boot logsENOENT: no such file or directory, open '/home/runner/.expo/state.json…'instead ofLogged in as …and serves an anonymous manifest. (extra.scopeKeystays@anonymous/…because Expo CLI only derives a scoped key from an EAS project, which this app does not have — that is expected.) Do not runnpx expo loginby 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 noextra.expoGo.username→ the sign-in step is missing or failed: check the workflow log for theLogged in as …line right after the script header, then restart theartifacts/chat-app: expoworkflow (pnpm --filter @workspace/chat-app run validate:preview-startupreports this asdev_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 onartifacts/chat-app/.expo/devices.json— Expo Go 57 on iOS sends noexpo-dev-client-idheader, so that file stays{"devices": []}even when the app is downloaded; instead setEXPO_DEV_REQUEST_LOG=1(redacted request evidence inartifacts/chat-app/.expo/dev-request-evidence.log, lines carryplatform=ios client=Expo Go) plusDEBUG=Metro:InspectorProxy,expo:start:server:middleware:manifestin the development environment, restart the Expo workflow, and read the workflow log: Expo Go shows up asGot new device connection: … app=host.exp.Exponentfollowed by aniOS Bundledline, 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 noiOS 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 — embedsreact-native-worklets0.10.0 /react-native-reanimated4.5.0, while SDK 57'sbundledNativeModules.json(whatexpo install --checkenforces) 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 beforeAppRegistry.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 inartifacts/chat-app/expo-go-native-modules.json(targetExpoGoIosVersion), lists them inexpo.install.exclude, andpnpm --filter @workspace/chat-app run validate:preview-runtimefails 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 ofBUNDLE_ONLY_THEN_CLOSEDafter a previouslyRUNNINGsetup 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 withexpo install --fix: it reinstates the crash. Native/EAS builds compile fromnode_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, expectedRUNNING). 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 throughartifacts/chat-app/lib/secureStorageKey.ts, and the Jest mock in__tests__/CryptoContext.test.tsxenforces 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.lockon purpose: task-completion validation launches all of them at once, and on this 4-CPU workspace that slowstsc~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 needsSENTRY_AUTH_TOKENin the GitHubmobile-releaseenvironment 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 nomobile-releaseenvironment, secrets, variables, runners, or workflow runs, and GitHubdevelopmentpredates the Sentry pipeline.@sentry/react-native7.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_MSandSOCKET_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 inartifacts/api-server/src/lib/clerkRetry.ts: a wait between attempts never exceedsMAX_CLERK_RETRY_DELAY_MS(30 s), whether it comes from Clerk'sretryAfterguidance (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 withAccountAccessDeadlineErroras 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,getAccountAccessrejects withAccountAccessUnavailableError(artifacts/api-server/src/lib/accountAccessUnavailable.ts) carrying Clerk's capped guidance for the next attempt;requireAuthorizedUserturns that into a 503 with aRetry-Afterheader and{ code: "ACCOUNT_ACCESS_UNAVAILABLE", retryAfterSeconds }, and the socket handshake rejects with the same message plusconnect_errordata: { 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.tsare identical), soapp.jsonpinsuserInterfaceStyle: "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 useKeyboardAwareScrollViewCompat, everything else usesKeyboardAvoidingViewfromreact-native-keyboard-controllerwithbehavior="padding"on both platforms (React Native's ownKeyboardAvoidingViewdoes not react on Android once the keyboard-controller provider owns the insets). Jest cannot see a drift back because every suite mocksreact-native-keyboard-controller, soartifacts/chat-app/scripts/validate-keyboard-strategy.mjs(first step of the packagetestscript,pnpm run validate:keyboard-strategyby hand) parses every top-level directory ofartifacts/chat-appexcept the known non-source ones (NON_SOURCE_DIRECTORIESin the script:node_modules/,assets/, the test trees__tests__/,__mocks__/,test-utils/,test-results/,coverage/ande2e/, the package's ownscripts/,docs/, and the build outputsdist/,web-build/,static-build/,build/,ios/andandroid/) and dot-directories, so todayapp/**,components/**,constants/**,contexts/**,hooks/**,lib/**,plugins/**,server/**,types/**andutils/**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-splitbehaviorconsumed only through a props spread is reported where it is defined (app/andcomponents/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 importsKeyboardAvoidingViewfromreact-native(including a shared module that re-exports it under another name and the screens that render that alias), choosesbehaviorbyPlatform.OS/Platform.select/process.env.EXPO_OSor by.ios/.androidmodule files, or usesKeyboardAwareScrollViewdirectly 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, behindexport *, or in a module the check cannot find is rejected as "comes from another file", and the controllerKeyboardAvoidingViewstill needs the literal"padding"at its own call site. The same script's whole-tree rule (root-keyboard-provider) readsapp/_layout.tsxand fails unless the tree its default export renders nests expo-router's navigator (Stack,Slot,Tabs,NativeTabs,DrawerorNavigator) insideKeyboardProviderfromreact-native-keyboard-controller— the controller'sKeyboardAvoidingViewand the compat component do nothing on devices without that provider, and every suite mocks it as a pass-through (__tests__/EntryBranding.test.tsxmocksKeyboardProvideritself), 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 achildren-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>.tsxare reported, never assumed fine. Change the strategy here and in that check together.artifacts/chat-app/__tests__/KeyboardAwareScrollViewCompat.test.tsxis the one suite that renders the real compat component: it runs under both Jest projects and fails if phones stop getting the controller'sKeyboardAwareScrollView, web stops getting React Native's plainScrollView, or thekeyboardShouldPersistTaps="handled"default is lost. The form-screen suites (SignIn, SignUp, Setup, NewRoom) replace the component with the shared stand-in inartifacts/chat-app/test-utils/keyboardAwareScrollViewCompatMock.tsx, which wraps itsScrollViewin a tagged host element, and look their inputs and submit controls up throughwithinKeyboardAwareScrollViewCompat(view); nothing else proves a form still uses the component (the source check only forbids the wrong keyboard components, the screens passkeyboardShouldPersistTaps="handled"and theirtestIDthemselves, and the app tsconfig does not flag an unused import), so a form rewritten onto a plainScrollViewor 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: aBlurViewover a clear bar on iOS (while high contrast is on, iOS draws the palette's densertabBarBackgroundpanel 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 opaquebackgroundbar 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 aBlurTargetViewaround each screen and re-blurs every frame below Android 12, so no blur ships until a device pass justifies it); the palette's translucenttabBarBackgroundpanel on a fixed 84pt bar on web. The Profile → Accessibility "Reduce transparency" toggle (reduceTransparencyincontexts/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 noisReduceTransparencyEnabled, 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'sNativeTabsinstead, 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 passesbackgroundColor(palettebackground),blurEffect="none",disableTransparentOnScrollEdgeandshadowColor(paletteborder) 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-throughtabBarBackgroundpanel (never opaque — Reduce transparency wins when both are on) plustintColor,iconColor{ default, selected }andlabelStyle{ default, selected }so the items take the high-contrast palette'sprimary/mutedForegroundinstead 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.mdrecords what is verified and what UIKit's rendering still owes (full-width bar vs. capsule, and whether unselected icons takeiconColor.defaultonce 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.tsxruns 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.tsxrenders the real layout and its real publicNativeTabsthrough expo-router's whole vendored chain (wrapper,withLayoutContext, navigator, trigger conversion, iOS appearance builders inbuild/native-tabs/appearance.ios, native tabs view) inside the minimum of expo-router's root the chain reads (aBaseNavigationContainerwith the theme as its prop, aRoutenode forapp/(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 throughhooks/useTabBarContentInset.ts, which reads expo-router's vendoredBottomTabBarHeightContext(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 vendoredNativeTabsView.ios.jsrenders each tab inside its ownSafeAreaProvider, where UIKit's safe area already includes the Liquid Glass bar.__tests__/VendoredBottomTabBar.test.tsxand__tests__/VendoredNativeTabsView.test.tsxrender 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 settextAlignVertical: "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 isartifacts/chat-app/jest.config.js: it runs the whole suite as theiOSproject (jest-expo's default preset, wherePlatform.OSis"ios") and the screen/layout suites listed inandroidLayoutSuitesa second time as theAndroidproject (jest-expo/android, guarded byjest.setup-android.js). Add a new layout suite to that list; key expectations that legitimately differ by platform withtest-utils/platform.ts(onTestPlatform({ ios, android })) rather than skipping; run one platform withpnpm exec jest --selectProjects Android. @clerk/expomust stay in thepluginsarray ofartifacts/chat-app/app.json(currently withappleSignIn: falsebecause 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 (ClerkExpopod, Swift packageclerk-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 inpod installwithundefined method 'package_product_dependencies' for nilfrom 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, noapp.config.*).- Sentry native uploads are conditional on a credential:
artifacts/chat-app/app.jsonroutes@sentry/react-nativethroughartifacts/chat-app/plugins/withSentryNativeUpload.js, which applies the official Expo plugin (Xcode phases,sentry.gradle,sentry.properties) only whenSENTRY_AUTH_TOKENis set at prebuild (environment variable, or a non-blank entry in an ignored.env.sentry-build-pluginfile) and otherwise omits the upload steps with a prebuild warning; theauthTokenplugin option is rejected becauseapp.jsonis committed. Reason: App Store publishes from Replit's Publish pane (Expo Launch) never carry the token, andsentry-cli react-native xcodefails the Release archive without one (SENTRY_ALLOW_FAILUREis not honoured by the iOS script; onlySENTRY_DISABLE_AUTO_UPLOAD=trueskips it). Never list@sentry/react-native/@sentry/react-native/expodirectly inpluginsagain — that bypasses the policy (pnpm --filter @workspace/chat-app run test:sentry-upload-policyguards it).@sentry/cliis also a direct Chat App devDependency pinned to the exact version@sentry/react-nativedepends on: Sentry's Xcode/Gradle scripts resolve it withrequire.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-nativeupgrade. Expo Launch builds do not run the app'seas-build-pre-installhook (node scripts/build.js --native-build-preflight, which fails closed without the token,SENTRY_RELEASE, andSENTRY_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 ofpnpm test:unit) discovers every script the mobile release workflow invokes (directly or through package scripts) that writesGITHUB_STEP_SUMMARY, requires each to be listed in itsscriptContractsinventory, 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 (thenative-branding-summary.mdfragment) counts as a summary and must stay identifier-free, while the detailednative-branding-check.mdreport 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.
- See the
pnpm-workspaceskill for workspace structure, TypeScript setup, and package details