diff --git a/apps/api/.env.example b/apps/api/.env.example index a1b5de443..56aae66ec 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -20,7 +20,7 @@ OPENAIAPI_KEY= # Get from OpenAI. Used in component generation and h PROVIDER_KEY_SECRET=API_KEY_SECRET_FOR_LOCAL_DEVELOPMENT_ONLY API_KEY_SECRET=API_KEY_SECRET_FOR_LOCAL_DEVELOPMENT_ONLY -PORT=3001 # Default local API port +PORT=8261 # Default local API port ## Email RESEND_API_KEY= diff --git a/apps/api/AGENTS.md b/apps/api/AGENTS.md index b386bc7e8..3a284c0c9 100644 --- a/apps/api/AGENTS.md +++ b/apps/api/AGENTS.md @@ -11,7 +11,7 @@ Detailed guidance for Claude Code agents working inside `apps/api`, the NestJS O ## Essential Commands ```bash -npm run dev # Start Nest in watch mode (port 3000 by default) +npm run dev # Start Nest in watch mode (port 8261 by default) npm run build # Compile to dist/ for production npm run start:prod # Run the compiled build npm run generate-config # Bootstraps runtime config snapshot @@ -98,7 +98,7 @@ apps/api/src 3. **Add DTOs + tests** before wiring controllers. 4. **Wire services** and ensure providers are registered inside the module. 5. **Add Swagger decorators** for every route (summary, description, auth requirements). -6. **Verify locally** with `npm run dev` and the Swagger UI at `http://localhost:3000/api`. +6. **Verify locally** with `npm run dev` and the Swagger UI at `http://localhost:8261/api`. 7. **Run lint, type-check, and tests** before committing. ## Common Pitfalls diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 1fa7132f8..81c5dbbe2 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -65,8 +65,8 @@ async function bootstrap() { await Sentry.close(2000).then(() => process.exit(1)); }); - console.log("Starting server on port", process.env.PORT || 3000); - await app.listen(process.env.PORT || 3000); + console.log("Starting server on port", process.env.PORT || 8261); + await app.listen(process.env.PORT || 8261); } function configureSwagger(app: INestApplication) { diff --git a/apps/web/.env.example b/apps/web/.env.example index c4d002783..e067d009f 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -40,7 +40,7 @@ NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_HOST= ## Hydra Smoketest Setup -NEXT_PUBLIC_TAMBO_API_URL=http://localhost:3001 +NEXT_PUBLIC_TAMBO_API_URL=http://localhost:8261 # Get this after you setup your local environment NEXT_PUBLIC_TAMBO_API_KEY= @@ -59,7 +59,7 @@ GITHUB_TOKEN= ## Nextauth login stuff # Run `openssl rand -hex 8` to generate a random secret NEXTAUTH_SECRET=... -NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_URL=http://localhost:8260 # Get these from a developer. You must use the "developer" clients here, not # "production" clients, so that they support localhost-based redirects GITHUB_CLIENT_ID=... @@ -73,4 +73,4 @@ NEXT_PUBLIC_SENTRY_ORG= NEXT_PUBLIC_SENTRY_PROJECT= # The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin. # It's used for authentication when uploading source maps. -SENTRY_AUTH_TOKEN= \ No newline at end of file +SENTRY_AUTH_TOKEN= diff --git a/apps/web/README.md b/apps/web/README.md index 7fcf2de91..d70bf52f7 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -10,7 +10,7 @@ npm install npm run dev ``` -The dev server runs on `http://localhost:3000`. +The dev server runs on `http://localhost:8260`. ## Commands diff --git a/apps/web/app/robots.ts b/apps/web/app/robots.ts index 504729ebf..47c9ac8dc 100644 --- a/apps/web/app/robots.ts +++ b/apps/web/app/robots.ts @@ -1,7 +1,7 @@ import { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://localhost:3000"; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://localhost:8260"; return { rules: { diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts index c0340751b..27f8fd55f 100644 --- a/apps/web/app/sitemap.ts +++ b/apps/web/app/sitemap.ts @@ -4,7 +4,7 @@ import { MetadataRoute } from "next"; export default async function sitemap(): Promise { try { // Use hardcoded domain for production instead of dynamic headers - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://localhost:3000"; + const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://localhost:8260"; // Get blog posts const posts = await getPostListItems(); diff --git a/apps/web/lib/config.tsx b/apps/web/lib/config.tsx index 3f7a854f2..3a44a5061 100644 --- a/apps/web/lib/config.tsx +++ b/apps/web/lib/config.tsx @@ -7,7 +7,7 @@ export const siteConfig = { name: "tambo-ai", description: "An open-source AI orchestration framework for your React front end.", - url: env.NEXT_PUBLIC_APP_URL || "http://localhost:3000", + url: env.NEXT_PUBLIC_APP_URL || "http://localhost:8260", keywords: [ "AI-Powered React Components", "Contextual UI Generation", diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 5a61d7281..6a9bc5a0e 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -39,7 +39,7 @@ export const env = createEnv({ GOOGLE_CLIENT_SECRET: z.string().transform(allowEmptyString).optional(), /** Generate with `openssl rand -hex 32` */ NEXTAUTH_SECRET: z.string().min(8), - /** URL of the client app so we can redirect back to it after auth, e.g. https://tambo.co or http://localhost:3000 */ + /** URL of the client app so we can redirect back to it after auth, e.g. https://tambo.co or http://localhost:8260 */ NEXTAUTH_URL: z.string().url(), /** Email address to send emails from. Required if using email authentication. */ EMAIL_FROM_DEFAULT: z.string().transform(allowEmptyString).optional(), diff --git a/apps/web/lib/urlSecurity.test.ts b/apps/web/lib/urlSecurity.test.ts index ddac791fb..a97aad4ce 100644 --- a/apps/web/lib/urlSecurity.test.ts +++ b/apps/web/lib/urlSecurity.test.ts @@ -6,7 +6,7 @@ describe("urlSecurity", () => { test("skips validation when ALLOW_LOCAL_MCP_SERVERS is set", async () => { jest.doMock("@/lib/env", () => ({ env: { ALLOW_LOCAL_MCP_SERVERS: "1" } })); const { validateSafeURL } = await import("@/lib/urlSecurity"); - const res = await validateSafeURL("http://localhost:3000"); + const res = await validateSafeURL("http://localhost:8260"); expect(res.safe).toBe(true); }); diff --git a/apps/web/lib/utils.test.ts b/apps/web/lib/utils.test.ts index da0c35595..7a0e09b10 100644 --- a/apps/web/lib/utils.test.ts +++ b/apps/web/lib/utils.test.ts @@ -8,7 +8,7 @@ describe("utils", () => { }); test("absoluteUrl uses siteConfig.url fallback", () => { - expect(absoluteUrl("/x")).toBe("http://localhost:3000/x"); + expect(absoluteUrl("/x")).toBe("http://localhost:8260/x"); }); test("constructMetadata builds expected metadata", () => { @@ -24,7 +24,7 @@ describe("utils", () => { // @ts-expect-error openGraph is present expect(meta.openGraph.title).toBe("Hello"); // @ts-expect-error alternates exists - expect(meta.alternates.canonical).toBe("http://localhost:3000/a"); + expect(meta.alternates.canonical).toBe("http://localhost:8260/a"); }); test("formatDate produces Today for same-day", () => { diff --git a/apps/web/package.json b/apps/web/package.json index fa9232e4e..3e5d19674 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev", + "dev": "next dev -p 8260", "build": "SKIP_ENV_VALIDATION=true next build --no-lint", "start": "next start", "lint": "eslint", diff --git a/docs/package.json b/docs/package.json index acc0e0e97..71ebade30 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "next build", "check-types": "tsc --noEmit", - "dev": "next dev --turbo", + "dev": "next dev --turbo -p 8263", "lint": "eslint .", "start": "next start", "postinstall": "fumadocs-mdx", diff --git a/package.json b/package.json index ce2833a0d..a480249bd 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,11 @@ "test": "turbo test", "dev": "turbo dev --filter=@tambo-ai/showcase --filter=@tambo-ai/docs", "dev:cloud": "turbo dev --filter=@tambo-ai-cloud/web --filter=@tambo-ai-cloud/api", + "dev:cloud:full": "turbo dev --filter=@tambo-ai-cloud/web --filter=@tambo-ai-cloud/api --filter=@tambo-ai/showcase --filter=@tambo-ai/docs", + "dev:local": "./scripts/dev-local.sh", "dev:web": "turbo dev --filter=@tambo-ai-cloud/web", "dev:api": "turbo dev --filter=@tambo-ai-cloud/api", + "dev:docs": "turbo dev --filter=@tambo-ai/docs", "sync:showcase": "tsx scripts/sync-showcase-components.ts", "sync:showcase:watch": "tsx scripts/sync-showcase-components.ts --watch" }, diff --git a/scripts/dev-local.sh b/scripts/dev-local.sh new file mode 100755 index 000000000..cd7c79659 --- /dev/null +++ b/scripts/dev-local.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Start full local dev environment with file upload support +# Usage: ./scripts/dev-local.sh + +set -e + +echo "Starting local dev environment..." + +echo "Checking that ports 8260-8263 are free (web/api/showcase/docs)..." +if ! command -v node >/dev/null 2>&1; then + echo "Node.js is required to run this script. Please install Node and try again." + exit 1 +fi + +for port in 8260 8261 8262 8263; do + if ! node - "$port" <<'NODE' +const net = require("node:net"); + +// Exit 0 if the port on 127.0.0.1 is available, 1 otherwise. +const portArg = process.argv[2]; +const port = Number.parseInt(portArg, 10); +if (!Number.isInteger(port) || port <= 0 || port > 65535) { + process.exit(1); +} + +const server = net.createServer(); +server.unref(); + +server.on("error", () => { + process.exit(1); +}); + +server.listen({ host: "127.0.0.1", port }, () => { + server.close(() => { + process.exit(0); + }); +}); +NODE + then + echo "Port ${port} is already in use. Stop the process using it, then re-run this script." + exit 1 + fi +done + +# 1. Start Postgres (if not running) +if ! docker ps | grep -q tambo_postgres; then + echo "Starting Postgres..." + docker compose --env-file docker.env up postgres -d + sleep 3 +fi + +# 2. Start Supabase (if not running) +# Use npx so developers don't need a globally installed Supabase CLI. +if ! npx --yes supabase status -o json 2>/dev/null | node <<'NODE' +const fs = require("node:fs"); + +const input = fs.readFileSync(0, "utf8").trim(); +if (input.length === 0) { + process.exit(1); +} + +let data; +try { + data = JSON.parse(input); +} catch { + process.exit(1); +} + +if (data === null || typeof data !== "object" || Array.isArray(data)) { + process.exit(1); +} + +const apiUrl = data["API_URL"]; +// `supabase status -o json` emits connection info keyed by env var names. +// Treat a non-empty `API_URL` as the signal that the local stack is running. +process.exit(typeof apiUrl === "string" && apiUrl.length > 0 ? 0 : 1); +NODE +then + echo "Starting Supabase..." + npx --yes supabase start +fi + +# 3. Run migrations (idempotent) +echo "Running migrations..." +DATABASE_URL="postgresql://postgres:postgres@localhost:5433/tambo" npm run db:migrate -w packages/db 2>/dev/null || true + +# 4. Start all dev servers +echo "" +echo "Starting dev servers..." +echo " Web: http://localhost:8260" +echo " API: http://localhost:8261" +echo " Showcase: http://localhost:8262" +echo " Docs: http://localhost:8263" +echo " Supabase: http://127.0.0.1:54423" +echo "" + +npm run dev:cloud:full diff --git a/showcase/package.json b/showcase/package.json index 1cc5fec87..61b5f4650 100644 --- a/showcase/package.json +++ b/showcase/package.json @@ -4,7 +4,7 @@ "private": true, "license": "MIT", "scripts": { - "dev": "next dev", + "dev": "next dev -p 8262", "build": "next build", "postbuild": "next-sitemap", "start": "next start", diff --git a/supabase/config.toml b/supabase/config.toml index 7ebda459b..fce03d1c8 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -97,10 +97,11 @@ file_size_limit = "50MiB" enabled = true # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used # in emails. -# site_url = "http://127.0.0.1:3000" -site_url = "http://localhost:3000" +# Keep this in sync with the local `apps/web` dev server port and your OAuth provider redirect URIs. +# site_url = "http://127.0.0.1:8260" +site_url = "http://localhost:8260" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://127.0.0.1:3000", "http://localhost:3000", "http://localhost:3000/cli-auth", "http://localhost:3000/dashboard"] +additional_redirect_urls = ["https://127.0.0.1:8260", "http://localhost:8260", "http://localhost:8260/cli-auth", "http://localhost:8260/dashboard"] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). jwt_expiry = 3600 # If disabled, the refresh token will never expire.