Skip to content
Open
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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions .env.prod.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
5 changes: 5 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"]
Expand Down
2 changes: 2 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,24 @@ export async function deleteCard(id: string): Promise<void> {
}

// 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`;
}
Expand Down