feat: update Dockerfile and scripts for frontend, add Turbo integration and environment configuration#22
Conversation
…on and environment configuration
|
You've used up your 5 PR reviews for this month under the Korbit Starter Plan. You'll get 5 more reviews on October 6th, 2025 or you can upgrade to Pro for unlimited PR reviews and enhanced features in your Korbit Console. |
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds Turbo orchestration and type-check scripts, reintroduces Bun+Expo frontend server and multi-stage Dockerfiles, updates frontend env/schema (removes EXPO_PUBLIC_STORAGE_PREFIX, updates API URL), refactors API/workers Dockerfiles, and adds/updates docker-compose and ignore files. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant BunServer as Bun HTTP Server (apps/frontend/server.ts)
participant Static as ./dist/client
participant Expo as Expo Request Handler (server build)
Client->>BunServer: HTTP request
alt static asset (/_expo/static/* or /assets/* or /canvaskit.wasm)
BunServer->>Static: check & read file
alt exists
Static-->>BunServer: file (200)
BunServer-->>Client: static response
else
BunServer-->>Client: 404
end
else dynamic route
BunServer->>Expo: delegate to createRequestHandler({ build: SERVER_BUILD_DIR })
Expo-->>BunServer: rendered response
BunServer-->>Client: served response
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideThis PR integrates Turborepo as the monorepo build orchestrator, standardizes dev/build scripts across projects, introduces a Bun-powered server for the frontend, and adds a multi-stage Dockerfile for containerizing the frontend app. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
CI Feedback 🧐A test triggered by this PR failed. Here is an AI-generated analysis of the failure:
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
- turbo.json is currently empty—add appropriate pipeline configurations (inputs, outputs, and dependencies) to enable caching and task orchestration.
- The Dockerfile installs turbo globally even though it’s already a devDependency—consider using the local turbo binary (node_modules/.bin/turbo) to ensure version consistency and avoid global installs.
- Add a .dockerignore to the frontend Docker build context to exclude unnecessary files and speed up image builds.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- turbo.json is currently empty—add appropriate pipeline configurations (inputs, outputs, and dependencies) to enable caching and task orchestration.
- The Dockerfile installs turbo globally even though it’s already a devDependency—consider using the local turbo binary (node_modules/.bin/turbo) to ensure version consistency and avoid global installs.
- Add a .dockerignore to the frontend Docker build context to exclude unnecessary files and speed up image builds.
## Individual Comments
### Comment 1
<location> `apps/frontend/server.ts:10` </location>
<code_context>
+const server = Bun.serve({
+ port: 4000,
+ routes: {
+ "/_expo/static/*": async (request) => {
+ const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
+ const exists = await file.exists();
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the file-serving logic into a reusable helper to eliminate code duplication across route handlers.
Here’s a quick refactor that pulls the “check-exists + return file or 404” logic into a single helper. You still end up with three route entries, but zero duplication:
```ts
import path from "node:path";
import { createRequestHandler } from "@expo/server";
import Bun from "bun";
const handler = createRequestHandler(path.join(import.meta.dir, "dist/server"));
function serveFrom(distPath: string, notFoundMsg = "Not found") {
return async (req: Request) => {
const urlPath = new URL(req.url).pathname;
const file = Bun.file(path.join(import.meta.dir, distPath, urlPath));
if (!(await file.exists())) {
return new Response(notFoundMsg, { status: 404 });
}
return new Response(file);
};
}
const server = Bun.serve({
port: 4000,
routes: {
"/_expo/static/*": serveFrom("dist/client"),
"/assets/*": serveFrom("dist/client"),
"/canvaskit.wasm": serveFrom("dist/client/canvaskit.wasm", "Not found here"),
"/*": (req) => handler(req),
},
});
// biome-ignore lint/suspicious/noConsole: logging the server URL
console.log(`Server listening on ${server.url}`);
```
Steps:
1. Create a single `serveFrom` helper that takes a base path & optional 404 message.
2. Replace each inline handler with a call to `serveFrom(...)`.
3. Keep your existing route patterns – behavior is unchanged, but duplication is gone.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||||||
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
.gitignore(1 hunks).vscode/settings.json(1 hunks)apps/api/package.json(1 hunks)apps/frontend/.env(1 hunks)apps/frontend/Dockerfile(1 hunks)apps/frontend/package.json(1 hunks)apps/frontend/server.ts(1 hunks)package.json(1 hunks)packages/db/package.json(1 hunks)turbo.json(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use object destructuring for method parameters (wrap multiple parameters in an object; define shapes with TypeScript interfaces)
Use authClient.useSession() for session state
Avoid React hooks; use Legend State observables directly (exception: Better Auth hooks)
Import UI components via path aliases (e.g., @/src/components/ui/)
Prefer union string/number literals over TypeScript enums
Use export type and import type for types
Don't use non-null assertions (!) or implicit any
Avoid TypeScript namespaces; prefer ESM modules
Files:
apps/frontend/server.ts
**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use Platform.OS and process.env.EXPO_OS for conditional platform logic
Files:
apps/frontend/server.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx,js,jsx}: Don't use the arguments object
Use for...of instead of Array.forEach
Use arrow functions instead of function expressions
Use Date.now() instead of new Date().getTime()
Use .flatMap() instead of map().flat() when possible
Prefer regex literals over RegExp constructor when possible
Use concise optional chaining instead of chained logical expressions
Always provide radix to parseInt()
Don't use var; use const/let and prefer const when single assignment
Use template literals over string concatenation
Use strict equality (===/!==)
Don't use console or debugger in committed code
Prevent import cycles and avoid namespace imports
Don't hardcode secrets (API keys/tokens) in source
Don't have unused vars, params, imports, labels, or private members
Avoid await inside loops; ensure Promise-like statements are handled
Disallow bitwise operators and comma operator
Ensure switch statements are exhaustive; default clause comes last; no fallthrough or duplicate cases
Use object spread over Object.assign when creating new objects
Prefer Array.isArray over instanceof Array; use at() instead of integer index access where appropriate
No unreachable code, duplicate conditions, or assignments in conditionals
Always use radix with parseInt; prefer Number.isNaN/Number.isFinite over globals
Disallow using global eval, with, var, and unsafe negation
Ensure typeof comparisons use valid strings; getters return values; constructors don’t return values
Files:
apps/frontend/server.ts
🧠 Learnings (1)
📚 Learning: 2025-09-02T09:56:34.362Z
Learnt from: chakrihacker
PR: fyndx/universal-starter#15
File: apps/backend/package.json:43-55
Timestamp: 2025-09-02T09:56:34.362Z
Learning: For Eden Treaty type safety in monorepo setups, TypeScript source files can be referenced directly in package.json "types" fields (e.g., "./src/index.ts") since Eden Treaty works with compile-time type inference and doesn't require emitted .d.ts files for local development.
Applied to files:
apps/api/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (5)
.vscode/settings.json (1)
35-38: Dockerfile formatter override aligns with container changes.Line 37 ensures Dockerfiles get formatted with the Azure Containers extension, matching the new Docker workflow and avoiding surprise edits when switching languages.
.gitignore (1)
20-23: Ignore Turbo cache artifacts.Line 23 keeping
.turbo/out of the repo prevents committing Turbo’s build cache while the existing*.pemrule still shields private keys—nice hygiene.packages/db/package.json (1)
7-7: Build script now covers Prisma client generation.Line 7 wiring
buildtobun run db:generatelets Turbo call into Prisma’s generator directly, so the DB package produces its client artifacts during builds without extra steps.apps/frontend/.env (1)
1-1: Staging API endpoint switch acknowledged.Line 1 points local Expo builds at the Coolify-hosted staging API, which lines up with the new Docker/Turbo staging workflow.
apps/api/package.json (1)
9-9: Type-check script completes the Turbo task.Line 9’s
check-typeshook gives Turbo a concrete command for API type checks, catching TS regressions without emitting build artifacts.
| "/_expo/static/*": async (request) => { | ||
| const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`); | ||
| const exists = await file.exists(); | ||
| return exists | ||
| ? new Response(file) | ||
| : new Response("Not found", { status: 404 }); | ||
| }, | ||
| "/assets/*": async (request) => { | ||
| const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`); | ||
| const exists = await file.exists(); | ||
| return exists | ||
| ? new Response(file) | ||
| : new Response("Not found", { status: 404 }); | ||
| }, | ||
| "/*": async (request) => await handler(request), | ||
| "/canvaskit.wasm": async (_request) => { | ||
| const file = Bun.file("./dist/client/canvaskit.wasm"); | ||
| const exists = await file.exists(); | ||
| return exists | ||
| ? new Response(file) | ||
| : new Response("Not found here", { status: 404 }); |
There was a problem hiding this comment.
Block path traversal in static asset handlers.
new URL(request.url).pathname comes straight from the caller. Because we splice it directly into ./dist/client/${…}, ../ segments are honored by the filesystem, so a request like /_expo/static/../../server.ts lets an attacker read arbitrary files from the container. Please normalize the target path against the exported client root and reject anything that escapes it before calling Bun.file. For example:
+const clientRoot = path.join(import.meta.dir, "dist/client");
+
+const serveStaticAsset = async (request: Request) => {
+ const { pathname } = new URL(request.url);
+ const assetPath = path.resolve(clientRoot, `.${pathname}`);
+ const relative = path.relative(clientRoot, assetPath);
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
+ return new Response("Not found", { status: 404 });
+ }
+ const file = Bun.file(assetPath);
+ return (await file.exists())
+ ? new Response(file)
+ : new Response("Not found", { status: 404 });
+};
+
const server = Bun.serve({
port: 4000,
routes: {
- "/_expo/static/*": async (request) => {
- const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
- const exists = await file.exists();
- return exists
- ? new Response(file)
- : new Response("Not found", { status: 404 });
- },
- "/assets/*": async (request) => {
- const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`);
- const exists = await file.exists();
- return exists
- ? new Response(file)
- : new Response("Not found", { status: 404 });
- },
- "/*": async (request) => await handler(request),
- "/canvaskit.wasm": async (_request) => {
- const file = Bun.file("./dist/client/canvaskit.wasm");
- const exists = await file.exists();
- return exists
- ? new Response(file)
- : new Response("Not found here", { status: 404 });
- },
+ "/_expo/static/*": serveStaticAsset,
+ "/assets/*": serveStaticAsset,
+ "/canvaskit.wasm": async (_request) => {
+ const file = Bun.file(path.join(clientRoot, "canvaskit.wasm"));
+ return (await file.exists())
+ ? new Response(file)
+ : new Response("Not found here", { status: 404 });
+ },
+ "/*": async (request) => handler(request),
},
});This keeps lookups confined to the exported web bundle and closes the disclosure hole.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "/_expo/static/*": async (request) => { | |
| const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`); | |
| const exists = await file.exists(); | |
| return exists | |
| ? new Response(file) | |
| : new Response("Not found", { status: 404 }); | |
| }, | |
| "/assets/*": async (request) => { | |
| const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`); | |
| const exists = await file.exists(); | |
| return exists | |
| ? new Response(file) | |
| : new Response("Not found", { status: 404 }); | |
| }, | |
| "/*": async (request) => await handler(request), | |
| "/canvaskit.wasm": async (_request) => { | |
| const file = Bun.file("./dist/client/canvaskit.wasm"); | |
| const exists = await file.exists(); | |
| return exists | |
| ? new Response(file) | |
| : new Response("Not found here", { status: 404 }); | |
| // Define the client root directory once | |
| const clientRoot = path.join(import.meta.dir, "dist/client"); | |
| // Helper to safely serve static assets under clientRoot | |
| const serveStaticAsset = async (request: Request) => { | |
| const { pathname } = new URL(request.url); | |
| // Resolve against clientRoot and force the leading “.” so that paths like "/foo" map to "./foo" | |
| const assetPath = path.resolve(clientRoot, `.${pathname}`); | |
| // Ensure the resolved path stays within clientRoot | |
| const relative = path.relative(clientRoot, assetPath); | |
| if (relative.startsWith("..") || path.isAbsolute(relative)) { | |
| return new Response("Not found", { status: 404 }); | |
| } | |
| const file = Bun.file(assetPath); | |
| return (await file.exists()) | |
| ? new Response(file) | |
| : new Response("Not found", { status: 404 }); | |
| }; | |
| const server = Bun.serve({ | |
| port: 4000, | |
| routes: { | |
| // Use the shared, hardened handler for both static routes | |
| "/_expo/static/*": serveStaticAsset, | |
| "/assets/*": serveStaticAsset, | |
| // Serve the WebAssembly blob explicitly before the catch-all | |
| "/canvaskit.wasm": async (_request) => { | |
| const file = Bun.file(path.join(clientRoot, "canvaskit.wasm")); | |
| return (await file.exists()) | |
| ? new Response(file) | |
| : new Response("Not found here", { status: 404 }); | |
| }, | |
| // Fallback to the application handler | |
| "/*": async (request) => handler(request), | |
| }, | |
| }); |
…nfigurations with environment variables
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
docker/docker-compose.prod.yml(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
| environment: | ||
| - AXIOM_DOMAIN=${AXIOM_DOMAIN:-https://api.axiom.co} | ||
| - AXIOM_TOKEN=${AXIOM_TOKEN} | ||
| - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} | ||
| - DATABASE_URL=${DATABASE_URL} | ||
| - DATASET_NAME=${DATASET_NAME:-universal_starter} | ||
| - EMAIL_PROVIDER=${EMAIL_PROVIDER:-resend} | ||
| - REDIS_URL=${REDIS_URL} | ||
| - RESEND_API_KEY=${RESEND_API_KEY} |
There was a problem hiding this comment.
Don’t rely on env_file for interpolation
These ${VAR} entries are expanded before service env_files are loaded. Unless each variable is exported in the shell (or defined in a root .env passed via --env-file), Compose will warn that they’re unset and inject empty strings, so secrets like AXIOM_TOKEN will silently become blank at runtime. Either keep the values in the service env_file only, or ensure they’re defined in the interpolation sources (shell/.env adjacent to this compose file) and mark them as required using :? if you want hard failures.(docs.docker.com)
🤖 Prompt for AI Agents
In docker/docker-compose.prod.yml around lines 21 to 29, the service is using
${VAR} interpolation which happens before service env_file loading so secrets
can be empty at runtime; either remove the interpolations and reference the
variables only via the service env_file (so the file contains plain VAR=value
entries), or ensure every interpolated variable is supplied from an
interpolation source (exported in the shell or placed in a root .env adjacent to
this compose file) and make required variables fail fast using the ${VAR:?error}
form; for secrets prefer external env_file or Docker secrets rather than inline
interpolation to avoid accidental empty values.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/frontend/.env (1)
1-1: Ensure no double slashes in auth-client baseURLIn
apps/frontend/src/lib/auth-client.tsat line 22, you buildbaseURLas`${env.EXPO_PUBLIC_API_URL}/api/auth`Since
EXPO_PUBLIC_API_URLnow ends with a slash, this yields//api/auth. Either strip the trailing slash fromEXPO_PUBLIC_API_URL(e.g.env.EXPO_PUBLIC_API_URL.replace(/\/$/, '')) or switch to a URL-safe join such asnew URL('/api/auth', env.EXPO_PUBLIC_API_URL).toString()to normalize paths.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/frontend/.env(1 hunks)apps/frontend/.env.example(1 hunks)apps/frontend/Dockerfile(1 hunks)apps/frontend/src/config/env.ts(1 hunks)docker-compose.yml(1 hunks)docker/Dockerfile.base(0 hunks)
💤 Files with no reviewable changes (1)
- docker/Dockerfile.base
✅ Files skipped from review due to trivial changes (1)
- docker-compose.yml
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/frontend/Dockerfile
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use object destructuring for method parameters (wrap multiple parameters in an object; define shapes with TypeScript interfaces)
Use authClient.useSession() for session state
Avoid React hooks; use Legend State observables directly (exception: Better Auth hooks)
Import UI components via path aliases (e.g., @/src/components/ui/)
Prefer union string/number literals over TypeScript enums
Use export type and import type for types
Don't use non-null assertions (!) or implicit any
Avoid TypeScript namespaces; prefer ESM modules
Files:
apps/frontend/src/config/env.ts
**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use Platform.OS and process.env.EXPO_OS for conditional platform logic
Files:
apps/frontend/src/config/env.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx,js,jsx}: Don't use the arguments object
Use for...of instead of Array.forEach
Use arrow functions instead of function expressions
Use Date.now() instead of new Date().getTime()
Use .flatMap() instead of map().flat() when possible
Prefer regex literals over RegExp constructor when possible
Use concise optional chaining instead of chained logical expressions
Always provide radix to parseInt()
Don't use var; use const/let and prefer const when single assignment
Use template literals over string concatenation
Use strict equality (===/!==)
Don't use console or debugger in committed code
Prevent import cycles and avoid namespace imports
Don't hardcode secrets (API keys/tokens) in source
Don't have unused vars, params, imports, labels, or private members
Avoid await inside loops; ensure Promise-like statements are handled
Disallow bitwise operators and comma operator
Ensure switch statements are exhaustive; default clause comes last; no fallthrough or duplicate cases
Use object spread over Object.assign when creating new objects
Prefer Array.isArray over instanceof Array; use at() instead of integer index access where appropriate
No unreachable code, duplicate conditions, or assignments in conditionals
Always use radix with parseInt; prefer Number.isNaN/Number.isFinite over globals
Disallow using global eval, with, var, and unsafe negation
Ensure typeof comparisons use valid strings; getters return values; constructors don’t return values
Files:
apps/frontend/src/config/env.ts
🪛 dotenv-linter (3.3.0)
apps/frontend/.env.example
[warning] 1-1: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
apps/frontend/.env
[warning] 1-1: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (1)
apps/frontend/src/config/env.ts (1)
4-9: No lingeringEXPO_PUBLIC_STORAGE_PREFIXusages found—please verify non-code sources
Ripgrep returned zero matches for the removed variable. Manually confirm it isn’t referenced in Docker/CI configs, documentation or uncommitted.envfiles.
| @@ -1,2 +1 @@ | |||
| EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com | |||
| EXPO_PUBLIC_STORAGE_PREFIX=universal-starter No newline at end of file | |||
| EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com No newline at end of file | |||
There was a problem hiding this comment.
Synchronize example API URL with the new default
.env now defaults to https://staging-universal-api.coolify.fyndx.io/, but the example still points at the old Render endpoint. Anyone bootstrapping from the example will talk to the wrong API. Please align the example with the current default.
-EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com
+EXPO_PUBLIC_API_URL=https://staging-universal-api.coolify.fyndx.io/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| EXPO_PUBLIC_API_URL=https://universal-starter.onrender.com | |
| # apps/frontend/.env.example | |
| EXPO_PUBLIC_API_URL=https://staging-universal-api.coolify.fyndx.io/ |
🧰 Tools
🪛 dotenv-linter (3.3.0)
[warning] 1-1: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🤖 Prompt for AI Agents
In apps/frontend/.env.example around lines 1 to 1, the EXPO_PUBLIC_API_URL value
still points to the old Render endpoint; update this example value to match the
new default API URL (https://staging-universal-api.coolify.fyndx.io/) so that
developers bootstrapping from the example use the current default endpoint.
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
apps/frontend/package.json(1 hunks)apps/frontend/server.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Use object destructuring for method parameters (wrap multiple parameters in an object; define shapes with TypeScript interfaces)
Use authClient.useSession() for session state
Avoid React hooks; use Legend State observables directly (exception: Better Auth hooks)
Import UI components via path aliases (e.g., @/src/components/ui/)
Prefer union string/number literals over TypeScript enums
Use export type and import type for types
Don't use non-null assertions (!) or implicit any
Avoid TypeScript namespaces; prefer ESM modules
Files:
apps/frontend/server.ts
**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Use Platform.OS and process.env.EXPO_OS for conditional platform logic
Files:
apps/frontend/server.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx,js,jsx}: Don't use the arguments object
Use for...of instead of Array.forEach
Use arrow functions instead of function expressions
Use Date.now() instead of new Date().getTime()
Use .flatMap() instead of map().flat() when possible
Prefer regex literals over RegExp constructor when possible
Use concise optional chaining instead of chained logical expressions
Always provide radix to parseInt()
Don't use var; use const/let and prefer const when single assignment
Use template literals over string concatenation
Use strict equality (===/!==)
Don't use console or debugger in committed code
Prevent import cycles and avoid namespace imports
Don't hardcode secrets (API keys/tokens) in source
Don't have unused vars, params, imports, labels, or private members
Avoid await inside loops; ensure Promise-like statements are handled
Disallow bitwise operators and comma operator
Ensure switch statements are exhaustive; default clause comes last; no fallthrough or duplicate cases
Use object spread over Object.assign when creating new objects
Prefer Array.isArray over instanceof Array; use at() instead of integer index access where appropriate
No unreachable code, duplicate conditions, or assignments in conditionals
Always use radix with parseInt; prefer Number.isNaN/Number.isFinite over globals
Disallow using global eval, with, var, and unsafe negation
Ensure typeof comparisons use valid strings; getters return values; constructors don’t return values
Files:
apps/frontend/server.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (1)
apps/frontend/server.ts (1)
13-26: Sanitize static file paths to prevent traversal
new URL(request.url).pathnamegoes straight into./dist/client/${…}. Requests such as/_expo/static/../../server.tsescape the client bundle and expose arbitrary files from the container. Normalize the target against a fixedclientRoot, reject anything that leaves it, and reuse the sanitized path for all static routes.-const server = Bun.serve({ - port: 4000, - routes: { - "/_expo/static/*": async (request) => { - const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`); - const exists = await file.exists(); - return exists - ? new Response(file) - : new Response("Not found", { status: 404 }); - }, - "/assets/*": async (request) => { - const file = Bun.file(`./dist/client/${new URL(request.url).pathname}`); - const exists = await file.exists(); - return exists - ? new Response(file) - : new Response("Not found", { status: 404 }); - }, +const clientRoot = path.join(import.meta.dir, "dist/client"); + +const serveStatic = async (request: Request) => { + const { pathname } = new URL(request.url); + const resolved = path.resolve(clientRoot, `.${pathname}`); + if (!resolved.startsWith(clientRoot + path.sep)) { + return new Response("Not found", { status: 404 }); + } + const file = Bun.file(resolved); + return (await file.exists()) + ? new Response(file) + : new Response("Not found", { status: 404 }); +}; + +const server = Bun.serve({ + port: 4000, + routes: { + "/_expo/static/*": serveStatic, + "/assets/*": serveStatic,
… and dependency management
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
apps/frontend/Dockerfile (1)
7-10: Consider if watchman is necessary in the builder stage.Watchman is typically used for file watching during development, which isn't needed during Docker builds. This adds unnecessary dependencies and increases build time.
-RUN apt-get update && \ - apt-get install -y watchman && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* +# Removed watchman as it's not needed for Docker builds
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/api/.dockerignore(1 hunks)apps/api/Dockerfile(1 hunks)apps/frontend/Dockerfile(1 hunks)apps/workers/Dockerfile(1 hunks)apps/workers/package.json(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/workers/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Sourcery review
🔇 Additional comments (4)
apps/api/.dockerignore (1)
1-96: Comprehensive .dockerignore patterns look good.The expanded ignore rules provide excellent coverage for dependencies, build artifacts, environment files, and development-specific directories. This significantly reduces the Docker build context size and prevents sensitive files from being included in the image.
The patterns are well-organized into logical sections and follow Docker best practices.
apps/workers/Dockerfile (1)
1-4: LGTM: Standard multi-stage setup with global turbo.The base image setup and global turbo installation follow the consistent pattern across services.
apps/api/Dockerfile (2)
2-4: LGTM: Consistent multi-stage setup.The base image setup with global turbo installation follows the established pattern across all services.
22-23: Good practice: Creating public directory before build.Ensuring the public directory exists before the build prevents potential build failures.
| # Copy built server and public assets | ||
| COPY --from=installer /app/apps/api/server server | ||
| COPY --from=installer /app/apps/api/.env* ./ | ||
| COPY --from=installer /app/apps/api/public ./public | ||
|
|
There was a problem hiding this comment.
Security concern: Environment file handling and user permissions.
Similar to the workers Dockerfile, copying .env* files to the production image may expose sensitive configuration. Also consider running as non-root user.
# Production image
FROM gcr.io/distroless/base
WORKDIR /app
+USER nonroot
# Copy built server and public assets
COPY --from=installer /app/apps/api/server server
-COPY --from=installer /app/apps/api/.env* ./
+# Consider using environment variables from Docker Compose instead
COPY --from=installer /app/apps/api/public ./public📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Copy built server and public assets | |
| COPY --from=installer /app/apps/api/server server | |
| COPY --from=installer /app/apps/api/.env* ./ | |
| COPY --from=installer /app/apps/api/public ./public | |
| # Production image | |
| FROM gcr.io/distroless/base | |
| WORKDIR /app | |
| USER nonroot | |
| # Copy built server and public assets | |
| COPY --from=installer /app/apps/api/server server | |
| # Consider using environment variables from Docker Compose instead | |
| COPY --from=installer /app/apps/api/public ./public |
🤖 Prompt for AI Agents
In apps/api/Dockerfile around lines 29 to 33, the Dockerfile copies `.env*` into
the production image and runs as root, which risks leaking secrets and running
with excessive privileges; remove the COPY of `.env*` from the final image (use
build-time ARGs, Docker secrets, or environment variables injected by the
orchestrator instead) and ensure the container runs as a non-root user by adding
a non-root USER (create/group and chown files or set appropriate permissions in
the builder stage) so the runtime image contains no sensitive env files and does
not run as root.
| ARG EXPO_PUBLIC_API_URL | ||
| ENV EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL |
There was a problem hiding this comment.
Add default value for build argument.
The EXPO_PUBLIC_API_URL build argument lacks a default value, which could cause build failures if not provided.
-ARG EXPO_PUBLIC_API_URL
+ARG EXPO_PUBLIC_API_URL=http://localhost:3000
ENV EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL🤖 Prompt for AI Agents
In apps/frontend/Dockerfile around lines 22 to 23, the build ARG
EXPO_PUBLIC_API_URL has no default which can break builds when not supplied;
update the ARG to include a sensible default (for example ARG
EXPO_PUBLIC_API_URL=http://localhost:3000 or an empty string) and keep the ENV
EXPO_PUBLIC_API_URL=$EXPO_PUBLIC_API_URL line so the environment variable is
populated when no build-arg is provided.
| FROM gcr.io/distroless/base AS web | ||
| WORKDIR /app | ||
| ENV NODE_ENV=production | ||
| COPY --from=installer /app/apps/frontend/web-server /app/server | ||
| COPY --from=installer /app/apps/frontend/dist /app/dist | ||
| CMD [ "./server" ] | ||
| EXPOSE 4000 No newline at end of file |
There was a problem hiding this comment.
Fix CMD syntax and add security user directive.
The CMD syntax should use array format, and consider running as non-root user for better security.
FROM gcr.io/distroless/base AS web
WORKDIR /app
ENV NODE_ENV=production
+USER nonroot
COPY --from=installer /app/apps/frontend/web-server /app/server
COPY --from=installer /app/apps/frontend/dist /app/dist
-CMD [ "./server" ]
+CMD ["./server"]
EXPOSE 4000📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| FROM gcr.io/distroless/base AS web | |
| WORKDIR /app | |
| ENV NODE_ENV=production | |
| COPY --from=installer /app/apps/frontend/web-server /app/server | |
| COPY --from=installer /app/apps/frontend/dist /app/dist | |
| CMD [ "./server" ] | |
| EXPOSE 4000 | |
| FROM gcr.io/distroless/base AS web | |
| WORKDIR /app | |
| ENV NODE_ENV=production | |
| USER nonroot | |
| COPY --from=installer /app/apps/frontend/web-server /app/server | |
| COPY --from=installer /app/apps/frontend/dist /app/dist | |
| CMD ["./server"] | |
| EXPOSE 4000 |
🤖 Prompt for AI Agents
In apps/frontend/Dockerfile around lines 28-34, change the CMD to JSON/exec form
and run the container as a non-root user: update CMD to use array syntax (e.g.
["./server"]) and add a USER directive (e.g. USER 1000) to drop root privileges;
ensure the installer stage sets ownership of /app, /app/server and /app/dist to
that UID (chown in the installer stage or create the user there) so the non-root
user can read/execute the files before copying into the distroless image.
| COPY --from=installer /app/apps/workers/worker worker | ||
| COPY --from=installer /app/apps/workers/.env* ./ | ||
|
|
There was a problem hiding this comment.
Security concern: Review environment file handling.
Copying .env* files to the production image may expose sensitive configuration. Consider using explicit environment variables in the Docker Compose configuration instead of copying files.
Additionally, the distroless image runs as root by default. Consider adding a USER directive if the worker doesn't require root privileges.
# Production image
FROM gcr.io/distroless/base
WORKDIR /app
+# Run as non-root user (distroless provides nonroot user)
+USER nonroot
COPY --from=installer /app/apps/workers/worker worker
-COPY --from=installer /app/apps/workers/.env* ./
+# Consider using environment variables from Docker Compose instead of copying .env files📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| COPY --from=installer /app/apps/workers/worker worker | |
| COPY --from=installer /app/apps/workers/.env* ./ | |
| # Production image | |
| FROM gcr.io/distroless/base | |
| WORKDIR /app | |
| # Run as non-root user (distroless provides nonroot user) | |
| USER nonroot | |
| COPY --from=installer /app/apps/workers/worker worker | |
| # Consider using environment variables from Docker Compose instead of copying .env files |
🤖 Prompt for AI Agents
In apps/workers/Dockerfile around lines 27-29, the Dockerfile copies `.env*`
into the production image (exposing secrets) and the distroless base runs as
root; remove the COPY --from=installer /app/apps/workers/.env* ./ line and
instead surface required values via explicit build-time ARGs or runtime
environment variables passed in Docker Compose/Kubernetes (or mount a secrets
file at runtime), update documentation to list required env names and defaults,
and add a non-root USER directive appropriate for the distroless image (create
or switch to a low-privilege user) so the worker does not run as root.
Summary by Sourcery
Integrate Turborepo and Bun workflows across the monorepo, modernize package scripts, introduce a Bun-based production server for the frontend, and add a multi-stage Dockerfile for building and serving the frontend application.
New Features:
Enhancements:
Build: