Skip to main content
Prerequisite: You should have installed Node.js (matching the repository floor of >=22.13.0).

Installation

Supabase Auth declaration compatibility

The repository pins a Bun patch for @supabase/auth-js@2.117.1. Its credential interface retains every native PublicKeyCredential member except toJSON, which the package declares separately with its existing response type. Native DOM JSON uses an encoded large-blob string; the package’s existing extension output type uses ArrayBuffer. Treat these as distinct contracts. The patch changes declarations only. It does not encode extension data, alter authentication behavior, or qualify browser passkey delivery. Both shipped declaration formats and the package source carry the same correction. Keep skipLibCheck disabled when checking this compatibility boundary:
Bun installs the patch through patchedDependencies; do not edit installed dependencies as a permanent fix. Before updating Auth JS or removing the patch, check the strict public verification types and the negative native-credential and encoded-blob controls against the installed replacement. The finite behavior controls call the installed ESM and CommonJS serialization functions with typed mocks. They check registration and authentication separately. The native toJSON branch returns its encoded extension output unchanged; the fallback returns raw extension buffers unchanged. Stringifying a fallback large-blob buffer produces {}, so this patch does not prove extension data can be transmitted correctly. Native registration JSON also requires fields that the existing fallback does not emit. The controls retain expected type errors for native DOM JSON and credentials without toJSON. The package’s serializer signatures still require toJSON and advertise the package’s raw-extension response type even though the implementation supports both branches. Those signature limitations remain separate from this inheritance correction. The mocks exercise pure serialization, not browser credential acquisition or server verification. Step 1. Install bun on your machine (if you don’t already have it yet), by running the following command: macOS/Linux:
Windows:

Install the repository dependencies

Use the Bun version pinned by the root packageManager field. The version reference is generated from repository manifests and lockfiles, so it records the current pins without relying on outdated prose or a globally installed CLI.
The Tiptap Pro registry step is retired; no private registry setup is required. bun setup is not a minimal installation command: it can build packages and probe services. Use development without Docker for the supported minimal Next.js path. Rust and TanStack Start are not in use and are not required to work on the active web apps. This repository’s Bun configuration enforces a minimum release age of 1 day for new npm package versions. If Bun skips a just-published release during install, wait for the package to age past the gate or choose an older published version. First-party package families that publish coordinated releases together may be explicitly excluded from the age gate in bunfig.toml; keep each trusted family’s exact-name list aligned with the packages resolved in bun.lock and cover the list with a policy test. Discord’s Python environment is resolved separately in apps/discord/uv.lock. For targeted transitive updates, use uv lock --upgrade-package pyjwt --upgrade-package urllib3, then confirm that unrelated package records and the project’s Python floor are unchanged. Validate artifact hashes against PyPI and run the owning locked Python CI, including the offline dependency security tests in tests/test_dependency_security.py. PyJWT 2.15.1 includes malformed-payload/JWKS hardening and accepts valid trailing Base64URL padding while continuing to reject invalid characters. urllib3 2.8.0 bounds chunk-size lines and fixes HTTPS proxy TLS configuration and Deflate streaming. Proxy-specific trust settings belong in proxy_ssl_context and the proxy identity options; destination TLS settings are separate. These dependencies are reached through Supabase Auth/MSAL and Requests, respectively. A locked CI environment does not establish the version in an already deployed Modal image: app.py has a separate floating .pip_install(...) recipe. Verify that image’s resolved versions during the separately authorized deployment process. To complete the initial setup, please restart your IDE so that it can recognize the newly installed dependencies. Additionally, some recommended VS Code Extensions may only work after restarting your IDE.
If you’re using VS Code, you can install following the recommended extensions that will help you with the development process: Oxc (this repo standardizes on Oxc for linting and formatting via .oxfmtrc.json and .oxlintrc.json, not ESLint or Prettier), Vitest, Tailwind CSS IntelliSense, Version Lens, Error Lens, Pretty TypeScript Errors, Material Icon Theme.

Zed

This monorepo tracks project-level Zed settings in .zed/settings.json. Opening the repository root in Zed can otherwise trigger expensive scans across generated output and local dependency trees, especially node_modules, .turbo, .next, package dist directories, Flutter build output, Rust target, and Python virtualenv/cache folders. Keep large generated directories in file_scan_exclusions. Zed’s file_scan_exclusions setting replaces the default list instead of extending it, so include Zed’s default VCS/system exclusions whenever adding project exclusions. The project uses TypeScript 7 RC’s tsc for compiler checks and a single TypeScript language server path for TypeScript, TSX, and JavaScript; avoid enabling competing TypeScript language servers for the same language unless you are deliberately comparing language server behavior. Every app configuration overrides TypeScript 7’s types compatibility wildcard with its explicit runtime globals, so editors with an older bundled language service do not report Cannot find type definition file for '*' across the app tree.

Development

Start Next.js apps

To develop all apps and packages (without requiring a local setup), run the following command:
bun
This command will start all Next.js apps in development mode. You can access the platform app by visiting the following URL: https://tuturuuu.localhost
Local app development uses Portless, so each app gets a stable Tuturuuu subdomain instead of relying on its fallback port. The app package dev scripts run the repo’s Portless-safe wrapper, and the underlying framework command remains available through dev:app for direct port-based debugging. The wrapper only removes stale static aliases for the same app when their default backend port is closed, then starts Portless with the original app config. This fixes old worktree aliases such as zalo-qr-chat-setup.tuturuuu.localhost without cleaning certificates, deleting unrelated routes, or touching other apps’ active aliases. Package-local bun dev --force delegates to portless run --name <app> --force so Portless can take over a live route when you explicitly request it. Root app-specific commands such as bun dev:chat, bun dev:calendar, and bun dev:edu reuse app servers that are already running. The launcher checks active Portless routes first, then the app’s default localhost port; if it finds a direct localhost listener, it tries to register a matching Portless alias so cross-app auth can still use the *.tuturuuu.localhost URL. Pass --force or --no-reuse when you intentionally want the command to include already-running app workspaces. Force mode starts the package dev --force path so Portless can take over the matching route instead of leaving a stale static alias in place.

Inactive migration tooling

Rust (apps/backend) and TanStack Start (apps/tanstack-web) are paused and not in use. Retained commands and Worker entrypoints are future-resumption references, not steps in the current Next.js setup. Do not start these services or require their migration gates for routine app development. See the active runtime map. bun dev:web is intentionally lean: by default it starts only apps/web and does not start the @tuturuuu/types or @tuturuuu/supabase package watch builds. Use this default for app-route and UI work so the local process tree stays small. When you are actively editing those package sources and need their dist output rebuilt live, opt in explicitly:
You can also pass --no-shared-watchers to other root app launchers when you want the same low-memory posture for a focused app session. apps/web does not mount React Query Devtools in its default development provider. Keep the default off for faster cold route compiles and lower Turbopack memory pressure; add a local, temporary devtools mount only while debugging query cache behavior, then remove it before committing. Keep the always-mounted public shell small. The mobile menu drawer, marketing footer body, report-problem dialog, and authenticated dropdown-only affordances should stay behind dynamic imports when possible, and shell icons should use @tuturuuu/icons/lucide-static instead of the root @tuturuuu/icons entrypoint. This prevents the public /login compile graph from pulling large Radix, footer, or full icon-package chunks into every bun dev:web session. bun dev:chat also manages the local chat realtime sidecar. It reuses localhost:7817 when a sidecar is already running; otherwise it starts apps/chat-realtime so apps/web can proxy chat SSE traffic for apps/chat. When debugging without Portless, run an app’s dev:app script directly. If auth redirects or absolute links must stay on the raw listener port, set BASE_URL, WEB_APP_URL, or the app-specific URL environment variable to that legacy fallback port for the direct debug session. Keep runtime defaults, auth redirects, and cross-app links on the Portless origins during local development. Use the shared local app URL helpers instead of hard-coding localhost fallbacks in satellite app constants or proxies, so a browser session that starts on tasks.tuturuuu.localhost stays under the tuturuuu.localhost namespace through login and return URLs. When a satellite proxies a Web-owned API collection through an external rewrite, use an exact collection rule and a separate nonempty :path+ descendant rule. A :path* destination can compile to a regex backreference whose empty value leaves a trailing slash. If Web strips that slash with a relative 308, the satellite can repeat the same request indefinitely. Infrastructure’s employee collection regression exercises both installed Next runtime matching and the deployment rewrite converter in apps/infrastructure/next.config.test.ts. Preserve satellite session and API guards, and verify the emitted deployment route plus an authenticated request after the authorized rollout; passing source tests alone does not establish production availability. For a production-mode smoke test of apps/web, use the preview deployment built by CI for the PR’s exact head commit. Do not create a local production build on this machine. Local development still uses the Portless hostname for cross-app login and routing checks. Use bun dev:edu when working on the education apps together. It starts Learn, Teach, their shared packages, and the central web app so cross-app login can complete locally. Next.js still prints the internal listener port, for example http://localhost:4803, because Portless assigns a free backend port to the app process. App dev:app scripts also print a Portless URL: line from the PORTLESS_URL environment variable; use that URL in the browser:
If Turbo cannot prompt for the Portless sudo password while starting app dev scripts, run the Portless setup helper first:
The helper starts the Portless HTTPS proxy on port 443 before Turbo launches package dev scripts. It no-ops when the proxy is already responding and skips in non-interactive shells or CI. bun setup runs this helper after dependency installation so fresh local checkouts are ready before the build step, then invokes Turborepo through the repo-local turbo:local helper instead of any globally installed Turbo binary. To install Portless as an OS startup service instead of only starting the current proxy daemon, run:

Diagnose a broken local environment

When a local app suddenly cannot reach another app — for example the Inventory landing page throws InternalApiError: 404 because its server-side call to https://tuturuuu.localhost/api/... is not routed — run the doctor:
It checks the Node runtime, Docker daemon, local Supabase ports, local Redis ports, apps/web Redis env wiring, app-local Supabase env consistency, whether the Portless proxy is responding on port 443, and the health of every registered Portless route. The report uses colored OK, WARN, and FAIL statuses in interactive terminals. Supabase env mismatch warnings are compact by default so secret-like values stay out of the normal output; use the verbose view when you need redacted per-file fingerprints:
The most common failure after overlapping or crashed dev sessions is a stale routing table: the proxy still points a live app route at a dead dev-server port, so cross-app internal API calls return 404. The doctor flags those routes as DEAD and exits non-zero. Static aliases for apps you simply are not running are reported as warnings, not failures, and include an exact cleanup command. An initialized but empty Portless route table is treated as healthy because no apps may be running yet. Apply the safe automatic Portless repair (start the proxy, or reset it when routes are stale) and re-run the checks:
bun setup deliberately no-ops when a proxy is already responding, so it does not clear a stale routing table on its own. To force a clean restart — stop the proxy, prune orphaned dev servers from crashed sessions, then start a fresh proxy that apps re-register against — run:
After a reset, restart your dev servers (for example bun dev:inventory) so each app re-registers its route with the new proxy. If bun doctor still warns about an inactive static alias after a reset, that alias was created with portless alias and is not deleted by proxy resets. Start the app if you still use the alias, or remove the alias shown in the doctor output:
If the doctor warns that app env files use mismatched Supabase values, choose the intended source app and keep these three keys together: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, and SUPABASE_SECRET_KEY. For quick local development, copy apps/web/.env.local to app-local .env.local files:
Preview the target files first with:
This sync compares app env files only. The root .env.local can intentionally target a different Docker or deployment workflow without keeping the app mismatch warning alive.

Diagnose web dev memory and cache pressure

When apps/web feels slow or Activity Monitor reports high Node.js usage, start with the dev diagnostic before moving routes into another app:
Read the report by pressure type:
  • Live Next dev process RSS is the memory held by the running next dev process tree. If this is high, inspect the top processes and the latest trace spans before changing architecture.
  • apps/web/.next/dev/cache/turbopack is generated Turbopack filesystem cache. Multi-GB cache growth can make disk usage look like a live memory leak even when process RSS is modest.
  • apps/web/.next/cache can include the Next 16.3 Turbopack build filesystem cache from local builds. It speeds repeated local next build runs, but it is still local artifact state and must stay out of Turborepo task outputs.
  • .turbo/cache is Turborepo task cache. It is separate from the live Next.js dev server and should not be treated as app RSS.
  • Build memory policy reports Docker and next build memory budgets. Those caps explain build behavior only; do not compare them directly to next dev RSS.
  • Likely pressure calls out whether the current evidence points to live RSS, cache growth, watcher limits, or build-only memory settings.
If the report points at Turbopack cache growth, stop the web dev server and preview the targeted cleanup first:
Then remove the web Next dev output:
Use bun clean:dev:web --all-next-dev --dry-run when several app dev caches exist and you want to inspect every apps/*/.next/dev target before deleting them. Add --include-turbo-cache only when the report also calls out .turbo/cache growth after branch or dependency graph churn. Avoid the broad root bun clean command for this problem because it deletes dependency and build state that is unrelated to the current dev-server slowdown. Then restart the targeted app command you were using, such as bun dev:web. Avoid deleting unrelated app output while other agents or local sessions are working in the same checkout. If live RSS is the real pressure, use the latest .next/dev/trace spans and the matching process tree to reduce the compiled import graph first. Favor narrow package subpath imports and lazy-loaded settings panels before splitting an app boundary. A dedicated infrastructure app only reduces apps/web memory when infrastructure pages leave the web route graph; running apps/web and an infrastructure app together can increase total RAM. Native local dev leaves Watchpack polling off unless the environment explicitly sets WATCHPACK_POLLING. This keeps file watching lighter on macOS and Linux. If the diagnostic reports watcher EMFILE errors or route discovery stops updating on your machine, restart with polling enabled:
If the doctor warns that apps/web is missing Redis env, configure the local SRH bridge and start the Redis stack in one step:
bun redis:setup writes UPSTASH_REDIS_REST_URL=http://localhost:8079 and the matching local token into apps/web/.env.local, then runs bun redis:start. Restart bun dev:web after setup so the app picks up the new env values. To clear local Redis data while keeping the same local env wiring, run:
This starts the local Redis stack if needed, then runs FLUSHALL inside the Redis container. To stop the local dev environment, run:
This stops this repo’s Portless-registered Next.js dev servers, prunes Portless routes, removes stale static aliases for this project, stops local Supabase, and finishes by running bun doctor. Preview the cleanup first with:

Start Local Supabase Instance

To start a local supabase instance (database), run the following command:
bun
This command will start a local supabase instance on your machine. You can access the supabase instance by visiting the following URL: http://localhost:8003
You need to have Docker installed and running on your machine to start a local supabase instance.

Stop Local Supabase Instance

To stop the local supabase instance, run the following command:
standard
bun stop is broader than Supabase. It stops this repo’s local dev servers, cleans stale Portless aliases, stops Supabase, and verifies the result with bun doctor.

Better Development Experience

In case you want to run all local development servers, run the following command:
bun
Running devx will:
  1. Stop the currently running supabase instance and save current data as backup (if there is any)
  2. Install all dependencies
  3. Start a new supabase instance (using backed up data)
  4. Start all Next.js apps in development mode
If you want to have the same procedure without the backup, you can run bun devrs instead. This will:
  1. Stop the currently running supabase instance (if there is any)
  2. Install all dependencies
  3. Start a new supabase instance (with clean data from seed.sql)
  4. Start all Next.js apps in development mode
In case you don’t want to run a local supabase instance, you can run bun dev instead.

Local development

Seed accounts

There are 5 seed accounts that are already set up for local development:
  1. local@tuturuuu.com
  2. user1@tuturuuu.com
  3. user2@tuturuuu.com
  4. user3@tuturuuu.com
  5. user4@tuturuuu.com
You can use any of these accounts to log in to the app and quickly test the functionality of the app, since they are already set up with the necessary data.

Authentication

A local mail server (InBucket) is automatically set up by Supabase to handle authentication emails.
You can access the mail server by visiting the following URL: http://localhost:8004

Build

Builds run in CI on this machine’s workflow. Open a PR, wait for the relevant app build checks on its exact head commit, and inspect the preview deployment when runtime behavior needs verification. bun setup also invokes package builds, so use bun install for a new local worktree. Do not run bun run build or app-level build scripts locally. Run focused tests locally if needed; bun validate can build workspace dependencies through Turbo. Use the applicable test, type-check, lint, and build workflows on the PR’s exact head commit instead of running it here.

Test

To run all tests, run the following command:
bun
To run the Flutter mobile quality gate, use:
bun
bun check:mobile runs Dart format, Flutter analyze, and Flutter tests for apps/mobile. Flutter test output is streamed by default so slow suites still show progress instead of appearing stuck.

Verify dependency patches

Check the official stable version and publication time before resolving an update. Keep bunfig.toml’s release-age policy in force. Resolve a small coherent group in an isolated checkout using the package manager, then compare resolved entries and workspace declarations in the lockfile. A new nested entry can be required when a transitive consumer pins the old version exactly; report that constraint rather than claiming every consumer uses the new version. For example, DOMPurify 3.4.16 fixes in-place raw-text and hook-detachment behavior. Test the real browser and isomorphic packages with the owning app configurations, including safe markup, executable attributes and whole-document forms. An old version that fails the targeted regression provides a useful negative control. The contacts preview uses DOMPurify; Mail uses a separate sanitize-html contract, so tests of one sanitizer do not verify the other. Monaco 0.57.0 pins DOMPurify 3.4.15, including in its current stable registry metadata; upgrading compatible consumers does not remove that pin. Queue installs and test stages through ttr resources run after fresh resource checks, leaving urgent validation ahead of dependency maintenance. A focused test fixture does not establish full workspace resolution, type-checking or app builds. Require those results from CI on the exact implementation commit.

Mobile release builds

The native build commands verify or hydrate each flavor’s ignored Firebase SDK configuration from the tuturuuu-mobile Firebase project before compiling. The Firebase CLI must be authenticated with access to that project.
The default iOS command requires an Apple signing identity and provisioning profile. bun build:ios:ipa is an alias for the same signed release build. To compile the production app on a machine without signing credentials, use:
bun build:android only produces signed production bundles. Configure the protected upload keystore through all four ANDROID_KEYSTORE_* environment variables or apps/mobile/android/key.properties; the command fails before compilation when signing is absent and verifies the resulting AAB with jarsigner. The Mobile Store Deployment workflow hydrates these credentials in its protected runner and executes this exact command. Each store upload derives its app version from the major and minor values in apps/mobile/pubspec.yaml and the highest existing TestFlight prerelease version for that series. For example, an unused 0.11.0 ships as 0.11.0; once that version exists in TestFlight, later uploads advance to 0.11.1, then 0.11.2. An unused higher source patch is retained rather than incremented. Android and iOS receive the same version, while the unique CI build number remains separate. This does not commit a version bump; Release Please still owns the source version. Development builds remain available through bun build:android:dev. TestFlight distribution defaults to all available beta groups. Configure TESTFLIGHT_BETA_ENABLED=false to disable automatic distribution, or set TESTFLIGHT_BETA_GROUPS to a comma-separated list of group names or IDs to limit it. If Apple is still reviewing another build of the same app version, the new build remains available to internal testers and its external review is deferred. Reviews of older app versions do not block a newer version. The Mobile TestFlight Review Queue workflow checks every five minutes and can be run manually; it submits the newest processed internal build to the selected external groups without falling back to an older build. It never withdraws an earlier submission. Once an approved build is available, the retry also expires superseded builds older than 14 days while retaining the two newest uploads, the two newest approved builds, builds in review, and builds attached to an App Store release. The retry workflow accesses App Store Connect only after its code has been promoted to production. Validate the store workflow and review queue locally with node --test scripts/ci/mobile-store-workflow.test.js scripts/mobile-deployment/verify-store.test.mjs. To hydrate or validate a native Firebase configuration without compiling, run:
Tests are still a work in progress. We’re currently working on adding tests to all packages to ensure the best quality possible.

Git Conventions

Tuturuuu uses standardized conventions for Git commits and branch naming. For more information, see the Git Conventions guide to learn how to format your commits and branch names to align with our workflow.

Diagnosing a first-attempt Playwright failure

trace: 'on-first-retry' records a trace only when Playwright runs a retry. A suite with no configured retries therefore produces no trace for its initial failure. For the Inventory / Storefront cache contract, use trace: 'retain-on-failure' so the first failed navigation has an uploaded trace without adding automatic reruns. A URL assertion can complete before a streamed route enters its recovery boundary. Capture browser errors and recovery details after the failing content assertion, before deleting the local fixture. Keep the original assertion error, timeout, and shell-persistence checks intact; diagnostic failures must not mask it. The cache contract attaches bounded browser errors with Bearer/JWT values redacted and a failure screenshot. Its trace may contain local fixture request data, so keep it within the existing failure-artifact access and seven-day retention policy; never use production credentials or customer records in this fixture. Inspect the trace and attached errors before deciding whether the cause belongs to the application, fixture, or development runtime. A recovery boundary alone does not establish that a failure is a flake. A push changing this test/config already triggers e2e-tests.yaml; use that exact-head run rather than creating a duplicate manual dispatch. Manual dispatch currently selects the full E2E workflow, rather than an individual cache-contract job.

Validating Undici dependency upgrades

The direct Undici 8.x owners are Infrastructure and Web. Update their package declarations through Bun and review the lock diff: a scoped direct upgrade should change those workspace entries and the shared 8.x resolution. A recursive update can also refresh SDK/Discord consumers on maintained 6.x and 7.x lines; validate those as a separate consumer group rather than assuming the direct owners cover them. Preserve the repository’s minimum release age and lifecycle-script policy. Run the existing managed-cron network and chat link-preview tests in both owning apps using their actual Vitest configs. These checks preserve public-address DNS validation, private-address rejection and redirect handling with injected transport dependencies. They do not establish real socket, HTTP/2 or Worker transport behavior. Web’s src/lib/undici-compatibility.test.ts additionally uses the actual library to preserve a literal __proto__ cookie as an own property of a null-prototype result and retain an epoch expiry when clearing a cookie. Require exact-commit Infrastructure and Web type checks and builds on hosted CI after frozen-lock installation. Do not run local builds. A skipped build job is not verification. Infrastructure’s existing preview workflow also deploys; use a CI-only build route when deployment is outside the authorized scope.