Appearance
Self-hosting (sovereign deployment)
Futuros is designed to be redeployed in-region. This is Pillar 2 (the Sovereign Model): a LATAM government or multilateral can run the whole platform — the atlas, the baked corpus, the public API, and the AI assistant — on infrastructure inside its own jurisdiction, pointing inference at an in-region open-weight gateway so the corpus and every user query stay on sovereign soil.
Because the platform is a static SPA + baked JSON corpus + a small set of serverless functions, self-hosting comes in two tiers.
Two tiers, one repo
The repo's Dockerfile + Caddyfile produce a static deployment: the SPA, the baked public/data/ corpus, the Public API v1, and the embeds. That container (Caddy serving dist/) does not run the api/ serverless functions — so the chat, MCP server, data-trust contribute/revoke, consensus, and alerts endpoints are not served by it. To run those interactive services you need a host that executes the api/* Node functions (Vercel, or any Node/functions runtime), plus the env below.
Tier 1 — static sovereign mirror (Docker + Caddy)
The included multi-stage Dockerfile builds with Bun and ships a tiny Caddy image serving the static build:
dockerfile
FROM oven/bun:1.3 AS build
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
ARG VITE_GATE_PASSWORD # baked at build time (default: Futuros42)
ENV VITE_GATE_PASSWORD=${VITE_GATE_PASSWORD}
RUN bun run build
FROM caddy:2-alpine
COPY --from=build /app/dist /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 8080
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]Build and run:
bash
docker build -t futuros --build-arg VITE_GATE_PASSWORD=your-gate-pass .
docker run -p 8080:8080 -e PORT=8080 futuros
# → http://localhost:8080The Caddyfile listens on :{$PORT:8080}, gzips/zstd-compresses, sets the same security headers Vercel does, caches hashed /assets/* immutably and the /data/* corpus briefly, serves the marketing landing at /, and does SPA fallback (try_files {path} /index.html) so TanStack Router routes resolve. railway.json wires the same Dockerfile for Railway (builder DOCKERFILE, health check /intro).
This tier is a complete, offline-capable data-sovereignty mirror: the full atlas UI and the entire cited corpus, hosted wherever you run the container. Anything that needs a server (below) degrades honestly — the app stays usable, the interactive features announce they are not configured rather than faking a result.
Tier 2 — full platform (serverless functions + sovereign inference)
The interactive surfaces live in api/* as Node serverless functions (chat, mcp, contribute, revoke, consensus, alerts-subscribe, alerts-digest). Deploy them on a runtime that executes them (Vercel is the reference target) and set the env vars below. The static assets and corpus are the same build as Tier 1.
Environment variables
Grouped by feature. Everything is optional — each unset key degrades a specific feature honestly (see the table further down). Client keys are VITE_-prefixed and baked into the build; the rest are server-side only.
Note: the repo's
env.exampledocuments only the map, analytics, and chat/ retrieval keys. The trust-ledger (Upstash), alerts (Mailgun / webhooks), and Perplexity keys below are read by the code but are not inenv.example— the authoritative list is here.
Build / client (baked, VITE_-prefixed):
| Var | Feature | Without it |
|---|---|---|
VITE_MAPBOX_TOKEN | Mapbox basemap on the atlas | MapLibre fallback renders boundaries on a dark canvas |
VITE_GATE_PASSWORD | Password gate on the SPA (/atlas) | Falls back to in-source default Futuros42 |
VITE_POSTHOG_KEY | PostHog product analytics (prod domains only) | No-op |
VITE_POSTHOG_HOST | PostHog region override | Defaults to US Cloud |
AI assistant — inference provider (server-side; first match wins):
| Var(s) | Feature |
|---|---|
SOVEREIGN_INFERENCE_URL + SOVEREIGN_INFERENCE_KEY | Pillar 2 sovereign path. Points the same Anthropic-Messages agent loop at an in-region / self-hosted open-weight gateway (LiteLLM, or vLLM/TGI behind a Messages-compatible shim, serving Llama/Qwen/DeepSeek/Mistral). When both are set they win over OpenRouter/Anthropic and the chat shows a "sovereign mode" badge — the query + corpus never leave the host. |
SOVEREIGN_INFERENCE_MODEL | Your gateway's served-model-name (default local-model) |
SOVEREIGN_INFERENCE_LABEL | UI label for the sovereign provider (default Soberano) |
OPENROUTER_API_KEY | Managed fallback: routes via OpenRouter's Anthropic-native endpoint (default model anthropic/claude-sonnet-4.6) |
ANTHROPIC_API_KEY | Calls api.anthropic.com directly (default claude-sonnet-4-6) |
CHAT_MODEL / CHAT_FOLLOWUP_MODEL | Override answer / follow-up model ids for the active provider |
Provide one provider. Selection order is sovereign → OpenRouter → Anthropic; with none set, the chat emits a not_configured event and declines rather than erroring.
AI assistant — retrieval & external web (server-side):
| Var(s) | Feature | Without it |
|---|---|---|
VOYAGE_API_KEY (+ CHAT_EMBED_MODEL, CHAT_RERANK_MODEL) | Semantic search_corpus over the baked index + cross-encoder rerank | Degrades to keyword/BM25 search (no hard failure) |
EXA_API_KEY | Chat's last-resort web fallback (web_search/fetch_url) + pulse ingestion | Chat answers from the corpus only and declines out-of-corpus questions |
PERPLEXITY_API_KEY (+ PERPLEXITY_MODEL) | perplexity_search live web synthesis (chat + MCP) | Tool returns a "not configured" note; answers stay corpus-only |
Data Trust (Pillar 1) & consensus — Upstash Redis (server-side):
| Var(s) | Feature | Without it |
|---|---|---|
UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN | Append-only trust ledger (/contribuir receipts, /api/revoke) and consensus vote store (/api/consensus) | Returns a valid receipt with persisted:false; consensus degrades to local-only — never fakes persistence |
The Vercel KV aliases KV_REST_API_URL / KV_REST_API_TOKEN are also accepted.
Alerts & email (server-side):
| Var(s) | Feature | Without it |
|---|---|---|
MAILGUN_API_KEY + MAILGUN_DOMAIN + MAILGUN_FROM | Email delivery for /api/alerts-subscribe confirmations and Data-Trust contribution/revoke notices, via Mailgun over fetch | alerts-subscribe returns a mailto: fallback instead of pretending to subscribe |
MAILGUN_NOTIFY / ALERTS_NOTIFY_EMAIL | Internal copy of subscribe/contribution notices | No internal copy |
ALERTS_WEBHOOKS | The weekly digest cron (/api/alerts-digest, Mon 13:00 UTC) dispatches the global anomaly digest to these comma-separated webhook URLs | Digest dispatches to nobody |
CRON_SECRET | Authenticates the cron trigger to /api/alerts-digest | Endpoint unguarded |
The weekly alerts digest is webhook-based (
ALERTS_WEBHOOKS), not email — Mailgun handles the subscribe confirmation and Data-Trust notices. Both exist; don't conflate them.
Ingestion (build-time scripts only — not needed to serve the app): the refresh pipeline reads keys such as OPENAQ_API_KEY, CLOUDFLARE_RADAR_TOKEN, YOUTUBE_API_KEY, REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET, SERPAPI_KEY, plus the EXA_/PERPLEXITY_/GDELT_ tuning vars. These matter only when you re-run ingestion (below), never to serve an already-baked corpus.
The corpus is static, baked at build time
There is no database in the serving path. The entire corpus under public/data/ is baked at build time and shipped as static files (the same files the Public API v1 exposes). Serving Futuros means serving those files — which is why Tier 1 (Caddy over dist/) is a complete data mirror.
To refresh the data you re-run the ingestion/bake scripts and rebuild:
bash
bun run ingest # multi-source ingest → parameter-cache + citations + signals
bun run bake:all # narratives, scores, indices, api, … → public/data/**
bun run build # tsc + vite; prebuild provenance gates must passThe prebuild step runs provenance gates (traceability, source deep-links, citations, scores, catalog) that fail the build if a figure lost its source — so a self-hosted rebuild preserves the honesty contract by construction. A sovereign operator can run this pipeline entirely in-region: baked corpus in-region, inference in-region, nothing leaves the host.
Recommended sovereign setup
- Stand up an in-region open-weight gateway (LiteLLM in front of vLLM/TGI serving Llama/Qwen/DeepSeek/Mistral) that speaks the Anthropic Messages API.
- Deploy the SPA + baked corpus (Tier 1 container) and the
api/*functions on in-region infrastructure. - Set
SOVEREIGN_INFERENCE_URL+SOVEREIGN_INFERENCE_KEY(+_MODEL,_LABEL). The chat badges "sovereign mode"; corpus + queries never leave your jurisdiction. - Add
UPSTASH_REDIS_REST_*only if you want the trust ledger / consensus persisted; otherwise those features degrade honestly.