diff --git a/.env.example b/.env.example index 6fa3bc1..ba7e119 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,14 @@ JWT_SECRET=host39-dev-secret JWT_EXPIRES_IN=7d FRONTEND_URL=http://localhost:3002 NEXT_PUBLIC_HOST39_API_URL=http://localhost:3010 + +# Host that serves public cards. In local dev the API serves them on the same +# origin, so this matches NEXT_PUBLIC_HOST39_API_URL. In production these differ +# (cards live on agentcards.host39.org). +PUBLIC_BASE_URL=http://localhost:3010 +NEXT_PUBLIC_HOST39_CARDS_URL=http://localhost:3010 + +# Required by the server when running WITHOUT Docker (`cd server && npm run dev`). +# The docker-compose stack sets DATABASE_URL itself, so it is ignored there. +# `docker compose up db -d` exposes Postgres on host port 5434. +DATABASE_URL=postgresql://host39:host39-local@localhost:5434/host39 diff --git a/.env.prod.example b/.env.prod.example index eb1bc76..11f9ed6 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -12,6 +12,14 @@ JWT_EXPIRES_IN=7d # Frontend origin (used for CORS + email links) FRONTEND_URL=https://host39.org +# Host that actually serves public cards. Baked into /.well-known/ai-catalog.json +# entries and each card's _meta.publicUrl. If unset, these default to +# http://localhost:3010, which is wrong in production. +PUBLIC_BASE_URL=https://agentcards.host39.org + # ── Web (baked into Next.js bundle at build time) ───────────────────────────── # API is served under the same domain as the frontend — no CORS NEXT_PUBLIC_HOST39_API_URL=https://host39.org +# Card-serving host shown/copied in the dashboard. Public cards live on +# agentcards.host39.org, NOT host39.org (which routes non-API paths to the UI). +NEXT_PUBLIC_HOST39_CARDS_URL=https://agentcards.host39.org diff --git a/README.md b/README.md index 96153cb..b725007 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,13 @@ docker compose up --build | Variable | Required | Default | Description | |---|---|---|---| -| `JWT_SECRET` | yes (prod) | dev default | Must be ≥ 32 chars in production | +| `DATABASE_URL` | yes | (none) | Postgres connection string. Set by docker-compose; required explicitly when running the server without Docker. | +| `JWT_SECRET` | yes (prod) | dev default | Must be at least 16 chars in production | | `JWT_EXPIRES_IN` | no | `7d` | Token lifetime | | `FRONTEND_URL` | no | `http://localhost:3002` | Used for CORS and redirects | +| `PUBLIC_BASE_URL` | no | `http://localhost:3010` | Host that serves public cards. Baked into `/.well-known/ai-catalog.json` and each card's `_meta.publicUrl`. Set to `https://agentcards.host39.org` in production. | | `NEXT_PUBLIC_HOST39_API_URL` | no | `http://localhost:3010` | API base URL baked into the frontend | +| `NEXT_PUBLIC_HOST39_CARDS_URL` | no | falls back to API URL | Card-serving host shown/copied in the dashboard. Set to `https://agentcards.host39.org` in production. | | `POSTGRES_PASSWORD` | yes | `host39-local` in dev | Postgres password | | `PORT` | no | `3010` | API server port | diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 05bd352..6534dd2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -36,6 +36,10 @@ services: JWT_SECRET: ${JWT_SECRET} JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d} FRONTEND_URL: ${FRONTEND_URL} + # Host that actually serves public cards (agentcards.host39.org). + # Baked into /.well-known/ai-catalog.json entries and each card's + # _meta.publicUrl, so it must NOT default to localhost in production. + PUBLIC_BASE_URL: ${PUBLIC_BASE_URL} depends_on: db: condition: service_healthy @@ -47,6 +51,7 @@ services: dockerfile: Dockerfile args: NEXT_PUBLIC_HOST39_API_URL: ${NEXT_PUBLIC_HOST39_API_URL} + NEXT_PUBLIC_HOST39_CARDS_URL: ${NEXT_PUBLIC_HOST39_CARDS_URL} environment: PORT: 3002 depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index f9c8365..47206fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,12 +17,18 @@ services: server: build: { context: ./server, dockerfile: Dockerfile } environment: - NODE_ENV: production + # This is the local dev stack, so run as development: the shipped default + # JWT_SECRET is accepted (the production guards reject it) and logs are + # verbose. Use docker-compose.prod.yml for production. + NODE_ENV: development PORT: 3010 DATABASE_URL: postgresql://host39:${POSTGRES_PASSWORD:-host39-local}@db:5432/host39 JWT_SECRET: ${JWT_SECRET:-host39-dev-secret} JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d} FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3002} + # Base URL cards are served from, baked into catalog/_meta public URLs. + # In dev the API serves cards on the same origin. + PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:3010} ports: ["3010:3010"] depends_on: db: @@ -34,6 +40,7 @@ services: dockerfile: Dockerfile args: NEXT_PUBLIC_HOST39_API_URL: ${NEXT_PUBLIC_HOST39_API_URL:-http://localhost:3010} + NEXT_PUBLIC_HOST39_CARDS_URL: ${NEXT_PUBLIC_HOST39_CARDS_URL:-http://localhost:3010} environment: PORT: 3002 ports: ["3002:3002"] diff --git a/web/Dockerfile b/web/Dockerfile index 35143ad..61df996 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -5,6 +5,8 @@ RUN npm ci COPY . . ARG NEXT_PUBLIC_HOST39_API_URL ENV NEXT_PUBLIC_HOST39_API_URL=$NEXT_PUBLIC_HOST39_API_URL +ARG NEXT_PUBLIC_HOST39_CARDS_URL +ENV NEXT_PUBLIC_HOST39_CARDS_URL=$NEXT_PUBLIC_HOST39_CARDS_URL RUN npm run build FROM node:20-alpine AS runner diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 64198be..16ea79b 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -152,13 +152,24 @@ export async function deleteCard(id: string): Promise { } // URL generation helpers (client-side preview) +// +// Public agent cards are served from the card-serving host (in production, +// `agentcards.host39.org`), which is a *different* origin from the API/dashboard +// host (`host39.org`). Use NEXT_PUBLIC_HOST39_CARDS_URL so the URL we show and +// copy actually resolves. Fall back to the API URL for local dev, where the API +// serves cards on the same origin. +const CARDS_BASE = + process.env.NEXT_PUBLIC_HOST39_CARDS_URL ?? + process.env.NEXT_PUBLIC_HOST39_API_URL ?? + "http://localhost:3010"; + export function getPublicUrl( identityType: "domain" | "email", domainOrEmail: string, slug: string, baseUrl?: string ): string { - const base = baseUrl ?? (process.env.NEXT_PUBLIC_HOST39_API_URL ?? "http://localhost:3010"); + const base = baseUrl ?? CARDS_BASE; if (identityType === "domain") { return `${base}/${domainOrEmail}/${slug}.json`; }