Skip to content

chore: add local full-stack dev setup#573

Open
sidpalas wants to merge 10 commits intoColeMurray:mainfrom
sidpalas:sp/full-stack-local-dev
Open

chore: add local full-stack dev setup#573
sidpalas wants to merge 10 commits intoColeMurray:mainfrom
sidpalas:sp/full-stack-local-dev

Conversation

@sidpalas
Copy link
Copy Markdown
Contributor

@sidpalas sidpalas commented Apr 29, 2026

Addresses #486

Summary

  • Adds a new local full-stack setup flow for running the web app and control plane against real GitHub and sandbox services.
  • Centralizes local env management around root .env.local, with generated service-specific env files for web and control-plane.
  • Adds local D1 migration support and npm scripts for local full-stack development.

@ColeMurray -- what do you think of this env management approach: using root .env.local as the single editable source of truth, then generating service-specific env files for web and control-plane? Open to suggestions if you have a preferred approach!

Future Work

  • Extend the root env generation flow to bot integrations such as Slack, GitHub, and Linear

Summary by CodeRabbit

  • Documentation

    • Added end-to-end local development guides for running the web app and control plane together and updated the setup guide with a new local path.
  • New Features

    • Added a .env example template and CLI scripts to generate per-service env files, provision sandbox secrets, and run local migrations/servers.
  • Style

    • Added linting overrides for maintenance scripts.
  • Chores

    • Added npm scripts and a dotenv dev dependency to streamline local workflows.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68b5651b-689c-4b55-b421-bc2a5d32fd4a

📥 Commits

Reviewing files that changed from the base of the PR and between 0e7ad2d and fda4065.

📒 Files selected for processing (3)
  • scripts/d1-migrate.sh
  • scripts/env-utils.mjs
  • scripts/modal-secrets.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/modal-secrets.mjs

📝 Walkthrough

Walkthrough

Adds environment templates, validation and serialization utilities, CLI scripts to generate per-service env files and provision Modal secrets, docs for running the web app and control plane locally with real GitHub OAuth and remote sandboxes, npm scripts, and D1 migration script improvements for local/remote modes.

Changes

Cohort / File(s) Summary
Env template & docs
/.env.example, docs/LOCAL_WEB_CONTROL_PLANE.md, docs/SETUP_GUIDE.md
Adds .env.example; documents end-to-end local web + control plane workflow with GitHub App, tunneling (ngrok), Modal/Daytona sandboxes, and guidance for env handling and secrets syncing; updates setup guide adding "Path C".
Scripts linting & package scripts
eslint.config.js, package.json, packages/control-plane/package.json
Adds ESLint override for scripts/ (Node/ES2022, allows console); adds dev helper npm scripts (dev:env, dev:modal-secrets, dev:db:local, dev:control-plane, dev:web) and dotenv devDependency; adds dev script for control-plane wrangler server.
Env handling & distribution
scripts/env-utils.mjs, scripts/local-env.mjs
New env-utils.mjs exports paths, key groups, required-key resolution, env loading/normalization (unescape \\n), derived/public key derivation, validation, serialization, and write helpers. local-env.mjs validates root env and writes packages/web/.env.local and packages/control-plane/.dev.vars.
Modal secrets provisioning
scripts/modal-secrets.mjs
Adds CLI that reads root env, determines if Modal secrets are required, computes allowed host(s), and runs Modal CLI secret create commands for llm-api-keys, github-app, and internal-api, failing fast on errors.
D1 migration & infra invocation
scripts/d1-migrate.sh, terraform/environments/production/d1.tf
Refactors migration script to accept mode (local/remote) via D1_OPTIONS, tightens filename parsing, uses printf, updates applied-version parsing; Terraform now invokes the script with remote arg.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Developer
  participant LocalScripts as "Local Scripts\n(env-utils, local-env, modal-secrets)"
  participant Web as "Web App\nlocalhost:3000"
  participant ControlPlane as "Control Plane\nlocalhost:8787"
  participant Modal as "Modal Sandbox / Secrets"
  participant GitHub as "GitHub App OAuth"
  participant D1 as "D1 Database"

  Developer->>LocalScripts: run `npm run dev:env`
  LocalScripts->>LocalScripts: read & normalize root `.env.local`
  LocalScripts->>Web: write `packages/web/.env.local`
  LocalScripts->>ControlPlane: write `packages/control-plane/.dev.vars`
  Developer->>LocalScripts: run `npm run dev:modal-secrets`
  LocalScripts->>Modal: create/update secrets (llm-api-keys, github-app, internal-api)
  Developer->>GitHub: configure App callback to CONTROL_PLANE_URL
  Developer->>ControlPlane: start control plane (wrangler dev)
  Developer->>Web: start web dev server
  Developer->>D1: run `scripts/d1-migrate.sh` (local/remote)
  D1-->>Developer: migration results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through envs by lantern light,
Keys unescaped, secrets tucked in tight,
Modal hummed and tunnels gleamed,
Control plane answered, web app beamed,
A carrot-coded warren — devs, delight! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding infrastructure and tooling for local full-stack development (web app and control plane together).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/SETUP_GUIDE.md (1)

45-57: ⚠️ Potential issue | 🟡 Minor

Separate required vs optional quick-check commands.

Putting optional tools in the same block as required checks can make Path A/B users think setup failed when those binaries are intentionally absent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/SETUP_GUIDE.md` around lines 45 - 57, Split the single "Quick check"
code block into two labeled blocks (Required checks and Optional checks) in
docs/SETUP_GUIDE.md: create one fenced block containing the truly required
commands (e.g., node -v, npm -v, git --version, python --version) and a second
fenced block titled "Optional checks" containing tools like uv --version, modal
--version, npx wrangler --version, jq --version, ngrok --version; ensure
headings or short notes clarify that absence of optional tools is acceptable so
Path A/B users won't treat missing optional binaries as failures.
🧹 Nitpick comments (6)
scripts/local-env.mjs (1)

23-24: Consider making the post-step hint provider-aware.

The script always prompts Modal secret sync; this can be confusing when SANDBOX_PROVIDER is set to a non-Modal path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/local-env.mjs` around lines 23 - 24, The two console.log hints that
tell users to "Next, sync Modal secrets..." should be conditional on the sandbox
provider; check process.env.SANDBOX_PROVIDER and only print those console.log
messages when the provider indicates Modal (e.g., equals or contains "modal");
otherwise skip or print a provider-appropriate hint. Update the code around the
existing console.log calls so the messages are gated by SANDBOX_PROVIDER and
keep the original messages unchanged when shown.
.env.example (1)

5-58: Optional: reorder keys if dotenv-linter warnings are CI-gated.

Current grouping is readable, but it triggers multiple UnorderedKey warnings. If dotenv-linter is enforced, reordering will keep CI clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 5 - 58, Dotenv-linter is flagging UnorderedKey
warnings; reorder the keys in .env.example so they follow the linter's expected
order (typically alphabetical) to remove those warnings. Specifically, sort the
environment variable entries (for example GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY,
GITHUB_APP_INSTALLATION_ID, SANDBOX_PROVIDER, MODAL_TOKEN_ID,
MODAL_TOKEN_SECRET, MODAL_WORKSPACE, MODAL_API_SECRET, DAYTONA_API_KEY,
DAYTONA_API_URL, DAYTONA_BASE_SNAPSHOT, ANTHROPIC_API_KEY, TOKEN_ENCRYPTION_KEY,
REPO_SECRETS_ENCRYPTION_KEY, INTERNAL_CALLBACK_SECRET, NEXTAUTH_SECRET,
NEXTAUTH_URL, CONTROL_PLANE_URL, NEXT_PUBLIC_WS_URL, WEB_APP_URL, WORKER_URL,
DEPLOYMENT_NAME, ALLOWED_USERS, ALLOWED_EMAIL_DOMAINS, UNSAFE_ALLOW_ALL_USERS,
SCM_PROVIDER, LOG_LEVEL) while preserving comment blocks and example values; run
dotenv-linter locally or in CI to confirm warnings are resolved.
scripts/env-utils.mjs (1)

122-127: Silent error swallowing hides root cause.

The empty catch block discards the actual filesystem error (e.g., permission denied vs. file not found). Logging the error before the user-friendly message would help debugging.

🔧 Proposed improvement
   try {
     source = readFileSync(sourcePath, "utf8");
-  } catch {
+  } catch (err) {
+    if (process.env.DEBUG) console.error(err);
     console.error("Missing .env.local. Copy .env.example to .env.local and fill in real values.");
     process.exit(1);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/env-utils.mjs` around lines 122 - 127, The catch currently swallows
the actual filesystem error when calling readFileSync(sourcePath, "utf8") and
only prints a generic message; change the catch to capture the thrown error
(e.g., catch (err)) and include that error in the log before calling
process.exit(1) so you see details like ENOENT or EACCES; update the
console.error call(s) around readFileSync/source to print both a user-friendly
instruction and the captured error (or use console.error("...", err)) to
preserve root-cause information.
scripts/modal-secrets.mjs (2)

5-17: shellWords edge case: empty string returns [""] instead of [].

When command is an empty string, the regex matches nothing, so the nullish coalescing returns [""]. This could cause spawnSync to fail cryptically if MODAL_CLI="" is set.

🛡️ Proposed fix to handle empty command
 function shellWords(command) {
+  if (!command?.trim()) {
+    return ["modal"];
+  }
   return (
     command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/modal-secrets.mjs` around lines 5 - 17, The shellWords function
returns [""] for an empty command because the regex match is null and the
fallback uses [command]; update shellWords to treat empty or all-whitespace
command as no words: if command.trim() === "" return [] (or change the nullish
fallback to []). Locate the shellWords function and replace the current
nullish-coalescing fallback ([command]) with a guard that returns an empty array
for empty input so callers like spawnSync receive [] instead of [""]. Ensure the
function still strips surrounding quotes for non-empty inputs.

31-34: Consider logging the command on failure for debugging.

When spawnSync fails, the script exits but doesn't indicate which secret creation failed if the Modal CLI itself doesn't print useful output. The console.log on line 30 helps, but capturing and logging stderr on failure could aid troubleshooting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/modal-secrets.mjs` around lines 31 - 34, When handling the spawnSync
call that assigns to result, enhance the failure logging so you print which
command failed and the captured stderr/stdout; specifically, after const result
= spawnSync(command, args, {...}) check result.status or result.error and log a
clear message including the command and args (or reconstructed command string)
and result.stderr (and result.stdout) before exiting. Reference the existing
symbols spawnSync, result, command, args, root and the current console.log so
the new log appears when the Modal CLI fails.
docs/LOCAL_WEB_CONTROL_PLANE.md (1)

31-46: Document ngrok alternatives or free-tier limitations.

ngrok's free tier cycles URLs on restart. Consider mentioning this and alternatives (e.g., cloudflared tunnel, localhost.run) for developers who hit rate limits or need stable URLs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/LOCAL_WEB_CONTROL_PLANE.md` around lines 31 - 46, Update the section
that instructs to run "ngrok http 8787" to note ngrok's free-tier behavior
(ephemeral URLs on restart and potential rate limits) and add brief alternatives
such as "cloudflared tunnel" and "localhost.run" with a short line about using a
stable tunnel for production/local development; also update the example env
variables (CONTROL_PLANE_URL, NEXT_PUBLIC_WS_URL, WORKER_URL) to mention
replacing them with your tunnel URL and call out that stable paid ngrok plans or
cloudflared provide persistent hostnames if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/d1-migrate.sh`:
- Line 53: The script sets VERSION from FILENAME using VERSION=$(printf "%s"
"$FILENAME" | grep -oE '^[0-9]+') but does not validate that VERSION is
non-empty; add a check right after that assignment to ensure VERSION contains
only digits (e.g., test -n "$VERSION" && [[ "$VERSION" =~ ^[0-9]+$ ]]) and if it
fails print an error referencing the offending FILENAME and exit non‑zero so the
SQL execution/tracking steps are not run with an empty version; ensure error
messages mention the variables VERSION and FILENAME so it's clear what failed.
- Around line 44-46: The APPLIED assignment masks errors by appending
"2>/dev/null || true" to the WRANGLER/jq pipeline; remove the stderr redirection
and the "|| true" so failures in the command that queries _schema_migrations
propagate (fail the script under set -euo pipefail). Locate the APPLIED
assignment (the line using WRANGLER d1 execute with DATABASE_NAME and D1_OPTIONS
and jq -r '.[0].results[].version // empty') and update it to let errors surface
instead of silencing them.

In `@scripts/env-utils.mjs`:
- Around line 5-6: The current root and sourcePath definitions use resolve(new
URL("..", import.meta.url).pathname) which yields POSIX-style paths on Windows;
replace that URL->pathname usage with Node's fileURLToPath to get a
platform-correct path. Import fileURLToPath from "url" and use fileURLToPath(new
URL("..", import.meta.url)) (or fileURLToPath(import.meta.url) combined with
dirname) when computing root and then resolve(root, ".env.local") for
sourcePath; update the symbols root and sourcePath accordingly so resolve
receives a proper OS path.

In `@scripts/modal-secrets.mjs`:
- Around line 55-56: The code constructs controlPlaneUrl with new
URL(env.CONTROL_PLANE_URL) which will throw a TypeError for malformed URLs;
update the initialization of controlPlaneUrl/allowedHosts to validate and handle
bad input: wrap the new URL(...) in a try/catch or explicitly validate
env.CONTROL_PLANE_URL before constructing controlPlaneUrl, and on failure throw
or log a clear error (including the invalid value) and exit, or fall back to a
safe default; reference controlPlaneUrl, allowedHosts, and env.CONTROL_PLANE_URL
when making this change so the script no longer crashes with an unhandled
exception for malformed CONTROL_PLANE_URL.

---

Outside diff comments:
In `@docs/SETUP_GUIDE.md`:
- Around line 45-57: Split the single "Quick check" code block into two labeled
blocks (Required checks and Optional checks) in docs/SETUP_GUIDE.md: create one
fenced block containing the truly required commands (e.g., node -v, npm -v, git
--version, python --version) and a second fenced block titled "Optional checks"
containing tools like uv --version, modal --version, npx wrangler --version, jq
--version, ngrok --version; ensure headings or short notes clarify that absence
of optional tools is acceptable so Path A/B users won't treat missing optional
binaries as failures.

---

Nitpick comments:
In @.env.example:
- Around line 5-58: Dotenv-linter is flagging UnorderedKey warnings; reorder the
keys in .env.example so they follow the linter's expected order (typically
alphabetical) to remove those warnings. Specifically, sort the environment
variable entries (for example GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET,
GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_INSTALLATION_ID,
SANDBOX_PROVIDER, MODAL_TOKEN_ID, MODAL_TOKEN_SECRET, MODAL_WORKSPACE,
MODAL_API_SECRET, DAYTONA_API_KEY, DAYTONA_API_URL, DAYTONA_BASE_SNAPSHOT,
ANTHROPIC_API_KEY, TOKEN_ENCRYPTION_KEY, REPO_SECRETS_ENCRYPTION_KEY,
INTERNAL_CALLBACK_SECRET, NEXTAUTH_SECRET, NEXTAUTH_URL, CONTROL_PLANE_URL,
NEXT_PUBLIC_WS_URL, WEB_APP_URL, WORKER_URL, DEPLOYMENT_NAME, ALLOWED_USERS,
ALLOWED_EMAIL_DOMAINS, UNSAFE_ALLOW_ALL_USERS, SCM_PROVIDER, LOG_LEVEL) while
preserving comment blocks and example values; run dotenv-linter locally or in CI
to confirm warnings are resolved.

In `@docs/LOCAL_WEB_CONTROL_PLANE.md`:
- Around line 31-46: Update the section that instructs to run "ngrok http 8787"
to note ngrok's free-tier behavior (ephemeral URLs on restart and potential rate
limits) and add brief alternatives such as "cloudflared tunnel" and
"localhost.run" with a short line about using a stable tunnel for
production/local development; also update the example env variables
(CONTROL_PLANE_URL, NEXT_PUBLIC_WS_URL, WORKER_URL) to mention replacing them
with your tunnel URL and call out that stable paid ngrok plans or cloudflared
provide persistent hostnames if needed.

In `@scripts/env-utils.mjs`:
- Around line 122-127: The catch currently swallows the actual filesystem error
when calling readFileSync(sourcePath, "utf8") and only prints a generic message;
change the catch to capture the thrown error (e.g., catch (err)) and include
that error in the log before calling process.exit(1) so you see details like
ENOENT or EACCES; update the console.error call(s) around readFileSync/source to
print both a user-friendly instruction and the captured error (or use
console.error("...", err)) to preserve root-cause information.

In `@scripts/local-env.mjs`:
- Around line 23-24: The two console.log hints that tell users to "Next, sync
Modal secrets..." should be conditional on the sandbox provider; check
process.env.SANDBOX_PROVIDER and only print those console.log messages when the
provider indicates Modal (e.g., equals or contains "modal"); otherwise skip or
print a provider-appropriate hint. Update the code around the existing
console.log calls so the messages are gated by SANDBOX_PROVIDER and keep the
original messages unchanged when shown.

In `@scripts/modal-secrets.mjs`:
- Around line 5-17: The shellWords function returns [""] for an empty command
because the regex match is null and the fallback uses [command]; update
shellWords to treat empty or all-whitespace command as no words: if
command.trim() === "" return [] (or change the nullish fallback to []). Locate
the shellWords function and replace the current nullish-coalescing fallback
([command]) with a guard that returns an empty array for empty input so callers
like spawnSync receive [] instead of [""]. Ensure the function still strips
surrounding quotes for non-empty inputs.
- Around line 31-34: When handling the spawnSync call that assigns to result,
enhance the failure logging so you print which command failed and the captured
stderr/stdout; specifically, after const result = spawnSync(command, args,
{...}) check result.status or result.error and log a clear message including the
command and args (or reconstructed command string) and result.stderr (and
result.stdout) before exiting. Reference the existing symbols spawnSync, result,
command, args, root and the current console.log so the new log appears when the
Modal CLI fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8ee263a-a68b-4a1b-99d6-71c091868630

📥 Commits

Reviewing files that changed from the base of the PR and between 6a4fdc7 and 0e7ad2d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • .env.example
  • docs/LOCAL_WEB_CONTROL_PLANE.md
  • docs/SETUP_GUIDE.md
  • eslint.config.js
  • package.json
  • packages/control-plane/package.json
  • scripts/d1-migrate.sh
  • scripts/env-utils.mjs
  • scripts/local-env.mjs
  • scripts/modal-secrets.mjs
  • terraform/environments/production/d1.tf

Comment thread scripts/d1-migrate.sh Outdated
Comment thread scripts/d1-migrate.sh Outdated
Comment thread scripts/env-utils.mjs Outdated
Comment thread scripts/modal-secrets.mjs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant