Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 2 additions & 2 deletions apps/api/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand All @@ -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=...
Expand All @@ -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=
SENTRY_AUTH_TOKEN=
2 changes: 1 addition & 1 deletion apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/robots.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
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();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/urlSecurity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
97 changes: 97 additions & 0 deletions scripts/dev-local.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion showcase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading